HttpClient 完整教程

Posted y_keven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HttpClient 完整教程相关的知识,希望对你有一定的参考价值。

前言

Http协议应该是互联网中最重要的协议。持续增长的web服务、可联网的家用电器等都在继承并拓展着Http协议,向着浏览器之外的方向发展。

虽然jdk中的java.net包中提供了一些基本的方法,通过http协议来访问网络资源,但是大多数场景下,它都不够灵活和强大。HttpClient致力于填补这个空白,它可以提供有效的、最新的、功能丰富的包来实现http客户端。

为了拓展,HttpClient即支持基本的http协议,还支持http-aware客户端程序,如web浏览器,Webservice客户端,以及利用or拓展http协议的分布式系统。

1、HttpClient的范围/特性

  • 是一个基于HttpCore的客户端Http传输类库
  • 基于传统的(阻塞)IO
  • 内容无关

2、HttpClient不能做的事情

  • HttpClient不是浏览器,它是一个客户端http协议传输类库。HttpClient被用来发送和接受Http消息。HttpClient不会处理http消息的内容,不会进行javascript解析,不会关心content type,如果没有明确设置,httpclient也不会对请求进行格式化、重定向url,或者其他任何和http消息传输相关的功能。

 

第一章 基本概念

1.1. 请求执行

HttpClient最基本的功能就是执行Http方法。一个Http方法的执行涉及到一个或者多个Http请求/Http响应的交互,通常这个过程都会自动被HttpClient处理,对用户透明。用户只需要提供Http请求对象,HttpClient就会将http请求发送给目标服务器,并且接收服务器的响应,如果http请求执行不成功,httpclient就会抛出异样。

下面是个很简单的http请求执行的例子:

 

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

 

1.1.1. HTTP请求

所有的Http请求都有一个请求行(request line),包括方法名、请求的URI和Http版本号。

HttpClient支持HTTP/1.1这个版本定义的所有Http方法:GET,HEAD,POST,PUT,DELETE,TRACEOPTIONS。对于每一种http方法,HttpClient都定义了一个相应的类:HttpGetHttpHeadHttpPostHttpPutHttpDeleteHttpTraceHttpOpquertions。

Request-URI即统一资源定位符,用来标明Http请求中的资源。Http request URIs包含协议名、主机名、主机端口(可选)、资源路径、query(可选)和片段信息(可选)。

 

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

 

HttpClient提供URIBuilder工具类来简化URIs的创建和修改过程。

 

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

 

上述代码会在控制台输出:

 

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

 

1.1.2. HTTP响应

 

服务器收到客户端的http请求后,就会对其进行解析,然后把响应发给客户端,这个响应就是HTTP response.HTTP响应第一行是协议版本,之后是数字状态码和相关联的文本段。

 

  1. HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,   
  2. HttpStatus.SC_OK, "OK");  
  3.   
  4. System.out.println(response.getProtocolVersion());  
  5. System.out.println(response.getStatusLine().getStatusCode());  
  6. System.out.println(response.getStatusLine().getReasonPhrase());  
  7. System.out.println(response.getStatusLine().toString());  

上述代码会在控制台输出:

 

 

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

 

1.1.3. 消息头

一个Http消息可以包含一系列的消息头,用来对http消息进行描述,比如消息长度,消息类型等等。HttpClient提供了方法来获取、添加、移除、枚举消息头。

 

  1. HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,   
  2.     HttpStatus.SC_OK, "OK");  
  3. response.addHeader("Set-Cookie",   
  4.     "c1=a; path=/; domain=localhost");  
  5. response.addHeader("Set-Cookie",   
  6.     "c2=b; path=\\"/\\", c3=c; domain=\\"localhost\\"");  
  7. Header h1 = response.getFirstHeader("Set-Cookie");  
  8. System.out.println(h1);  
  9. Header h2 = response.getLastHeader("Set-Cookie");  
  10. System.out.println(h2);  
  11. Header[] hs = response.getHeaders("Set-Cookie");  
  12. System.out.println(hs.length);  

上述代码会在控制台输出:

 

 

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

 

最有效的获取指定类型的消息头的方法还是使用HeaderIterator接口。

 

  1. HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,   
  2.     HttpStatus.SC_OK, "OK");  
  3. response.addHeader("Set-Cookie",   
  4.     "c1=a; path=/; domain=localhost");  
  5. response.addHeader("Set-Cookie",   
  6.     "c2=b; path=\\"/\\", c3=c; domain=\\"localhost\\"");  
  7.   
  8. HeaderIterator it = response.headerIterator("Set-Cookie");  
  9.   
  10. while (it.hasNext())   
  11.     System.out.println(it.next());  
  12.   

 

上述代码会在控制台输出:

 

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

 

HeaderIterator也提供非常便捷的方式,将Http消息解析成单独的消息头元素。

 

  1. HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,   
  2.     HttpStatus.SC_OK, "OK");  
  3. response.addHeader("Set-Cookie",   
  4.     "c1=a; path=/; domain=localhost");  
  5. response.addHeader("Set-Cookie",   
  6.     "c2=b; path=\\"/\\", c3=c; domain=\\"localhost\\"");  
  7.   
  8. HeaderElementIterator it = new BasicHeaderElementIterator(  
  9.     response.headerIterator("Set-Cookie"));  
  10.   
  11. while (it.hasNext())   
  12.     HeaderElement elem = it.nextElement();   
  13.     System.out.println(elem.getName() + " = " + elem.getValue());  
  14.     NameValuePair[] params = elem.getParameters();  
  15.     for (int i = 0; i < params.length; i++)   
  16.         System.out.println(" " + params[i]);  
  17.       
  18.   

 

上述代码会在控制台输出:

 

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

 

1.1.4. HTTP实体

Http消息可以携带http实体,这个http实体既可以是http请求,也可以是http响应的。Http实体,可以在某些http请求或者响应中发现,但不是必须的。Http规范中定义了两种包含请求的方法:POST和PUT。HTTP响应一般会包含一个内容实体。当然这条规则也有异常情况,如Head方法的响应,204没有内容,304没有修改或者205内容资源重置。

HttpClient根据来源的不同,划分了三种不同的Http实体内容。

  • streamed流式: 内容是通过流来接受或者在运行中产生。特别是,streamed这一类包含从http响应中获取的实体内容。一般说来,streamed实体是不可重复的。
  • self-contained自我包含式:内容在内存中或通过独立的连接或其它实体中获得。self-contained类型的实体内容通常是可重复的。这种类型的实体通常用于关闭http请求。
  • wrapping包装式: 这种类型的内容是从另外的http实体中获取的。

当从Http响应中读取内容时,上面的三种区分对于连接管理器来说是非常重要的。对于由应用程序创建而且只使用HttpClient发送的请求实体,streamed和self-contained两种类型的不同就不那么重要了。这种情况下,建议考虑如streamed流式这种不能重复的实体,和可以重复的self-contained自我包含式实体。

1.1.4.1. 可重复的实体

一个实体是可重复的,也就是说它的包含的内容可以被多次读取。这种多次读取只有self contained(自包含)的实体能做到(比如ByteArrayEntity或者StringEntity)。

1.1.4.2. 使用Http实体

由于一个Http实体既可以表示二进制内容,又可以表示文本内容,所以Http实体要支持字符编码(为了支持后者,即文本内容)。

当需要执行一个完整内容的Http请求或者Http请求已经成功,服务器要发送响应到客户端时,Http实体就会被创建。

如果要从Http实体中读取内容,我们可以利用HttpEntity类的getContent方法来获取实体的输入流(java.io.InputStream),或者利用HttpEntity类的writeTo(OutputStream)方法来获取输出流,这个方法会把所有的内容写入到给定的流中。
当实体类已经被接受后,我们可以利用HttpEntity类的getContentType()getContentLength()方法来读取Content-TypeContent-Length两个头消息(如果有的话)。由于Content-Type包含mime-types的字符编码,比如text/plain或者text/html,HttpEntity类的getContentEncoding()方法就是读取这个编码的。如果头信息不存在,getContentLength()会返回-1,getContentType()会返回NULL。如果Content-Type信息存在,就会返回一个Header类。

当为发送消息创建Http实体时,需要同时附加meta信息。

 

  1. StringEntity myEntity = new StringEntity("important message",   
  2.    ContentType.create("text/plain""UTF-8"));  
  3.   
  4. System.out.println(myEntity.getContentType());  
  5. System.out.println(myEntity.getContentLength());  
  6. System.out.println(EntityUtils.toString(myEntity));  
  7. System.out.println(EntityUtils.toByteArray(myEntity).length);  

上述代码会在控制台输出:

 

 

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

 

1.1.5. 确保底层的资源连接被释放

 

为了确保系统资源被正确地释放,我们要么管理Http实体的内容流、要么关闭Http响应。

 

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

关闭Http实体内容流和关闭Http响应的区别在于,前者通过消耗掉Http实体内容来保持相关的http连接,然后后者会立即关闭、丢弃http连接。

 

请注意HttpEntitywriteTo(OutputStream)方法,当Http实体被写入到OutputStream后,也要确保释放系统资源。如果这个方法内调用了HttpEntitygetContent()方法,那么它会有一个java.io.InpputStream的实例,我们需要在finally中关闭这个流。

但是也有这样的情况,我们只需要获取Http响应内容的一小部分,而获取整个内容并、实现连接的可重复性代价太大,这时我们可以通过关闭响应的方式来关闭内容输入、输出流。

 

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

上面的代码执行后,连接变得不可用,所有的资源都将被释放。

 

1.1.6. 消耗HTTP实体内容

HttpClient推荐使用HttpEntitygetConent()方法或者HttpEntitywriteTo(OutputStream)方法来消耗掉Http实体内容。HttpClient也提供了EntityUtils这个类,这个类提供一些静态方法可以更容易地读取Http实体的内容和信息。和以java.io.InputStream流读取内容的方式相比,EntityUtils提供的方法可以以字符串或者字节数组的形式读取Http实体。但是,强烈不推荐使用EntityUtils这个类,除非目标服务器发出的响应是可信任的,并且http响应实体的长度不会过大。

 

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

 

有些情况下,我们希望可以重复读取Http实体的内容。这就需要把Http实体内容缓存在内存或者磁盘上。最简单的方法就是把Http Entity转化成BufferedHttpEntity,这样就把原Http实体的内容缓冲到了内存中。后面我们就可以重复读取BufferedHttpEntity中的内容。

 

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

 

1.1.7. 创建HTTP实体内容

 

HttpClient提供了一些类,这些类可以通过http连接高效地输出Http实体内容。HttpClient提供的这几个类涵盖的常见的数据类型,如String,byte数组,输入流,和文件类型:StringEntity,ByteArrayEntity,InputStreamEntity,FileEntity

 

  1. File file = new File("somefile.txt");  
  2. FileEntity entity = new FileEntity(file,   
  3.     ContentType.create("text/plain""UTF-8"));          
  4.   
  5. HttpPost httppost = new HttpPost("http://localhost/action.do");  
  6. httppost.setEntity(entity);  
请注意由于 InputStreamEntity 只能从下层的数据流中读取一次,所以它是不能重复的。推荐,通过继承 HttpEntity 这个自包含的类来自定义HttpEntity类,而不是直接使用 InputStreamEntity 这个类。 FileEntity 就是一个很好的起点(FileEntity就是继承的HttpEntity)。

 

1.7.1.1. HTML表单

很多应用程序需要模拟提交Html表单的过程,举个例子,登陆一个网站或者将输入内容提交给服务器。HttpClient提供了UrlEncodedFormEntity这个类来帮助实现这一过程。

  1. List<NameValuePair> formparams = new ArrayList<NameValuePair>();  
  2. formparams.add(new BasicNameValuePair("param1""value1"));  
  3. formparams.add(new BasicNameValuePair("param2""value2"));  
  4. UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);  
  5. HttpPost httppost = new HttpPost("http://localhost/handler.do");  
  6. httppost.setEntity(entity);  

 

UrlEncodedFormEntity实例会使用所谓的Url编码的方式对我们的参数进行编码,产生的结果如下:

  1. param1=value1&m2=value2  

 

1.1.7.2. 内容分块

一般来说,推荐让HttpClient自己根据Http消息传递的特征来选择最合适的传输编码。当然,如果非要手动控制也是可以的,可以通过设置HttpEntitysetChunked()为true。请注意:HttpClient仅会将这个参数看成是一个建议。如果Http的版本(如http 1.0)不支持内容分块,那么这个参数就会被忽略。

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

 

1.1.8.RESPONSE HANDLERS

最简单也是最方便的处理http响应的方法就是使用ResponseHandler接口,这个接口中有handleResponse(HttpResponse response)方法。使用这个方法,用户完全不用关心http连接管理器。当使用ResponseHandler时,HttpClient会自动地将Http连接释放给Http管理器,即使http请求失败了或者抛出了异常。

  1. CloseableHttpClient httpclient = HttpClients.createDefault();  
  2. HttpGet httpget = new HttpGet("http://localhost/json");  
  3.   
  4. ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>()   
  5.   
  6.     @Override  
  7.     public JsonObject handleResponse(  
  8.             final HttpResponse response) throws IOException   
  9.         StatusLine statusLine = response.getStatusLine();  
  10.         HttpEntity entity = response.getEntity();  
  11.         if (statusLine.getStatusCode() >= 300)   
  12.             throw new HttpResponseException(  
  13.                     statusLine.getStatusCode(),  
  14.                     statusLine.getReasonPhrase());  
  15.           
  16.         if (entity == null)   
  17.             throw new ClientProtocolException("Response contains no content");  
  18.           
  19.         Gson gson = new GsonBuilder().create();  
  20.         ContentType contentType = ContentType.getOrDefault(entity);  
  21.         Charset charset = contentType.getCharset();  
  22.         Reader reader = new InputStreamReader(entity.getContent(), charset);  
  23.         return gson.fromJson(reader, MyJsonObject.class);  
  24.       
  25. ;  
  26. MyJsonObject myjson = client.execute(httpget, rh);  

 

1.2. HttpClient接口

对于Http请求执行过程来说,HttpClient的接口有着必不可少的作用。HttpClient接口没有对Http请求的过程做特别的限制和详细的规定,连接管理、状态管理、授权信息和重定向处理这些功能都单独实现。这样用户就可以更简单地拓展接口的功能(比如缓存响应内容)。

一般说来,HttpClient实际上就是一系列特殊的handler或者说策略接口的实现,这些handler(测试接口)负责着处理Http协议的某一方面,比如重定向、认证处理、有关连接持久性和keep alive持续时间的决策。这样就允许用户使用自定义的参数来代替默认配置,实现个性化的功能。

  1. ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy()   
  2.   
  3.     @Override  
  4.     public long getKeepAliveDuration(  
  5.             HttpResponse response,  
  6.             HttpContext context)   
  7.         long keepAlive = super.getKeepAliveDuration(response, context);  
  8.         if (keepAlive == -1)   
  9.             // Keep connections alive 5 seconds if a keep-alive value  
  10.             // has not be explicitly set by the server  
  11.             keepAlive = 5000;  
  12.           
  13.         return keepAlive;  
  14.       
  15.   
  16. ;  
  17. CloseableHttpClient httpclient = HttpClients.custom()  
  18.         .setKeepAliveStrategy(keepAliveStrat)  
  19.         .build();  

 

1.2.1.HTTPCLIENT的线程安全性

HttpClient已经实现了线程安全。所以希望用户在实例化HttpClient时,也要支持为多个请求使用。

1.2.2.HTTPCLIENT的内存分配

当一个CloseableHttpClient的实例不再被使用,并且它的作用范围即将失效,和它相关的连接必须被关闭,关闭方法可以调用CloseableHttpClientclose()方法。

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

 

1.3.Http执行上下文

最初,Http被设计成一种无状态的、面向请求-响应的协议。然而,在实际使用中,我们希望能够在一些逻辑相关的请求-响应中,保持状态信息。为了使应用程序可以保持Http的持续状态,HttpClient允许http连接在特定的Http上下文中执行。如果在持续的http请求中使用了同样的上下文,那么这些请求就可以被分配到一个逻辑会话中。HTTP上下文就和一个java.util.Map<String, Object>功能类似。它实际上就是一个任意命名的值的集合。应用程序可以在Http请求执行前填充上下文的值,也可以在请求执行完毕后检查上下文。

HttpContext可以包含任意类型的对象,因此如果在多线程中共享上下文会不安全。推荐每个线程都只包含自己的http上下文。

在Http请求执行的过程中,HttpClient会自动添加下面的属性到Http上下文中:

  • HttpConnection的实例,表示客户端与服务器之间的连接
  • HttpHost的实例,表示要连接的目标服务器
  • HttpRoute的实例,表示全部的连接路由
  • HttpRequest的实例,表示Http请求。在执行上下文中,最终的HttpRequest对象会代表http消息的状态。Http/1.0和Http/1.1都默认使用相对的uri。但是如果使用了非隧道模式的代理服务器,就会使用绝对路径的uri。
  • HttpResponse的实例,表示Http响应
  • java.lang.Boolean对象,表示是否请求被成功的发送给目标服务器
  • RequestConfig对象,表示http request的配置信息
  • java.util.List<Uri>对象,表示Http响应中的所有重定向地址

我们可以使用HttpClientContext这个适配器来简化和上下文交互的过程。

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

 

同一个逻辑会话中的多个Http请求,应该使用相同的Http上下文来执行,这样就可以自动地在http请求中传递会话上下文和状态信息。
在下面的例子中,我们在开头设置的参数,会被保存在上下文中,并且会应用到后续的http请求中。

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

 

1.4. 异常处理

HttpClient会被抛出两种类型的异常,一种是java.io.IOException,当遇到I/O异常时抛出(socket超时,或者socket被重置);另一种是HttpException,表示Http失败,如Http协议使用不正确。通常认为,I/O错误时不致命、可修复的,而Http协议错误是致命了,不能自动修复的错误。

1.4.1.HTTP传输安全

Http协议不能满足所有类型的应用场景,我们需要知道这点。Http是个简单的面向协议的请求/响应的协议,当初它被设计用来支持静态或者动态生成的内容检索,之前从来没有人想过让它支持事务性操作。例如,Http服务器成功接收、处理请求后,生成响应消息,并且把状态码发送给客户端,这个过程是Http协议应该保证的。但是,如果客户端由于读取超时、取消请求或者系统崩溃导致接收响应失败,服务器不会回滚这一事务。如果客户端重新发送这个请求,服务器就会重复的解析、执行这个事务。在一些情况下,这会导致应用程序的数据损坏和应用程序的状态不一致。

即使Http当初设计是不支持事务操作,但是它仍旧可以作为传输协议为某些关键程序提供服务。为了保证Http传输层的安全性,系统必须保证应用层上的http方法的幂等性。

1.4.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默认把非实体方法gethead方法看做幂等方法,把实体方法postput方法看做非幂等方法。

1.4.3.异常自动修复

默认情况下,HttpClient会尝试自动修复I/O异常。这种自动修复仅限于修复几个公认安全的异常。

  • HttpClient不会尝试修复任何逻辑或者http协议错误(即从HttpException衍生出来的异常)。
  • HttpClient会自动再次发送幂等的方法(如果首次执行失败)。
  • HttpClient会自动再次发送遇到transport异常的方法,前提是Http请求仍旧保持着连接(例如http请求没有全部发送给目标服务器,HttpClient会再次尝试发送)。

1.4.4.请求重试HANDLER

如果要自定义异常处理机制,我们需要实现HttpRequestRetryHandler接口。

  1. HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler()   
  2.   
  3.     public boolean retryRequest(  
  4.             IOException exception,  
  5.             int executionCount,  
  6.             HttpContext context)   
  7.         if (executionCount >= 5)   
  8.             // Do not retry if over max retry count  
  9.             return false;  
  10.           
  11.         if (exception instanceof InterruptedIOException)   
  12.             // Timeout  
  13.             return false;  
  14.           
  15.         if (exception instanceof UnknownHostException)   
  16.             // Unknown host  
  17.             return false;  
  18.           
  19.         if (exception instanceof ConnectTimeoutException)   
  20.             // Connection refused  
  21.             return false;  
  22.           
  23.         if (exception instanceof SSLException)   
  24.             // SSL handshake exception  
  25.             return false;  
  26.           
  27.         HttpClientContext clientContext = HttpClientContext.adapt(context);  
  28.         HttpRequest request = clientContext.getRequest();  
  29.         boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);  
  30.         if (idempotent)   
  31.             // Retry if the request is considered idempotent  
  32.             return true;  
  33.           
  34.         return false;  
  35.       
  36. ;  
  37. CloseableHttpClient httpclient = HttpClients.custom()  
  38.         .setRetryHandler(myRetryHandler)  
  39.         .build();  

 

1.5.终止请求

有时候由于目标服务器负载过高或者客户端目前有太多请求积压,http请求不能在指定时间内执行完毕。这时候终止这个请求,释放阻塞I/O的进程,就显得很必要。通过HttpClient执行的Http请求,在任何状态下都能通过调用HttpUriRequestabort()方法来终止。这个方法是线程安全的,并且能在任何线程中调用。当Http请求被终止了,本线程(即使现在正在阻塞I/O)也会通过抛出一个InterruptedIOException异常,来释放资源。

1.6. Http协议拦截器

HTTP协议拦截器是一种实现一个特定的方面的HTTP协议的代码程序。通常情况下,协议拦截器会将一个或多个头消息加入到接受或者发送的消息中。协议拦截器也可以操作消息的内容实体—消息内容的压缩/解压缩就是个很好的例子。通常,这是通过使用“装饰”开发模式,一个包装实体类用于装饰原来的实体来实现。一个拦截器可以合并,形成一个逻辑单元。

协议拦截器可以通过共享信息协作——比如处理状态——通过HTTP执行上下文。协议拦截器可以使用Http上下文存储一个或者多个连续请求的处理状态。

通常,只要拦截器不依赖于一个特定状态的http上下文,那么拦截执行的顺序就无所谓。如果协议拦截器有相互依赖关系,必须以特定的顺序执行,那么它们应该按照特定的顺序加入到协议处理器中。

协议处理器必须是线程安全的。类似于servlets,协议拦截器不应该使用变量实体,除非访问这些变量是同步的(线程安全的)。

下面是个例子,讲述了本地的上下文时如何在连续请求中记录处理状态的:

  1. CloseableHttpClient httpclient = HttpClients.custom()  
  2.         .addInterceptorLast(new HttpRequestInterceptor()   
  3.   
  4.             public void process(  
  5.                     final HttpRequest request,  
  6.                     final HttpContext context) throws HttpException, IOException   
  7.                 AtomicInteger count = (AtomicInteger) context.getAttribute("count");  
  8.                 request.addHeader("Count", Integer.toString(count.getAndIncrement()));  
  9.               
  10.   
  11.         )  
  12.         .build();  
  13.   
  14. AtomicInteger count = new AtomicInteger(1);  
  15. HttpClientContext localContext = HttpClientContext.create();  
  16. localContext.setAttribute("count", count);  
  17.   
  18. HttpGet httpget = new HttpGet("http://localhost/");  
  19. for (int i = 0; i < 10; i++)   
  20.     CloseableHttpResponse response = httpclient.execute(httpget, localContext);  
  21.     try   
  22.         HttpEntity entity = response.getEntity();  
  23.      finally   
  24.         response.close();  
  25.       
  26.   

 

上面代码在发送http请求时,会自动添加Count这个header,可以使用wireshark抓包查看。

1.7.1. 重定向处理

HttpClient会自动处理所有类型的重定向,除了那些Http规范明确禁止的重定向。See Other (status code 303) redirects on POST and PUT requests are converted to GET requests as required by the HTTP specification. 我们可以使用自定义的重定向策略来放松Http规范对Post方法重定向的限制。

  1. LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();  
  2. CloseableHttpClient httpclient = HttpClients.custom()  
  3.         .setRedirectStrategy(redirectStrategy)  
  4.         .build();  

 

HttpClient在请求执行过程中,经常需要重写请求的消息。 HTTP/1.0和HTTP/1.1都默认使用相对的uri路径。同样,原始的请求可能会被一次或者多次的重定向。最终结对路径的解释可以使用最初的请求和上下文。URIUtils类的resolve方法可以用于将拦截的绝对路径构建成最终的请求。这个方法包含了最后一个分片标识符或者原始请求。

  1. CloseableHttpClient httpclient = HttpClients.createDefault();  
  2. HttpClientContext context = HttpClientContext.create();  
  3. HttpGet httpget = new HttpGet("http://localhost:8080/");  
  4. CloseableHttpResponse response = httpclient.execute(httpget, context);  
  5. try   
  6.     HttpHost target = context.getTargetHost();  
  7.     List<URI> redirectLocations = context.getRedirectLocations();  
  8.     URI location = URIUtils.resolve(httpget.getURI(), target, redirectLocations);  
  9.     System.out.println("Final HTTP location: " + location.toASCIIString());  
  10.     // Expected to be an absolute URI  
  11.  finally   
  12.     response.close();  
  13.   

 

第二章 连接管理

 

2.1.持久连接

两个主机建立连接的过程是很复杂的一个过程,涉及到多个数据包的交换,并且也很耗时间。Http连接需要的三次握手开销很大,这一开销对于比较小的http消息来说更大。但是如果我们直接使用已经建立好的http连接,这样花费就比较小,吞吐率更大。

HTTP/1.1默认就支持Http连接复用。兼容HTTP/1.0的终端也可以通过声明来保持连接,实现连接复用。HTTP代理也可以在一定时间内保持连接不释放,方便后续向这个主机发送http请求。这种保持连接不释放的情况实际上是建立的持久连接。HttpClient也支持持久连接。

2.2.HTTP连接路由

HttpClient既可以直接、又可以通过多个中转路由(hops)和目标服务器建立连接。HttpClient把路由分为三种plain(明文 ),tunneled(隧道)和layered(分层)。隧道连接中使用的多个中间代理被称作代理链。

客户端直接连接到目标主机或者只通过了一个中间代理,这种就是Plain路由。客户端通过第一个代理建立连接,通过代理链tunnelling,这种情况就是Tunneled路由。不通过中间代理的路由不可能时tunneled路由。客户端在一个已经存在的连接上进行协议分层,这样建立起来的路由就是layered路由。协议只能在隧道—>目标主机,或者直接连接(没有代理),这两种链路上进行分层。

2.2.1.路由计算

RouteInfo接口包含了数据包发送到目标主机过程中,经过的路由信息。HttpRoute类继承了RouteInfo接口,是RouteInfo的具体实现,这个类是不允许修改的。HttpTracker类也实现了RouteInfo接口,它是可变的,HttpClient会在内部使用这个类来探测到目标主机的剩余路由。HttpRouteDirector是个辅助类,可以帮助计算数据包的下一步路由信息。这个类也是在HttpClient内部使用的。

HttpRoutePlanner接口可以用来表示基于http上下文情况下,客户端到服务器的路由计算策略。HttpClient有两个HttpRoutePlanner的实现类。SystemDefaultRoutePlanner这个类基于java.net.ProxySelector,它默认使用jvm的代理配置信息,这个配置信息一般来自系统配置或者浏览器配置。DefaultProxyRoutePlanner这个类既不使用java本身的配置,也不使用系统或者浏览器的配置。它通常通过默认代理来计算路由信息。

2.2.2. 安全的HTTP连接

为了防止通过Http消息传递的信息不被未授权的第三方获取、截获,Http可以使用SSL/TLS协议来保证http传输安全,这个协议是当前使用最广的。当然也可以使用其他的加密技术。但是通常情况下,Http信息会在加密的SSL/TLS连接上进行传输。

2.3. HTTP连接管理器

2.3.1. 管理连接和连接管理器

Http连接是复杂,有状态的,线程不安全的对象,所以它必须被妥善管理。一个Http连接在同一时间只能被一个线程访问。HttpClient使用一个叫做Http连接管理器的特殊实体类来管理Http连接,这个实体类要实现HttpClientConnectionManager接口。Http连接管理器在新建http连接时,作为工厂类;管理持久http连接的生命周期;同步持久连接(确保线程安全,即一个http连接同一时间只能被一个线程访问)。Http连接管理器和ManagedHttpClientConnection的实例类一起发挥作用,ManagedHttpClientConnection实体类可以看做http连接的一个代理服务器,管理着I/O操作。如果一个Http连接被释放或者被它的消费者明确表示要关闭,那么底层的连接就会和它的代理进行分离,并且该连接会被交还给连接管理器。这是,即使服务消费者仍然持有代理的引用,它也不能再执行I/O操作,或者更改Http连接的状态。

下面的代码展示了如何从连接管理器中取得一个http连接:

  1. HttpClientContext context = HttpClientContext.create();  
  2. HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();  
  3. HttpRoute route = new HttpRoute(new HttpHost("localhost"80));  
  4. // 获取新的连接. 这里可能耗费很多时间  
  5. ConnectionRequest connRequest = connMrg.requestConnection(route, null);  
  6. // 10秒超时  
  7. HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);  
  8. try   
  9.     // 如果创建连接失败  
  10.     if (!conn.isOpen())   
  11.         // establish connection based on its route info  
  12.         connMrg.connect(conn, route, 1000, context);  
  13.         // and mark it as route complete  
  14.         connMrg.routeComplete(conn, route, context);  
  15.       
  16.      // 进行自己的操作.  
  17.  finally   
  18.     connMrg.releaseConnection(conn, null1, TimeUnit.MINUTES);  
  19.   

 

如果要终止连接,可以调用ConnectionRequestcancel()方法。这个方法会解锁被ConnectionRequestget()方法阻塞的线程。

2.3.2.简单连接管理器

BasicHttpClientConnectionManager是个简单的连接管理器,它一次只能管理一个连接。尽管这个类是线程安全的,它在同一时间也只能被一个线程使用。BasicHttpClientConnectionManager会尽量重用旧的连接来发送后续的请求,并且使用相同的路由。如果后续请求的路由和旧连接中的路由不匹配,BasicHttpClientConnectionManager就会关闭当前连接,使用请求中的路由重新建立连接。如果当前的连接正在被占用,会抛出java.lang.IllegalStateException异常。

2.3.3.连接池管理器

相对BasicHttpClientConnectionManager来说,PoolingHttpClientConnectionManager是个更复杂的类,它管理着连接池,可以同时为很多线程提供http连接请求。Connections are pooled on a per route basis.当请求一个新的连接时,如果连接池有有可用的持久连接,连接管理器就会使用其中的一个,而不是再创建一个新的连接。

PoolingHttpClientConnectionManager维护的连接数在每个路由基础和总数上都有限制。默认,每个路由基础上的连接不超过2个,总连接数不能超过20。在实际应用中,这个限制可能会太小了,尤其是当服务器也使用Http协议时。

下面的例子演示了如果调整连接池的参数:

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

 

2.3.4.关闭连接管理器

当一个HttpClient的实例不在使用,或者已经脱离它的作用范围,我们需要关掉它的连接管理器,来关闭掉所有的连接,释放掉这些连接占用的系统资源。

  1. CloseableHttpClient httpClient = <...>  
  2. httpClient.close();  

 

2.4.多线程请求执行

当使用了请求连接池管理器(比如PoolingClientConnectionManager)后,HttpClient就可以同时执行多个线程的请求了。

PoolingClientConnectionManager会根据它的配置来分配请求连接。如果连接池中的所有连接都被占用了,那么后续的请求就会被阻塞,直到有连接被释放回连接池中。为了防止永远阻塞的情况发生,我们可以把http.conn-manager.timeout的值设置成一个整数。如果在超时时间内,没有可用连接,就会抛出ConnectionPoolTimeoutException异常。

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

    以上是关于HttpClient 完整教程的主要内容,如果未能解决你的问题,请参考以下文章

    HttpClient学习整理

    C++开发人脸性别识别教程(17)——添加辅助功能

    HttpClient教程

    干货关于「Serverless」的完整指南:你知道和不知道的

    ffmpeg 视频教程 添加水印附源码

    较新的 CouchDB/Sofa 教程