第一章 基础(1)

1.1 执行请求

HttpClient最基本的功能是执行HTTP方法。一个HTTP方法的执行涉及到一个或多个HTTP请求和响应的交换,这通常是在HttpClient内部处理的。用户需要提供一个请求对象,HttpClient负责传输这个请求到目标服务器并返回相对应的响应的对象,如果执行不成功,则抛出异常。

HttpClient API的入口就是HttpClient接口。

以下是最简单的形式的执行请求的例子:

    public void chapter1_1() throws IOException {

        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            // handle response
        } finally {
            response.close();
        }

    }

1.1.1 HTTP请求

所有的HTTP请求都有一个请求头,其中包含方法名请求URIHTTP协议版本号

HttpClient支持所有HTTP/1.1规格中定义的HTTP方法,包括:GET,HEAD, POST, PUT, DELETE, TRACEOPTIONS。每种方法都有与之对应的类:HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTraceHttpOptions

请求URI是请求资源的统一资源描述符(Uniform Resource Identifier)。HTTP请求URI包含协议(protocol schema)、主机名(host name)、可选的端口(optional port)、资源路径(resource path)、可选的查询(optional query)和可选的分块(optional fragment)。

HttpGet httpGet = new HttpGet(
                "http://www.google.com/search?h1=en&q=httpclient&btnG=Google+Search&aq=f&oq=");

HttpClient 提供 URIBuilder工具类来简化创建和修改请求URI:

      URI uri = new URIBuilder()
                .setScheme("http")
                .setHost("www.google.com")
                .setPath("/search")
                .setParameter("h1", "en")
                .setParameter("q", "httpclient")
                .setParameter("btnG", "Google+Search")
                .setParameter("aq", "f")
                .setParameter("oq", "")
                .build();
        HttpGet httpGet1 = new HttpGet(uri);
        System.out.println(httpGet1.getURI());

1.1.2 HTTP响应

HTTP响应是服务器接收和处理完请求报文后所返回的报文。报文的第一行由协议版本、状态码和及其描述组成。

    public void chapter1_1_2() throws Exception {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");

        System.out.println(response.getProtocolVersion());
        System.out.println(response.getStatusLine().getStatusCode());
        System.out.println(response.getStatusLine().getReasonPhrase());
        System.out.println(response.getStatusLine().toString());
    }

输出>

HTTP/1.1
200
OK
HTTP/1.1 200 OK

1.1.3 报文头部

HTTP报文包含一些用于描述报文的头部信息,如内容长度、内容类型等等 。HttpClient提供方法去获取、添加、移除和列举这些头部信息。

    public void chapter1_1_3() {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
        response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
        response.addHeader("Set-Cookie", "c2=b; path=\"/\"; domain=\"localhost\"");

        Header h1 = response.getFirstHeader("Set-Cookie");
        System.out.println(h1);

        Header h2 = response.getLastHeader("Set-Cookie");
        System.out.println(h2);

        Header[] hs = response.getHeaders("Set-Cookie");
        System.out.println(hs.length);
    }

输出>

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/"; domain="localhost"
2

获取所有给定类型头部信息最有效方式是使用HeaderIterator接口。

    public void chapter1_1_3_2() {
        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
        response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
        response.addHeader("Set-Cookie", "c2=b; path=\"/\"; domain=\"localhost\"");

        BasicHeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator("Set-Cookie"));
        while (it.hasNext()) {
            HeaderElement elem = it.nextElement();
            System.out.println(elem.getName() + " = " + elem.getValue());
            NameValuePair[] params = elem.getParameters();
            for (NameValuePair param : params) {
                System.out.println(" " + param);
            }
        }
    }

输出>

c1 = a
 path=/
 domain=localhost
c2 = b
 path=/
 domain=localhost

1.1.4 HTTP实体

HTTP报文可以携带与请求或响应相关联的内容实体。因为实体是可选的,并不是所有的请求和响应中都包含实体。使用实体的请求被称为包含实体的请求(entity enclosing request)。HTTP规格中定义了2种包含实体的请求的方法:POSTPUT
响应通常会包含一个内容实体。但也是例外,如HEAD方法的响应和 204 No Content, 304 Not Modified, 205 Reset Content 响应。

HttpClient区分三种实体类型,取决于它们内容的来源:

  • streamed: 内容是从流(stream)中获得,或联机生成的。这里包含从HTTP响应中的实体。流式实体(streamed entities)通常是不能重复的。
  • self-contained: 内容是在内存里。自包含实体(self-contained entities)通常是可重复的。这种类型的实体最多用于包含实体的请求(entity enclosing request)
  • wrapping: 内容从另一实体获得。

当从HTTP响应中获取数据流时,这些区分对于连接管理来说是重要的。对于应用创建的请求实体,且仅使用HttpClient来发送,streamed 还是 self-contained 的区别就不重要了。这种情况下,建议把不可重复的实体归为streamed类型,可重复的为self-contained类型。

1.1.4.1 可重复实体

可重复实体是指它的内容能够被重复读取。只有自包含实体(self-contained entities)才是可重复的(如ByteArrayEntityStringEntity)。

1.1.4.2 使用HTTP实体

因为实体可表示二进制和字符内容,所以它是支持字符编码的。
实体被创建的时机有 a) 执行包含内容的请求; b) 请求成功后,响应体使用实体将结果返回。

为了从输入报文的实体中读取内容,我们可以通过HttpEntity#getContent()方法获取输入流java.io.InputStream, 或者我们可以通过HttpEntity#writeTo(OutpusStream)方法将其写到另一个给定的输出流中。

当从响应报文中接收到实体后,HttpEntity#getContentTypeHttpEntity#getContentLength方法可以用来读取通用的元数据,如Content-TypeContent-Length头部信息(如果有的话)。Content-Type头部对于文本类型的多媒体类型(如text/plaintext/html)来说可能包含字符编码的信息,HttpEntity#getContentEncoding()方法可能用来读取该信息。如果头部不可用的话,HttpEntity#getContentLength返回-1,HttpEntity#getContentType返回NULL。如果Content-Type可用的话,Header对象将会被返回。

当给输出报文创建实体,元数据必须使用实体的创建者方法来创建:

    public void chapter1_1_4() throws IOException {
        StringEntity myEntity = new StringEntity("important message",
                ContentType.create("text/plain", "UTF-8"));

        System.out.println(myEntity.getContentType());
        System.out.println(myEntity.getContentLength());
        System.out.println(EntityUtils.toString(myEntity));
        System.out.println(EntityUtils.toByteArray(myEntity).length);
    }

输出>

Content-Type: text/plain; charset=UTF-8
17
important message
17

1.1.5 确保释放底层资源

为了确保正确地的释放系统资源,我们必须关闭实体关联的内容流(stream)或者响应(response)本身。

    public void chapter1_1_5() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                InputStream inputStream = entity.getContent();
                try {
                    // do something with inputStream
                } finally {
                    inputStream.close();
                }
            }
        } finally {
            response.close();
        }
    }

关闭内容流和关闭响应的区别在于,前者通过消费实体内容来试图保持底层连接,后者会立即关闭并且丢弃该连接。

请注意HttpEntity#writeTo(OUtputStream)方法也需要确保正确释放系统资源。如果这个方法通过调用HttpEntity#getContent方法来获取的java.io.InputStream实例,这也需要在一个finally子句中将其关闭。

我们也可以使用EntityUtils#consume(HttpEntity)方法来确认实体内容被完全消费并且底层流被关闭。

有一种情况,如果仅需要读取响应中的一部分内容,并且报文剩余内容的性能代价和保持连接的代价太高的话,我们可以通过关闭响应来结束内容流。

    public void chapter1_1_5_1() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                InputStream inputStream = entity.getContent();
                int byteOne = inputStream.read();
                int byteTwo = inputStream.read();
                // Do not need the rest
            }
        } finally {
            response.close();
        }
    }

连接将不会被重用,而且被该连接持有的所有资源将会被正确地释放。

1.1.6 消费实体内容

消费一个实体的内容推荐的方法是使用HttpEntity#getContent()HttpEntity#writeTo(OutpusStream)方法。HttpClient也包含EntityUtils类,其他包含一些静态方法可以理容易地读取实体内容或信息。这样我们就可以使用这个类的方法来读取整个字符或字节数据内容,而不是直接操作java.io.InputStream。然而,EntityUtils的使用是非常不推荐的,除非响应实体是来源于一个受信的HTTP服务器并且内容的长度是有限的。

    public void chapter1_1_6() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");
        CloseableHttpResponse response = httpClient.execute(httpGet);
        try {
            HttpEntity entity = response.getEntity();
            if (Objects.nonNull(entity)) {
                long len = entity.getContentLength();
                if (len != -1 && len < 2048) {
                    System.out.println(EntityUtils.toString(entity));
                } else {
                    // Stream content out
                    // content length is too large
                }
            }
        } finally {
            response.close();
        }
    }

在某些情况下,我们需要重复读取实体内容。此时,实体内容必须用某种方式来缓冲,或者在内容或者在磁盘中。完成缓存的最简单方式就是把原始的实体用BufferedHttpEntity类来包装。这能够使原来的实体被读进内存的缓存区中。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
  entity = new BufferedHttpEntity(entity);
}

1.1.7 生产实体内容

HttpClient提供一些类,这些类用来高效地将实体内容通过HTTP连接输出到流。这些类的实例能够与包含实体的请求关联, 如POSTPUT。HttpClient提供一些类来作为最常见的数据的容器,如字符串、字节数组、输入流和文件:StringEntityByteArrayEntityInputStreamEntityFileEntity

    public void chapter1_1_7() throws Exception {
        File file = new File("somefile.txt");
        FileEntity entity = new FileEntity(file, ContentType.create("text/plain", "UTF-8"));

        HttpPost httpPost = new HttpPost("http://localhost/action.do");
        httpPost.setEntity(entity);
    }

请注意InputStreamEntity不是可重复的,因为它只能从底层数据流中读取一次。通常推荐去实现一个自定义的HttpEntity,使其成为self-contained类型的,而不是去使用InputStreamEntityFileEntity就是一个很好的例子。

1.1.7.1 HTML表单

很多应用需要去模拟提交HTML表单的过程,例如,为了登陆或提交输入数据。HttpClient提供实体类UrlEncodedFormEntity来帮助这个提交过程。

    public void chapter1_1_7_1() throws Exception {
        List<NameValuePair> formParams = new ArrayList<>();
        formParams.add(new BasicNameValuePair("param1", "value1"));
        formParams.add(new BasicNameValuePair("param2", "value2"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
        
        HttpPost httpPost = new HttpPost("http://localhost/handle.do");
        httpPost.setEntity(entity);
    }

UrlEncodedFormEntity会使用URL编码来对参数进行编码,并且产生如下内容:

param1=value1&param2=value2

1.1.7.2 内容分块(Content chunking)

通常推荐让HttpClient基于传输的HTTP报文的属性去选择使用最合适的传输编码。然而,通过设置HttpEntity#setChunked()true来通知HttpClient使用chunk编码是可能的。当然,这仅仅只是一个提示而已。如果使用不支持chunk编码的HTTP协议,如HTTP/1.0,该值将会被忽略。

1.1.8 响应处理器(Response handlers)

最简单并且最方便的方式去处理响应是使用ResponseHandler接口,该接口包含handleResponse(HttpResponse response)方法。该方法完全地把用户从连接管理中解放出来。当使用ResponseHandler,HttpClient会自动地的确保连接会被释放回给连接管理器,不管执行请求是否成功或异常。

    public void chapter1_1_8() throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://www.baidu.com");

        ResponseHandler<MyJsonObject> rh = response -> {
            StatusLine statusLine = response.getStatusLine();
            HttpEntity entity = response.getEntity();
            if (statusLine.getStatusCode() >= 300) {
                throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
            }
            if (Objects.isNull(entity)) {
                throw new ClientProtocolException("Response contains no content");
            }

            Gson gson = new GsonBuilder().create();
            ContentType contentType = ContentType.getOrDefault(entity);
            Charset charset = contentType.getCharset();
            Reader reader = new InputStreamReader(entity.getContent(), charset);
            return gson.fromJson(reader, MyJsonObject.class);

        };
        
        MyJsonObject myJsonObject = httpClient.execute(httpGet, rh);

    }
    
    static class MyJsonObject {
        String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容