# HttpClient

返回开源分支:java-实用工具

# HttpClient简介与HTTP简介

🔝🔝返回顶部

超文本传输协议(HTTP)也许是当今互联网上所使用的最重要的协议了。
Web services,联网设备和网络计算的发展,都持续扩展了HTTP协议的角色,超越了用户使用的Web浏览器范畴,同时,也增加了需要HTTP协议支持的应用程序的数量。 尽管java.net包提供了通过HTTP访问资源的基本功能,但它缺少足够的灵活性和其它很多应用程序需要的功能。
HttpClient通过提供一个有效的,保持更新的,功能丰富的软件包来实现客户端最新的HTTP标准和建议,来弥补java.net包的在某些技术上的空白。
HttpClient为扩展而设计,同时为基本的HTTP协议提供强大的支持。有一些人会对HttpClient感兴趣,这些人通常是构建 HTTP 客户端应用程序(比如web浏览器,web服务客户端,利用或扩展 HTTP 协议进行来实现的分布式通信系统)的开发人员。

🔝🔝简介

# 范围

🔝🔝返回顶部

  • 基于HttpCore的客户端HTTP通信库
  • 基于经典 I/O(阻塞I/O)
  • 内容无关

# 不能做的

🔝🔝返回顶部

  • HttpClient 不是一个浏览器。它是一个客户端的 HTTP 通信实现库。HttpClient 的目标是发送和接收HTTP 报文。
  • HttpClient 不会去处理内容,执行嵌入在 HTML页面中的javascript 代码,猜测内容类型,如果没有明确设置,否则不会重新格式化请求/重定向URI,或其它和HTTP通信无关的功能。

# 2.HTTP协议简介

🔝🔝返回顶部

在使用HttpClient之前,你需要简单了解下HTTP协议及其基本概念:

  • HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW,World Wide Web )服务器传输超文本到本地浏览器的传送协议。
  • HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
  • HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。

它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器

# 2.1 HTTP请求

🔝🔝返回顶部

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host    img.mukewang.com
User-Agent    Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)
Accept    image/webp,image/*,*/*;q=0.8
Referer    http://www.imooc.com/
Accept-Encoding    gzip, deflate, sdch
Accept-Language    zh-CN,zh;q=0.8
1
2
3
4
5
6
7
  • 第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本.
  • 第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息
    • HOST将指出请求的目的地.
    • User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础.该信息由你的浏览器来定义,。
  • 第三部分:空行,请求头部后面的空行是必须的,即使第四部分的请求数据为空,也必须有空行。
  • 第四部分:请求数据也叫主体,可以添加任意的其他数据。

这个例子的请求数据为空。

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive

name=Professional%20Ajax&publisher=Wiley
1
2
3
4
5
6
7
8
  • 第一部分:请求行,第一行明了是post请求,以及http1.1版本。
  • 第二部分:请求头部,第二行至第六行。
  • 第三部分:空行,第七行的空行。
  • 第四部分:请求数据,第八行。

# 2.2 HTTP之响应消息Response

🔝🔝返回顶部

一般情况下,服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息。
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8

<html>
      <head></head>
      <body>
            <!--body goes here-->
      </body>
</html>
1
2
3
4
5
6
7
8
9
10

第一部分:状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为(ok)
第二部分:消息报头,用来说明客户端要使用的一些附加信息
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8
第三部分:空行,消息报头后面的空行是必须的
第四部分:响应正文,服务器返回给客户端的文本信息。
空行后面的html部分为响应正文。

# 2.3 HTTP之状态码

🔝🔝返回顶部

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

状态代码

  • 1xx:指示信息--表示请求已接收,继续处理
  • 2xx:成功--表示请求已被成功接收、理解、接受
  • 3xx:重定向--要完成请求必须进行更进一步的操作
  • 4xx:客户端错误--请求有语法错误或请求无法实现
  • 5xx:服务器端错误--服务器未能实现合法的请求

常见状态码

  • 200 OK //客户端请求成功
  • 400 Bad Request //客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden //服务器收到请求,但是拒绝提供服务
  • 404 Not Found //请求资源不存在,eg:输入了错误的URL
  • 500 Internal Server Error //服务器发生不可预期的错误
  • 503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
  • 更多状态码

# 2.4 HTTP请求方法

🔝🔝返回顶部

据HTTP标准,HTTP请求可以使用多种请求方法。

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

请求

  • GET 请求指定的页面信息,并返回实体主体。
  • HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
  • POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
  • POST请求可能会导致新的资源的建立和/或已有资源的修改。
  • PUT 从客户端向服务器传送的数据取代指定的文档的内容。
  • DELETE 请求服务器删除指定的页面。
  • CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
  • OPTIONS 允许客户端查看服务器的性能。
  • TRACE 回显服务器收到的请求,主要用于测试或诊断。

# 二.构建开发环境

🔝🔝返回顶部

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
1
2
3
4
5

# 三.HttpClient执行基本请求

🔝🔝返回顶部

HttpClient最基本的功能是执行HTTP方法,一次 HTTP 方法的执行包含一个或多个 HTTP 请求/响应的交互,通常由 HttpClient的内部来处理。使用者需要提供一个Request对象来执行HTTP请求,HttpClient就会把请求传送给目标服务器并返回一个相对应的Response对象,如果执行不成功,将会抛出一个异常。

显然,HttpClient API 的主要切入点就是定义描述上述契约的HttpClient接口。 一个简单的请求执行事例:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
<...>
} finally {
response.close();
1
2
3
4
5
6
7

# 1. HTTP 请求(Request)

🔝🔝返回顶部

所有 HTTP 请求都有一个请求起始行,这个起始行由方法名,请求 URI 和 HTTP 协议版本组成。HttpClient很好地支持了HTTP/1.1规范中所有的HTTP方法:GET,HEAD, POST,PUT, DELETE, TRACE 和 OPTIONS。每个方法都有一个特别的类:HttpGet,HttpHead, HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。URI是统一资源标识符的缩写,用来标识与请求相符合的资源。
HTTP 请求URI包含了一个协议名称,主机名,可选端口,资源路径,可选的参数,可选的片段。

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

HttpClient提供了URIBuilder工具类来简化创建、修改请求 URIs。

URI uri = new URIBuilder()
.setScheme("http")
.setHost("www.google.com")
.setPath("/search")
.setParameter("q", "httpclient")
.setParameter("btnG", "Google Search")
.setParameter("aq", "f")
.setParameter("oq", "")
.build();
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());
1
2
3
4
5
6
7
8
9
10
11

输出:

http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
1

# 2.HTTP 响应(Response)

🔝🔝返回顶部

HTTP 相应是服务器接收并解析请求信息后返回给客户端的信息,它的起始行包含了一个协议版本,一个状态码和描述状态的短语。

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());
1
2
3
4
5
6

输出:

HTTP/1.1
200
OK
HTTP/1.1 200 OK
1
2
3
4

# 3. 处理报文首部(Headers)

🔝🔝返回顶部

一个HTTP报文包含了许多描述报文的首部,比如内容长度,内容类型等。HttpClient提供了一些方法来取出,添加,移除,枚举首部。

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=\"/\", c3=c; 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);
1
2
3
4
5
6
7
8
9

输出:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
2
1
2
3

获得所有指定类型首部最有效的方式是使用HeaderIterator接口

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=\"/\", c3=c; domain=\"localhost\"");
HeaderIterator it = response.headerIterator("Set-Cookie");
while (it.hasNext()) {
System.out.println(it.next());
}
1
2
3
4
5
6
7

输出:

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

HttpClient也提供了其他便利的方法吧HTTP报文转化为单个的HTTP元素。

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=\"/\", c3=c; domain=\"localhost\"");
HeaderElementIterator 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 (int i = 0; i < params.length; i++) {
System.out.println(" " + params[i]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

输出:

c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost
1
2
3
4
5
6
7

# 4. HTTP实体(HTTP Entity)

🔝🔝返回顶部

HTTP报文能够携带与请求或相应相关联的内容实体。实体存在于某些请求、响应中,它门是可选的。使用实体的请求被称为内含实体请求(entity enclosing requests)。HTTP规范定义了两种内含实体请求,POST和PUT。而响应总是内含实体(有些响应不符合这一规则,
比如,对HEAD方法的响应和状态为204 No Content, 304 Not Modified, 205 Reset Content的响应)。

依据实体的内容来源,HttpClient区分出三种实体:

  • 流式实体(streamed):内容来源于一个流,或者在运行中产生【译者:原文为generated on the fly】。特别的,这个类别包括从响应中接收到的实体。流式实体不可重复。
  • 自包含实体(self-contained):在内存中的内容或者通过独立的连接/其他实体获得的内容。自包含实体是可重复的。这类实体大部分是HTTP内含实体请求。
  • 包装实体(wrapping):从另外一个实体中获取内容。

# 4.1 可重复实体

一个实体能够被重复,意味着它的内容能够被读取多次。它仅可能是自包含实体(像ByteArrayEntity或StringEntity)

# 4.2 使用HTTP实体

由于一个实体能够表现为二进制和字符内容,它支持二进制编码(为了支持后者,即字符内容)。

实体将会在一些情况下被创建:

当执行一个含有内容的请求时或者当请求成功,响应体作为结果返回给客户端时。为了读取实体的内容,可以通过HttpEntity#getContent() 方法取出输入流,返回一个java.io.InputStream,或者提供一个输出流给HttpEntity#writeTo(OutputStream) 方法,它将会返回写入给定流的所有内容。
当实体内部含有信息时,使用HttpEntity#getContentType()HttpEntity#getContentLength()方法将会读取一些基本的元数据,比如Content-Type和Content-Length这样的首部(如果他们可用的话),由于Content-Type首部能够包含文本MIME类型(像 text/plain 或text/html),它也包含了与MIME类型相对应的字符编码,HttpEntity#getContentEncoding()方法被用来读取这些字符编码。
如果对应的首部不存在,则Content-Length的返回值为-1,Content-Type返回值为NULL。如果Content-Type是可用的,一个Header类的对象将会返回。

当我们构建一个具有可用信息的实体时,元数据将会被实体构建器提供。

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);
1
2
3
4
5
6

输出:

Content-Type: text/plain; charset=utf-8
17
important message
17
1
2
3
4

# 5. 确保释放低级别的资源

🔝🔝返回顶部

为了确保正确的释放系统资源,你必须关掉与实体与实体相关的的内容流,还必须关掉响应本身。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
        HttpEntity entity = response.getEntity();
        if (entity != null) {
        InputStream instream = entity.getContent();
        try {
            // do something useful
        } finally {
            instream.close();
        }
    }
} finally {
        response.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

关闭内容流和关闭响应的不同点是:前者将会通过消费实体内容保持潜在的连接,而后者迅速的关闭并丢弃连接。
请注意,一旦实体被HttpEntity#writeTo(OutputStream)方法成功地写入时,也需要确保正确地释放系统资源。如果方法获得通过HttpEntity#getContent(),它也需要在一个finally子句中关闭流。
当使用实体时,你可以使用EntityUtils#consume(HttpEntity)方法来确保实体内容完全被消费并且使潜在的流关闭。

某些情况,整个响应内容的仅仅一小部分需要被取出,会使消费其他剩余内容的性能代价和连接可重用性代价太高,这时可以通过关闭响应来终止内容流(例子中可以看出,实体输入流仅仅读取了两个字节,就关闭了响应,也就是按需读取,而不是读取全部响应)。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
        InputStream instream = entity.getContent();
    int byteOne = instream.read();
        int byteTwo = instream.read();
    // Do not need the rest
}
} finally {
    response.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这样,连接将不会被重用,它分配的所有级别的资源将会被解除。

# 6.消费实体内容

为了消费实体内容,推荐的方式是使用HttpEntity#getContent()或者 HttpEntity#writeTo(OutputStream)方法。HttpClient也提供了一个EntityUtils类,它有几个静态方法更容易的从实体中读取内容或信息。取代了直接读取java.io.InputStream,你可以通过这个类的方法取出全部内容体并放入一个String 中或者byte数组中。可是,强烈不建议使用EntityUtils,除非响应实体来自于信任的HTTP服务器并且知道它的长度。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
        long len = entity.getContentLength();
        if (len != -1 && len < 2048) {
                System.out.println(EntityUtils.toString(entity));
    } else {
        // Stream content out
    }
}
} finally {
    response.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在某些情况下,多次读取实体内容是必要的。在这种情况下,实体内容必须以一些方式缓冲,内存或者硬盘中。为了达到这个目的,最简单的方法是把原始的实体用BufferedHttpEntity类包装起来。这将会使原始实体的内容读入一个in-memory缓冲区。所有方式的实体包装都是代表最原始的那个实体。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
    entity = new BufferedHttpEntity(entity);
}
1
2
3
4
5

# 7.生产实体内容

🔝🔝返回顶部

HttpClient提供了几个类,用来通过HTTP连接高效地传输内容。这些类的实例均与内含实体请求有关,比如POST和PUT,它们能够把实体内容封装进友好的HTTP请求中。对于基本的数据容器String, byte array, input stream, and file,HttpClient为它们提供了几个对应的类:StringEntity, ByteArrayEntity, InputStreamEntity, and FileEntity

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);
1
2
3
4

请注意InputStreamEntity是不可重复的,因为它仅仅能够从数据流中读取一次。
一般建议实现一个定制的HttpEntity类来代替使用一般的InputStreamEntity。FileEntity将会是一个好的出发点。

# 7.1 HTML表单

许多应用需要模仿一个登陆HTML表单的过程,比如:为了登陆一个web应用或者提交输入的数据。HttpClient提供了UrlEncodedFormEntity类来简化这个过程。

List<NameValuePair> formparams = new ArrayList<NameValuePair>();
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/handler.do");
httppost.setEntity(entity);
1
2
3
4
5
6
7

UrlEncodedFormEntity实例像上面一样使用URL编码方式来编码参数并生成下面的内容: param1=value1¶m2=value2

# 7.2 内容分块

通常,我们推荐让HttpClient选择基于被传递的HTTP报文属相最合适的传输编码方式。可能地,可以通过设置HttpEntity#setChunked()为true来通知HttpClient你要进行分块编码。
注意HttpClient将会使用这个标志作为提示。当使用一些不支持分块编码的HTTP版本(比如HTTP/1.0.)时,这个值将会忽略(分块编码是是HTTP1.1协议中定义的Web用户向服务器提交数据的一种方法)。

StringEntity entity = new StringEntity("important message",
                    ContentType.create("plain/text", Consts.UTF_8));
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);
1
2
3
4
5

# 8.响应处理器

🔝🔝返回顶部

最简单、最方便的方式来处理响应是使用ResponseHandler接口,它包含了一个handleResponse(HttpResponse response)方法。这个方法减轻使用者对于连接管理的担心。
当你使用ResponseHandler时,无论是请求执行成功亦或出现异常,HttpClient将会自动地确保连接释放回连接管理器中。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {
    @Override
    public JsonObject handleResponse(final HttpResponse response) throws IOException {
        StatusLine statusLine = response.getStatusLine();
        HttpEntity entity = response.getEntity();
        if (statusLine.getStatusCode() >= 300) {
            throw new HttpResponseException(statusLine.getStatusCode(),
                statusLine.getReasonPhrase());
        }
        if (entity == null) {
                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 myjson = client.execute(httpget, rh);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 1. HttpClient接口简介

🔝🔝返回顶部

HttpClient代表HTTP请求执行的最基本约定。它没有强加限制或具体细节给请求执行过程,它保留了连接管理,状态管理,认证,重定向等处理细节的个人实现。使用额外的功能来装饰这个接口是非常容易的,比如设置响应体缓存。

HttpClient实现作为特殊目的的处理器或策略接口的门面,负责处理HTTP协议特定的方面,比如重定向处理,认证处理,为连接持久化做决定或者保持持续连接。这使得用户很容易使用自定义的实现来取代那些默认的实现。

ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response,HttpContext context) {
            long keepAlive = super.getKeepAliveDuration(response, context);
            if (keepAlive == -1) {
            // 如果keep-alive的值内有被服务器明确设置,保持持续连接5秒
            keepAlive = 5000;
        }
        return keepAlive;
        }
    };
    CloseableHttpClient httpclient = HttpClients.custom().setKeepAliveStrategy(keepAliveStrat).build();
1
2
3
4
5
6
7
8
9
10
11
12

# 2. HttpClient线程安全

🔝🔝返回顶部

HttpClient的实现是线程安全的。对于不同的请求执行(允许在多个线程内),这个类的相同实例是可复用的。

# 3. HttpClient资源释放

🔝🔝返回顶部

当一个HttpClient实例不再需要时并且它不在连接管理器之内时,必须通过CloseableHttpClient#close()方法关闭。

CloseableHttpClient httpclient = HttpClients.createDefault();
    try {
        <...>
    } finally {
        httpclient.close();
    }
1
2
3
4
5
6

# 五. HttpClient执行上下文HttpContext

🔝🔝返回顶部

最初,HTTP是被设计成无状态的,面向请求-响应的协议。然而,现实世界中的应用程序经常需要通过一些逻辑相关的请求-响应交换来保持状态信息。 为了使应用程序能够维持一个过程状态, HttpClient允许HTTP请求在一个特定的执行上下文中来执行--称为HTTP上下文。如果相同的上下文在连续请求之间重用,那么多种逻辑相关的请求可以参与到一个逻辑会话中。HTTP上下文功能和java.util.Map<String,Object>很相似。 它仅仅是任意命名参数值的集合。应用程序可以在请求之前填充上下文属性,也可以在执行完成之后来检查上下文属性。

HttpContext能够包含任意的对象,因此在两个不同的线程中共享上下文是不安全的,建议每个线程都一个它自己执行的上下文。

在HTTP请求执行的这一过程中, HttpClient添加了下列属性到执行上下文中:

属性

HttpConnection实例代表连接到目标服务器的当前连接。
HttpHost实例代表连接到目标服务器的当前连接。
HttpRoute实例代表了完整的连接路由。
HttpRequest实例代表了当前的HTTP请求。最终的HttpRequest对象在执行上下文中总是准确代表了状态信息,因为它已经发送给了服务器。每个默认的HTTP/1.0 和 HTTP/1.1使用相对的请求URI,可是,请求以non-tunneling模式通过代理发送时,URI会是绝对的。
HttpResponse实例代表当前的HTTP响应。
java.lang.Boolean对象是一个标识,它标志着当前请求是否完整地传输给连接目标。
RequestConfig代表当前请求配置。
java.util.List<URI>对象代表一个含有执行请求过程中所有的重定向地址。

# 使用HttpClientContext适配器类来简化上下文状态的活动

HttpContext context = <...>
    HttpClientContext clientContext = HttpClientContext.adapt(context);
    HttpHost target = clientContext.getTargetHost();
    HttpRequest request = clientContext.getRequest();
    HttpResponse response = clientContext.getResponse();
    RequestConfig config = clientContext.getRequestConfig();
1
2
3
4
5
6

代表一个逻辑相关的会话中的多个请求序列应该被同一个HttpContext实例执行,以确保请求之间会话上下文和状态信息的自动传输。

下面的例子中:请求配置在最初被初始化,它将在执行上下文中一直保持。共享与同一个会话中所有连续的请求。

CloseableHttpClient httpclient = HttpClients.createDefault();
    RequestConfig requestConfig = RequestConfig.custom()
        .setSocketTimeout(1000)
        .setConnectTimeout(1000)
        .build();
    HttpGet httpget1 = new HttpGet("http://localhost/1");
    httpget1.setConfig(requestConfig);
    CloseableHttpResponse response1 = httpclient.execute(httpget1, context);
    try {
        HttpEntity entity1 = response1.getEntity();
    } finally {
        response1.close();
    }
    HttpGet httpget2 = new HttpGet("http://localhost/2");
    CloseableHttpResponse response2 = httpclient.execute(httpget2, context);
    try {
        HttpEntity entity2 = response2.getEntity();
    } finally {
        response2.close();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 使用http上下文来绑定cookie

🔝🔝返回顶部

import java.util.List;
import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

    /**
        - This example demonstrates the use of a local HTTP context populated with
        - custom attributes.
        */
    public class ClientCustomContext {
        public final static void main(String[] args) throws Exception {
            CloseableHttpClient httpclient = HttpClients.createDefault();
            try {
                // Create a local instance of cookie store
                CookieStore cookieStore = new BasicCookieStore();

                // Create local HTTP context
                HttpClientContext localContext = HttpClientContext.create();
                // Bind custom cookie store to the local context
                localContext.setCookieStore(cookieStore);

                HttpGet httpget = new HttpGet("http://localhost/");
                System.out.println("Executing request " + httpget.getRequestLine());

                // Pass local context as a parameter
                CloseableHttpResponse response = httpclient.execute(httpget, localContext);
                try {
                    System.out.println("----------------------------------------");
                    System.out.println(response.getStatusLine());
                    List<Cookie> cookies = cookieStore.getCookies();
                    for (int i = 0; i < cookies.size(); i++) {
                        System.out.println("Local cookie: " + cookies.get(i));
                    }
                    EntityUtils.consume(response.getEntity());
                } finally {
                    response.close();
                }
            } finally {
                httpclient.close();
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 六. HTTP协议拦截器

🔝🔝返回顶部

HTTP协议拦截器是一个实现了HTTP协议特定方面的程序。通常协议拦截器将作用于报文的一个特定的首部或一组相关的首部。或者添加一个特定的首部或一组相关的首部到将要发送的报文中。协议拦截器也可以操作报文内含的实体--显而易见的内容解压/压缩就是一个好的例子。包装实体类使用了装饰模式对原始的实体进行装饰。几个协议拦截器能够结合构成一个逻辑单元。
协议拦截器能够通过共享信息来合作--比如处理状态--通过HTTP上下文。协议拦截器使用HTTP上下文为一次请求或几个关联请求储存一个处理状态。
几个拦截器中被执行的顺序不依靠一个特别的执行上下文状态。如果这些拦截器具有相具有依赖关系,就必须以一个特定的顺序执行。比如希望他们以某个顺序执行,就必须以相同的序列加到协议进程中。
协议拦截器必须被实现为线程安全的。类似于servlet,协议拦截器将不会使用多个实例变量,除非访问的这些变量是同步的。

下面的例子说明了本地上下文在连续请求中保留处理状态的用法。

# 七. 异常处理

🔝🔝返回顶部

HttpClient 能够抛出两种类型的异常:

  • 1)java.io.IOException :在 I/O 失败时,如socket连接超时或被重置的异常;
  • 2)HttpException:标志 HTTP 请求失败的信号,如违反 HTTP 协议。通常 I/O 错误被认为是非致命的和可以恢复的,而 HTTP 协议错误,则被认为是致命的而且是不能自动恢复的。请注意HttpClient实现了可抛出异常HttpExceptions为ClientProtocolException,也是 java.io.IOException的子类。这使HttpClient使用者能够在一个单一的catch子句中处理 IOException 和HttpException。

# 1. HTTP传输安全

🔝🔝返回顶部

要理解 HTTP 协议并不是对所有类型的应用程序都适合的,这一点很重要。 HTTP 是一个 简单的面向请求/响应的协议,最初被设计用来支持取回静态或动态生成的内容。它从未打算支持事务性操作。比如,如果成功收到和处理请求, HTTP 服务器将不会考虑是否只完成了部分请求,它仅生成一个响应并发送一个状态码到客户端。如果客户端因为读取超时,请求取消或系统崩溃导致接收响应实体失败时,服务器不会试图回滚事务。如果客户端决定 决定发送相同的请求,那么服务器将不可避免地多次执行这个相同的事务。在一些情况下,这会导致应用数据污染或者应用程序状态不一致。 尽管 HTTP 从来都没有被设计来支持事务性处理,但它仍然能被用作于一个对目标应用提供被确定状态传输协议。要保证 HTTP 传输层的安全,系统必须保证 HTTP 方法在应用层的幂等性。

# 2.幂等方法

HTTP/1.1 详细地定义了幂等的方法: [Methods can also have the property of "idempotence" in that (aside from error or expiration issues)the side-effects of N > 0 identical requests is the same as for a single request]

换句话说,应用程序应该确保-它是准备着的来处理相同方法的不同执行含义。这是可以达到的,比如,通过提供一个唯一的事务 ID 和避免执行相同逻辑操作的方法。 请注意,这个问题对于 HttpClient 是不明确的。基于应用的浏览器确切的说也受到了相同的问题:与非幂等的 HTTP方法有关。 HttpClient 中非内含实体方法,比如GET和HEAD 是幂等的,而内含实体方法,比如POST和PUT则不是幂等的。

# 3.自动的异常回复

🔝🔝返回顶部

默认情况下, HttpClient 会试图自动从 I/O 异常中恢复。默认的自动恢复机制仅可以对几个被认为是安全的异常起作用。

  • HttpClient 不会尝试从任意逻辑或 HTTP 协议的异常(原文为errors)中恢复(那些是从 HttpException 类中派生出的异常类)。
  • HttpClient 将会自动重新执行那些假设是幂等的方法。
  • HttpClient 将会自动重新执行那些由于传输异常导致的失败,而 HTTP 请求仍然被传送到目标服务器的方法。(也就是请求没有完整的被传送到服务器)

# 4.请求尝试处理器(Request retry handler)

  • 为了能够使用自定义异常的恢复机制,你必须提供一个HttpRequestRetryHandler接口的实现。

请注意你可以使用StandardHttpRequestRetryHandler代替默认使用的,以便处理那些被RFC-2616定义为幂等的并且能够安全的重试的请求方法。方法有:GET, HEAD, PUT, DELETE, OPTIONS, and TRACE。

# 八.终止请求和重定向处理

🔝🔝返回顶部

# 1.终止请求

在一些情况下,由于目标服务器的高负载或客户端有很多同时的请求发出,那么 HTTP 请求会在预期的时间内执行失败。 这时,有必要过早地中止请求,解除在 I/O 执行中的线程锁。 HttpClient 执行时,可以在任意阶段通过调用HttpUriRequest#abort()方法中止请求。 这个方法是线程安全的,而且可以从任意线程中调用。当一个 HTTP 请求被中止时,它的执行线程--就封锁在 I/O 操作中了--而且保证通过抛出InterruptedIOException异常来解锁。

# 2.重定向处理

HttpClient自动处理所有类型的重定向。除了那些由 HTTP 规范明令禁止的,比如需要用 户干预的。参考其它(状态码 303)POST 和 PUT 请求的重定向转换为符合 HTTP 规范要求的 GET请求。你可以使用一个重定向策略,来突破POST方法自动重定向的限制(POST自动重定向为HTTP规范强加)。

在请求报文执行过程中,HttpClient经常需要改写它。每个默认的HTTP/1.0和HTTP/1.1使用相对URI。同样,原始请求需要从一个地址重定向到另一个地址多次。最终绝对的HTTP地址将会被原始的请求和上下文构建。功能方法URIUtils#resolve被使用来构建最终请求形成的绝对URI。这个方法包含了来自于重定向和原始请求的上一个标识。

# 九.HttpClient的连接管理

🔝🔝返回顶部

# 1. 连接持久化

两个主机之间建立连接的过程是很复杂的,包括了两个终端之间许多数据包的交换,会消耗了大量时间。对于很小的HTTP报文传输,上层连接的握手也是必须的【译者:上层连接指的是TCP/IP连接】。如果已有的连接能够重复使用,来执行多个请求,将会加大你程序的数据吞吐量。

HTTP/1.1 默认HTTP连接可以重复地执行多次请求。符合HTTP/1.0的终端,也可以使用某些机制来明确地表达为多次请求而优先使用持久连接。HTTP代理中,对于相同的目标主机,需要随后更多的请求,也能够在特定的时间段内保持空闲的持久连接。保持持续连接的能力通常被称为连接持久化。HttpClient完全地支持连接持久化。

# 2. HTTP连接路由

HttpClient 能够直接建立连接到目标主机,或者通过路由,但这会涉及多个中间连接----也被称为”一跳”。 HttpClient区分连接路由plain, tunneled 和layered。连接到目标主机的隧道使用多个中间代理,被称为代理链。

直接连接到目标主机或仅仅有一个代理的是plain路由。通过一个代理或者代理链连到目标主机的是tunneled 路由。没有代理的路由不是tunneled 路由。 通过已有链接加上协议分层是layered。协议可以在链接到目标主机的隧道上分层,也可以在没有代理的直接连接上分层。

# 2.1 路由计算

RouteInfo 接口代表一个最终的路由连接到目标主机的信息(通过多个步骤和跳)。HttpRoute 是 RouteInfo 的固定的具体实现。 HttpTracker是可变的 RouteInfo 实现,在HttpClient 内部使用来跟踪到最后的路由目标的剩余跳数。HttpTracker 可以在成功执行向路由目标的下一跳之后更新。 HttpRouteDirector是一个帮助类,可以用来计算路由中的下一跳。这个类在 HttpClient 内部使用。

HttpRoutePlanner 是一个接口,它代表计算到基于执行上下文到给定目标完整路由策略。 HttpClient 装载了 两 个 默 认 的 HttpRoutePlanner 实 现 。ProxySelectorRoutePlanner基于 java.net.ProxySelector 。默认情况下 ,它会从系统属性中或从正在运行浏览器中选JVM的代理设置 。DefaultHttpRoutePlanner 实现既不使用任何 Java 系统属性,也不使用系统或浏览器的代理设置。它总是通过默认的相同代理来计算路由。

# 2.2 安全HTTP连接

🔝🔝返回顶部

如果信息在两个不能由非认证的第三方进行读取或修改的终端之间传输, HTTP 连接可

以被认为是安全的。 SSL/TLS 协议是用来确保 HTTP 传输安全所使用的最广泛的技术。当然,其它加密技术也可以被使用。通常来说, HTTP 传输是在 SSL/TLS 加密连接之上分层的。

# 3. HTTP连接管理器

# 3.1.管理连接和连接管理器

HTTP 连接是复杂的,有状态的,非线程安全的对象,它需要恰当的管理以便正确地执行功 能。 “HTTP 连接”一次仅只能由一个执行线程来使用。 HttpClient 采用一个特殊实体来 管理访问 HTTP 连接,这被称为 HTTP 连接管理器,ClientConnectionManager接口就是代表。HTTP 连接管理器的目的是作为工厂创建新的 HTTP 连接,管理持久连接的生命周期和同步访问持久连接(确保同一时间仅有一个线程可以访问一个连接)。内部的 HTTP 连接管理器使用 OperatedClientConnection 实例,这个实例为真正的连接扮演了一个代理,来管理连接状态和控制I/O操作的执行。如果一个管理的连接被释放或者被使用者明确地关闭,潜在的连接就会从它的代理分离,退回到管理器中。 尽管“服务消费者”仍然保存一个代理实例的引用。它也不能执行任何I/O操作、有意或无意地改变真实连接的状态。

这里有一个从连接管理器中获取连接的示例:

HttpClientContext context = HttpClientContext.create();
    HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
    HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
    // Request new connection. This can be a long process
    ConnectionRequest connRequest = connMrg.requestConnection(route, null);
    // Wait for connection up to 10 sec
    HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
    try {
        // If not open
        if (!conn.isOpen()) {
            // establish connection based on its route info
            connMrg.connect(conn, route, 1000, context);
            // and mark it as route complete
            connMrg.routeComplete(conn, route, context);
        }
    // Do useful things with the connection.
    } finally {
        connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如果必要的话,调用ConnectionRequest#cancel()能够尽早地结束连接。这将会使锁定在ConnectionRequest#get()中的线程解锁。

#### 3.2.简单的连接管理器

🔝🔝返回顶部

BasicHttpClientConnectionManager是一个简单的连接管理器,它保持一次只有一个连接。尽管这个方法是线程安全的,它也应该一次使用一个线程。BasicHttpClientConnectionManager将会城尝试在同一个路由中随后的请求使用同一个连接.如果持续的连接不匹配连接的请求,他将会关闭现有连接并为给定的路由重新打开。如果连接已经被分配,将会抛出java.lang.IllegalStateException异常,连接管理器应该在EJB容器中实现。

# 3.3.连接池管理器Pooling connection manager

PoolingHttpClientConnectionManager是一个管理客户端连接更复杂的实现。它也为执行多线程的连接请求提供服务。对于每个基本的路由,连接都是池管理的。对于路由的请求,管理器在池中有可用的持久性连接,将被从池中取出连接服务,而不是创建一个新的连接。

对于每一个基本路由,PoolingHttpClientConnectionManager保持着一个最大限制的连接数。每个默认的实现对每个给定路由将会创建不超过2个的并发连接,一共不会超过 20 个连接。对于真实世界的许多程序,连接数太少了,特别是他们为其服务使用HTTP协议作为传输协议时。

下面例子说明了怎样设置连接池参数是最合适的:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
    // Increase max total connection to 200
    cm.setMaxTotal(200);
    // Increase default max connection per route to 20
    cm.setDefaultMaxPerRoute(20);
    // Increase max connections for localhost:80 to 50
    HttpHost localhost = new HttpHost("locahost", 80);
    cm.setMaxPerRoute(new HttpRoute(localhost), 50);
    CloseableHttpClient httpClient = HttpClients.custom()setConnectionManager(cm).build();
1
2
3
4
5
6
7
8
9

# 3.4.闭连接管理器

当一个HttpClient实例不再被需要时,而且即将超出使用范围,重要是的,关闭连接管理器来保证由管理器保持活动的所有连接被关闭,并且由连接分配的系统资源被释放。 CloseableHttpClient httpClient = <...> httpClient.close();

# 4.请求执行的多线程

🔝🔝返回顶部

当配备连接池管理器时,比如 PoolingClientConnectionManager, HttpClient 可以被用使用多线程来同时执行多个请求。

PoolingClientConnectionManager将会基于它的配置来分配连接。如果对于给定路由的所有连接都被使用了,那么连接的请求将会阻塞,直到一个连接被释放回连接池。 它 可以通过设置'http.conn-manager.timeout'为一个正数来保证连接管理器不会在连接请求执行时无限期的被阻塞。 如果连接请求不能在给定的时间内被响应,将会抛出ConnectionPoolTimeoutException 异常。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
    CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
    // URIs to perform GETs on
    String[] urisToGet = {"http://www.domain1.com/","http://www.domain2.com/",
                        "http://www.domain3.com/","http://www.domain4.com/"};
    // create a thread for each URI
    GetThread[] threads = new GetThread[urisToGet.length];
    for (int i = 0; i < threads.length; i++) {
        HttpGet httpget = new HttpGet(urisToGet[i]);
        threads[i] = new GetThread(httpClient, httpget);
    }
    // start the threads
    for (int j = 0; j < threads.length; j++) {
        threads[j].start();
    }
    // join the threads
    for (int j = 0; j < threads.length; j++) {
        threads[j].join();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

一个HttpClient实例是线程安全的,并且能够在多个执行线程直接共享,强烈建议每个线程保持一个它自己专有的HttpContext实例。

static class GetThread extends Thread {
    private final CloseableHttpClient httpClient;
    private final HttpContext context;
    private final HttpGet httpget;
    public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
    this.httpClient = httpClient;
    this.context = HttpClientContext.create();
    this.httpget = httpget;
    }
    @Override
    public void run() {
    try {
        CloseableHttpResponse response = httpClient.execute( httpget, context);
        try {
        HttpEntity entity = response.getEntity();
        } finally {
        response.close();
        }
    } catch (ClientProtocolException ex) {
        // Handle protocol errors
    } catch (IOException ex) {
        // Handle I/O errors
    }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 5.连接回收策略Connection eviction policy

🔝🔝返回顶部

一个典型阻塞 I/O 的模型的主要缺点是网络socket仅当 I/O 操作阻塞时才可以响应 I/O 事件。当一个连接被释放回管理器时,它可以被保持活动状态而却不能监控socket的状态 和响应任何 I/O 事件。 如果连接在服务器端关闭,那么客户端连接也不能去侦测连接状态中的变化(也不能关闭本端的socket去作出适当反馈)。

HttpClient 通过测试连接是否是连接是过时的来尝试去减轻这个问题。在服务器端关闭的连接已经不再有效了,优先使用之前使用执行 HTTP 请求的连接。过时的连接检查也并不是100%可靠。唯一可行的而不涉及每个空闲连接的socket模型线程的解决方案是:是使用专用的监控线程来收回因为长时间不活动而被认 为 是 过 期 的 连 接 。 监 控 线 程 可 以 周 期 地 调 用ClientConnectionManager#closeExpiredConnections() 方法来关闭所有过期 的 连 接 并且从 连 接 池 中 收 回 关 闭 的 连 接 。 它 也 可 以 选 择 性 调 用 ClientConnectionManager#closeIdleConnections()方法来关闭所有已经空 闲超过给定时间周期的连接。

public static class IdleConnectionMonitorThread extends Thread {
    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;
    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
    super();
    this.connMgr = connMgr;
    }
    @Override
    public void run() {
    try {
        while (!shutdown) {
        synchronized (this) {
        wait(5000);
        // Close expired connections
        connMgr.closeExpiredConnections();
        // Optionally, close connections
        // that have been idle longer than 30 sec
        connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
        }
        }
    } catch (InterruptedException ex) {
        // terminate
    }
    }
    public void shutdown() {
    shutdown = true;
    synchronized (this) {
        notifyAll();
    }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 6.连接保持策略

🔝🔝返回顶部

HTTP 规范没有详细说明一个持久连接可能或应该保持活动多长时间。一些 HTTP 服务器使用非标准的首部Keep-Alive来告诉客户端它们想在服务器端保持连接活动的周期秒数。如果这个信息存在, HttClient 就会使用它。如果响应中首部Keep-Alive 在不存在, HttpClient会假定无限期地保持连接。然而许多 HTTP 服务器的普遍配置是在特定非 活动周期之后丢掉持久连接来保护系统资源,往往这是不通知客户端的。默认的策略是过于乐观的,你必须提供一个自定义的keep-alive策略

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        // Honor 'keep-alive' header
        HeaderElementIterator it = new BasicHeaderElementIterator(
            response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            HeaderElement he = it.nextElement();
            String param = he.getName();
            String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                try {
                    return Long.parseLong(value) - 1000;
                } catch(NumberFormatException ignore) {
                }
            }
        }
        HttpHost target = (HttpHost) context.getAttribute(
                    HttpClientContext.HTTP_TARGET_HOST);
        if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
            // Keep alive for 5 seconds only
            return 5 - 1000;
        } else {
            // otherwise keep alive for 30 seconds
            return 30 - 1000;
        }
    }
};
CloseableHttpClient client = HttpClients.custom().setKeepAliveStrategy(myStrategy).build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 7.连接socket工厂

🔝🔝返回顶部

HTTP连接内部使用java.net.Socket 来通过网线处理传输数据。可是,他们依靠ConnectionSocketFactory接口来创建、初始化和连接socket。HttpClient为使用者提供了在程序运行时明确的socket初始化代码。PlainConnectionSocketFactory是创建,初始化普通的socket(非加密socket)的工厂。

socket的创建过程和把它连接到一个主机上的过程是非对称的 。所以当被连接操作阻塞时socket将会关闭。

HttpClientContext clientContext = HttpClientContext.create();
    PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
    Socket socket = sf.createSocket(clientContext);
    int timeout = 1000; //ms
    HttpHost target = new HttpHost("localhost");
    InetSocketAddress remoteAddress = new InetSocketAddress(netAddress.getByAddress(new byte[] {127,0,0,1}), 80);
    sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);
1
2
3
4
5
6
7

# 7.1.安全套接字分层

LayeredConnectionSocketFactory是ConnectionSocketFactory接口的拓展。分层的socket工厂可以创建在已经存在的普通socket之上。套接字分层主要通过代理来创建安全的socket。 HttpClient装载实现了 SSL/TLS 分层的 SSLSocketFactory。 请注意 HttpClient 不使用任 何自定义加密功能。它完全依赖于标准的Java Cryptography(JCE)和Secure Sockets(JSEE)扩展。

# 7.2.与连接管理器整合

🔝🔝返回顶部

自定义的连接socket工厂与一个被称为HTTP或HTTPS的特定协议模式有关,它被用来创建自定义连接管理器

ConnectionSocketFactory plainsf = <...>
    LayeredConnectionSocketFactory sslsf = <...>
    Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
        .register("http", plainsf).register("https", sslsf).build();
    HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
    HttpClients.custom().setConnectionManager(cm).build();
1
2
3
4
5
6

# 7.3.定制SSL/TLS

HttpClient 使用 SSLSocketFactory来创建 SSL连接。SSLSocketFactory允许高度定制。它可以使用 javax.net.ssl.SSLContext 的实例作为参数, 并使用它来创建能够自定义配置的 SSL连接。 KeyStore myTrustStore = <...> SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(myTrustStore).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext); SSLSocketFactory的定制意味着对SSL/TLS协议概念有一定的熟悉,详细说明它超出 了本文档的范围。请参考Java™Secure Socket Extension(JSSE) 参考指南http://java.sun.com/j2se/1.5.0/docs/guide/security/jsse/JSSERefGuide.html,从而获得javax.net.ssl.SSLContext的详细描述和相关工具。

# 7.4.主机名核实

🔝🔝返回顶部

为了信任证明并且客户端的表现在SSL/TLS级别上,HttpClient随意的核实是否目标主机名匹配储存在X.509服务器上的证书。一旦连接被建立,这个证明能提供服务器信任材料额外的保证。javax.net.ssl.HostnameVerifier接口代表了主机名验证的策略。 HttpClient有两个 javax.net.ssl.HostnameVerifier的实现。重点:主机名验证不应该和SSL信任验证混淆。

·DefaultHostnameVerifier:遵从RFC 2818规范,被HttpClient默认实现。主机名必须根据规定的证书匹配被几个可选择名字,如果没有合适的名字,将会给出最具体的证书CN 。一个通配符可能会出现在CN中和任何可选择的地方。 【he default implementation used by HttpClient is expected to becompliant with RFC 2818. The hostname must match any of alternative names specified by thecertificate, or in case no alternative names are given the most specific CN of the certificate subject.A wildcard can occur in the CN, and in any of the subject-alts.】

·NoopHostnameVerifier:主机名验证器根本就关掉了主机名验证。他接受任何合法的SSL会话,并且匹配目标主机 每次HttpClient默认使用DefaultHostnameVerifier实现。如果必要的话,你可以制定一个不同的主机名认证器实现 SSLContext sslContext = SSLContexts.createSystemDefault(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);

在HttpClient 4.4 版本,使用被Mozilla Foundation维护的公共的后缀列表,确保SSL证书中的通配符不能够被滥用来在顶级域名下启用多个域名。HttpClient在发布时复制了一份列表。 最新的版本列表能够在https://publicsuffix.org/list/https://publicsuffix.org/list/effective_tld_names.dat找到。强烈建议下载一份列表,防止在每天使用时访问原始位置。

PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(
    PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat"));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier (publicSuffixMatcher)
1
2
3

# 8.HttpClient代理配置

🔝🔝返回顶部

尽管HttpClient了解复杂的路由模式和路由链,但它只支持简单的重定向或者一跳代理连接。 告诉HttpClient通过代理连接目标服务器最简单的方式是设置默认的代理参数

HttpHost proxy = new HttpHost("someproxy", 8080);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom() .setRoutePlanner(routePlanner) .build();
1
2
3

你也可以使用标准的JRE代理选择器通知HttpClient来获得代理信息

SystemDefaultRoutePlanner routePlanner =
                       new SystemDefaultRoutePlanner(ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom() .setRoutePlanner(routePlanner) build();
1
2
3

你也可以实现一个自定义的RoutePlanner,来实现一个在完整控制HTTP路由计算的处理器。

HttpRoutePlanner routePlanner = new HttpRoutePlanner() {
  public HttpRoute determineRoute( HttpHost target, HttpRequest request,
                                 HttpContext context) throws HttpException {
    return new HttpRoute(target, null, new HttpHost("someproxy", 8080),
          "https".equalsIgnoreCase(target.getSchemeName()));
  }
};
CloseableHttpClient httpclient = HttpClients.custom() .setRoutePlanner(routePlanner).build();
1
2
3
4
5
6
7
8

# 十一. 状态管理(Cookie相关)

🔝🔝返回顶部

原始的 HTTP 是被设计为无状态的,面向请求/响应的协议,没有特别对一些逻辑相关的请求/响应交换的设置会话状态。 由于 HTTP 协议变得越来越普及和受欢迎,越来越多的从前没有打算使用它的系统也开始为应用程序来使用它,比如作为电子商务应用程序的传输。因此,支持状态管理就变得非常必要了。

网景公司,一度成为 Web 客户端和服务器软件开发者的领导方向,在它们基于专有规范的产品中实现了对 HTTP 状态管理的支持。 之后,网景公司试图通过发布规范草案来规范这种机制。 它们的努力通过 RFC 标准跟踪促成了这些规范定义。然而,在很多应用程序中的状态管理仍然基于网景公司的草案而不兼容官方的规范。 很多主要的Web浏览器开发者觉得有必要保留那些极大促进应用程序兼容性的标准草案。

Cookie 是 HTTP 代理和目标服务器可以交流保持会话的状态信息的令牌或小的数据包。网景公司的工程师把它称为“魔法小甜饼”("magic cookie"),这个名字好像有粘性一样。
HttpClient 使用 Cookie 接口来代表抽象的 cookie 令牌。 HTTP中简单形式中的cookie是名/值对。 通常一个 HTTP 的 cookie 也包含一些属性,比如版本号,合法的域名,指定 cookie 应用所在的源服务器URL路径的子集, cookie 的最长有效时间。
SetCookie接口代表由源服务器发送给HTTP代理的响应中的首部Set-Cookie,它用来维持一个对话状态。 ClientCookie接口和它指定的方法扩展了Cookie。比如:能够取回被源服务器定义的原始cookie。这对生成 Cookie 首部很重要,因为一些 cookie规范需要Cookie 首部应该包含特定的属性,这些属性只能在 Set-Cookie 头部中指定。

下面是客户端创建cookie对象的例子:

BasicClientCookie cookie = new BasicClientCookie("name", "value");
// Set effective domain and path attributes
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
// Set attributes exactly as sent by the server
cookie.setAttribute(ClientCookie.PATH_ATTR, "/");
cookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");
1
2
3
4
5
6
7

# 2.cookie详解

🔝🔝返回顶部

CookieSpec接口代表一个管理cookie的规范,cookie管理规范是强制的。

解析Set-Cookie头部规则
分析验证cookie规则
用给定的主机名,端口号,和原路径格式化Cookie首部

# HttpClient附带了几个CookieSpec的实现

🔝🔝返回顶部

Standard strict:State management policy compliant with the syntax and semantics of the wellbehaved profile defined by RFC 6265, section 4.

Standard:State management policy compliant with a more relaxed profile defined by RFC 6265, section 4 intended for interoperability with existing servers that do not conform to the well behaved profile.

Netscape draft (obsolete):This policy conforms to the original draft specification published by Netscape Communications. It should be avoided unless absolutely necessary for compatibility with legacy code.

RFC 2965 (obsolete):State management policy compliant with the obsolete state management specification defined by RFC 2965. Please do not use in new applications.--->已废弃

RFC 2109 (obsolete):State management policy compliant with the obsolete state management specification defined by RFC 2109. Please do not use in new applications--->已废弃

Browser compatibility (obsolete):This policy strives to closely mimic the (mis)behavior of older versions of browser applications such as Microsoft Internet Explorer and Mozilla FireFox. Please do not use in new applications.--->已废弃

Default:Default cookie policy is a synthetic policy that picks up either RFC 2965, RFC 2109 or Netscape draft compliant implementation based on properties of cookies sent with the HTTP response (such as version attribute, now obsolete). This policy will be deprecated in favor of the standard (RFC 6265 compliant) implementation in the next minor release of HttpClient.

Ignore cookies:所有的cookie将会被忽略
在新程序中,强烈建议使用Standard或Standard strict策略。废弃的规范仅应该使用在遗留项目中。下一个HttpClient版本将不会支持废弃的规范。

# 3.选择cookie策略

🔝🔝返回顶部

Cookie策略能够能够在HTTP客户端设置,如果需要的话,能够在HTTP请求中被重写(覆盖)。

RequestConfig globalConfig = RequestConfig.custom()
    .setCookieSpec(CookieSpecs.DEFAULT)
    .build();
CloseableHttpClient httpclient = HttpClients.custom()
    .setDefaultRequestConfig(globalConfig)
    .build();
RequestConfig localConfig = RequestConfig.copy(globalConfig)
    .setCookieSpec(CookieSpecs.STANDARD_STRICT)
.build();
HttpGet httpGet = new HttpGet("/");
httpGet.setConfig(localConfig);
1
2
3
4
5
6
7
8
9
10
11

# 4.自定义cookie策略

为了实现自定义的cookie策略你必须创建一个CookieSpec的实现,创建一个CookieSpecProvider的实现,用它来创建和初始化自定义规范的实例、和HttpClient注册工厂。一旦自定义的规范被注册,它将会和标准cookie规范一样被使用。

PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();
Registry<CookieSpecProvider> r = RegistryBuilder.<CookieSpecProvider>create()
   .register(CookieSpecs.DEFAULT,new DefaultCookieSpecProvider(publicSuffixMatcher))
   .register(CookieSpecs.STANDARD,newRFC6265CookieSpecProvider(publicSuffixMatcher))
   .register("easy", new EasySpecProvider())
   .build();
RequestConfig requestConfig = RequestConfig.custom()
   .setCookieSpec("easy")
   .build();
   CloseableHttpClient httpclient = HttpClients.custom()
   .setDefaultCookieSpecRegistry(r)
   .setDefaultRequestConfig(requestConfig)
   .build();
1
2
3
4
5
6
7
8
9
10
11
12
13

HttpClient能够使用任何实现了CookieStore接口的物理cookie。默认的CookieStore实现类被称为BasicCookieStore,它的内部是使用java.util.ArrayList的简单实现的。当容器进行垃圾回收时,储存在BasicClientCookie中的对象就会丢失。如果必要的话,使用者可以提供一个复杂的实现。

// Create a local instance of cookie store
CookieStore cookieStore = new BasicCookieStore();
// Populate cookies if needed
BasicClientCookie cookie = new BasicClientCookie("name", "value");
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
cookieStore.addCookie(cookie);
// Set the store
CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore)
   .build();
1
2
3
4
5
6
7
8
9
10

# 6.HTTP状态管理和执行上下文

🔝🔝返回顶部

在一个HTTP执行请求的过程中,HttpClient向执行上下文中添加了下面的状态管理对象

Lookup实例代表了真实的cookie详细记录,这个属性的值设置在本地上下文中,优先于默认的。
CookieSpec实例代表了真实的cookie规范
CookieOrigin实例代表了当前的原服务器的详情
CookieStore实例代表了当前的cookie store,这个属性的值设置在本地上下文中而优先于默认的。

在请求执行之前,本地的HttpContext对象能够定制HTTP状态管理上下文,或者在请求执行后检查它的状态。你可以使用分离的上下文以便实现每个用户(或每个线程)的状态管理。cookie规范记录、cookie store定义在本地的上下文中优先于那些在HTTP客户端设置的默认的上下文。

CloseableHttpClient httpclient = <...>

Lookup<CookieSpecProvider> cookieSpecReg = <...>
CookieStore cookieStore = <...>

HttpClientContext context = HttpClientContext.create();
context.setCookieSpecRegistry(cookieSpecReg);
context.setCookieStore(cookieStore);
HttpGet httpget = new HttpGet("http://somehost/");
CloseableHttpResponse response1 = httpclient.execute(httpget, context);
<...>
// Cookie origin details
CookieOrigin cookieOrigin = context.getCookieOrigin();
// Cookie spec used
CookieSpec cookieSpec = context.getCookieSpec();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 十二.使用HttpClient流畅风格的API

HttpClient 4.2 中 简化了基于流畅风格的外观API。如果你不需要HttpClient丰富的灵活性,流畅风格API提供了HttpClient中最基础的方法,使用简单。比如:流畅风格API简化了使用者处理连接管理器和分配资源。

下面有几个例子:

// Execute a GET with timeout settings and return response content as String.
Request.Get("http://somehost/")
    .connectTimeout(1000)
    .socketTimeout(1000)
     .execute()
    .returnContent().asString();
// Execute a POST with the 'expect-continue' handshake, using HTTP/1.1,
// containing a request body as String and return response content as byte array.
Request.Post("http://somehost/do-stuff")
    .useExpectContinue()
    .version(HttpVersion.HTTP_1_1)
    .bodyString("Important stuff", ContentType.DEFAULT_TEXT)
    .execute().returnContent().asBytes();
// Execute a POST with a custom header through the proxy containing a request body
// as an HTML form and save the result to the file
Request.Post("http://somehost/some-form")
    .addHeader("X-Custom-header", "stuff")
    .viaProxy(new HttpHost("myproxy", 8080))
    .bodyForm(Form.form().add("username", "vip").add("password", "secret").build())
    .execute().saveContent(new File("result.dump"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

你也可以直接使用Executor,以一个特定的安全上下文执行请求,这样认证信息就会进行缓存并再用于后续请求。

Executor executor = Executor.newInstance()
    .auth(new HttpHost("somehost"), "username", "password")
    .auth(new HttpHost("myproxy", 8080), "username", "password")
    .authPreemptive(new HttpHost("myproxy", 8080));

executor.execute(Request.Get("http://somehost/"))
    .returnContent().asString();

executor.execute(Request.Post("http://somehost/do-stuff")
    .useExpectContinue()
    .bodyString("Important stuff", ContentType.DEFAULT_TEXT))
    .returnContent().asString();
1
2
3
4
5
6
7
8
9
10
11
12

fluent facade API简化了使用者处理连接管理器和分配资源。在大多数情况下,它付出了在内存中缓存响应的代价。强烈建议使用ResponseHandler避免在内存中缓存响应。

Document result = Request.Get("http://somehost/content")
  .execute()
  .handleResponse(
    new ResponseHandler<Document>() {
      public Document handleResponse(final HttpResponse response) throws IOException {
        StatusLine statusLine = response.getStatusLine();
        HttpEntity entity = response.getEntity();
        if (statusLine.getStatusCode() >= 300) {
            throw new HttpResponseException( statusLine.getStatusCode(),statusLine.getReasonPhrase());
        }

        if (entity == null) {
            throw new ClientProtocolException("Response contains no content");
        }

        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();

        try {
            DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
            ContentType contentType = ContentType.getOrDefault(entity);
            if (!contentType.equals(ContentType.APPLICATION_XML)) {
                throw new ClientProtocolException("Unexpected content type:" +contentType);
            }
            String charset = contentType.getCharset();
            if (charset == null) {
                charset = HTTP.DEFAULT_CONTENT_CHARSET;
            }
            return docBuilder.parse(entity.getContent(), charset);
        } catch (ParserConfigurationException ex) {
            throw new IllegalStateException(ex);
        } catch (SAXException ex) {
            throw new ClientProtocolException("Malformed XML document", ex);
        }
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 十三、我的

🔝🔝返回顶部

# 6、post&get请求

public static void sendHttpPostRequest(String url,List<NameValuePair> list) throws ClientProtocolException, IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpPost post = new HttpPost(url);
    UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list,Consts.UTF_8);
    post.setEntity(entity);
    CloseableHttpResponse response = httpClient.execute(post);
    if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
        log.debug("POST请求返回结果"+EntityUtils.toString(response.getEntity()));
    } else {
        sendHttpPostRequest(url,list);
    }
    response.close();
    httpClient.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 接收请求

@RequestMapping(value="/postAlarm", method = RequestMethod.POST)
public @ResponseBody String obtainPostAlarm(@RequestParam ("alarmType") String alarmType,@RequestParam("alarmStatus") String alarmStatus) {
    log.debug("\nalarmStatus:"+alarmStatus+"\nalarmType:"+alarmType);
    return "success";
}

public static void sendHttpRequest(String url) throws ClientProtocolException, IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    CloseableHttpResponse response = null;
    HttpGet httpGet = new HttpGet(url);
    response = httpClient.execute(httpGet);
    if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
        HttpEntity entity = response.getEntity();
        String detail = EntityUtils.toString(entity);
        log.info("系统返回信息:"+detail);
    } else {
        sendHttpRequest(url);
    }
            response.close();
            httpClient.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2、request.getSession()

🔝🔝返回顶部

request.getSession(true):若存在会话则返回该会话,否则新建一个会话。 request.getSession(false):若存在会话则返回该会话,否则返回NULL

# I.使用

当向Session中存取登录信息时,一般建议:HttpSession session =request.getSession(); 当从Session中获取登录信息时,一般建议:HttpSession session =request.getSession(false);