1.1 Request 的执行
HttpClient最必不可少的功能就是执行HTTP的方法,执行HTTP方法会涉及到一个或者多个HTTP request/HTTP response交换,而这些过程通常会在HttpClient内部完成。使用者提交一个request的对象去执行,HttpClient会发送这个request到目标服务器并且获得一个对应的response对象,如果不成功的话则抛出一个异常。
自然而然,满足了上面描述的HttpClient接口就是HttpClient API的主要入口。
下面是一个最简单的request执行过程:
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://localhost/"); CloseableHttpResponse response = httpclient.execute(httpget); try { <...> } finally { response.close(); }
1.1.1 HTTP request
所有的HTTP request都有一个请求线,包含了请求的方法名称,请求的URI和HTTP协议版本。
HttpClient开箱即用的支持所有HTTP/1.1中定义的HTTP方法,包括GET,HEAD,POST,PUT,DELETE,TRACE 和 OPTIONS,这些HTTP方法类型都有一个与之对应的类:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace 和 HttpOptions。
Request-URI是一个统一资源定位符,它指明了用于处理该request的资源的位置。HTTP request URIs包含了一个协议类型,主机名,可选端口,资源路径,可选查询参数,可选片段。
HttpGet httpget = new HttpGet( "http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq="); HttpClient提供URIBuilder实用类用于简化request 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());
输出:
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
1.1.2 HTTP response
HTTP response是服务器在接收并且处理完request消息之后返回给客户端的消息,消息的第一行包含了协议版本,status code和与之关联的文本。
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消息包含多个header,这些header描述了消息的属性比如content length,content type等等,HttpClient提供了取出,添加,删除和遍历header的方法。
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);
输出:
Set-Cookie: c1=a; path=/; domain=localhost Set-Cookie: c2=b; path="/", c3=c; domain="localhost" 2
获取所有指定headers的最高效的方式是使用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()); }
输出:
Set-Cookie: c1=a; path=/; domain=localhost Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
该接口同时也提供HTTP消息转换到独立header元素的便利方法。
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]); } }
输出:
c1 = a path=/ domain=localhost c2 = b path=/ c3 = c domain=localhost
1.1.4 HTTP entity
HTTP消息可以携带跟request和response相关联的content entity。由于这些Entity是可选的,所以他们在某些request和response中可以查找到,有些则查不到。使用了entity的request被称为entity封装请求,HTTP定义了两种entity封装请求方法:POST 和 PUT。Response通常会封装一个content entity。不同的方法会有对应的异常,比如对于HEAD来说就有204 No Content,304 Not Modified, 205 Reset Content异常。
HttpClient根据内容来源将Entities区分为3种,根据内容来源区分为:
streamed: 内容来自于流或者实时产生。特别的是,这个分类包含了从HTTP responses接收到的entities,Streamed entities通常不可重复。
self-contained: 内容来自于内存或者通过独立于connection或者其他entity的手段获得,self-contained entities通常可以重复,其也是封装了entity的HTTP request最常用的Entities类型。
wrapping : 内容来自于其他的entity。
这种区分对于HTTP response输出内容时的连接管理是非常重要的。对于request entites来说,由于其是被应用创建并且通过HttpClient发送,streamed和self-contained有什么不同就没有那么重要。
由此,建议将不可重复的entites视为streamed,可以重复的entities视为self-contained。
1.1.4.1 可重复的entities
一个entity能够重复,表明它的内容可以被读取多次,这种情况只会在self contained entities中发生(比如ByteArrayEntity 或者 StringEntity)
1.1.4.2 使用HTTP entities
Entity可以同时代表二进制和文字内容,支持对字符进行编码,比如对character content进行编码。
Entity产生于request(封装内容 )执行时,或者request成功请求并且response body用于发送结果数据到客户端时。
从Entity中读取内容,可以通过HttpEntity#getContent()获取input stream,也可以向HttpEntity#writeTo(OutputStream)提供一个output stream,该方法会将内容一次性全部写回给指派的stream。
当消息到达并且Entity已经被接收时,可以用 HttpEntity#getContentType() 和 HttpEntity#getContentLength() 方法读取常用的元数据,如Content-Type 和 Content-Length 头信息,Content-Type头信息包含文本的mime-type编码信息如 text/plain 或者 text/html,该信息可以通过 HttpEntity#getContentType() 获取到,如果Header里面不包含这些信息,那么 HttpEntity#getContentLength() 会返回-1 并且 HttpEntity#getContentType() 返回 NULL,如果Header包含这些信息,那么 HttpEntity#getContentType() 会返回 Header 对象。
当创建一个出站消息的entity时,必须同时指定其相关的参数。
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 确保释放低级资源
为了保证恰当的释放系统资源,使用完毕后必须关闭entity相关的stream和response。
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(); }
关闭内容流和关闭response的不同之处在于,关闭内容流会尝试通过消费entity的content来保持底层连接,而关闭response则是直接关闭和丢弃掉这个连接。
注意 HttpEntity#writeTo(OutputStream) 方法也需要在entity被写出到流之后恰当的释放系统资源。如果通过HttpEntity#getContent()获取了java.io.InputStream流的实例,那么在finally阶段也需要关闭该流。
当处理streaming entites时,可以使用 EntityUtils#consume(HttpEntity) 方法来确保entity内容已经被完全消费并且底层stream已经被关闭。
有另一种情况是,当我们只需要从response content里面获取一小部分数据,但是消费剩余数据和保持连接复用的性能损失又太高时,我们可以直接关闭response。
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.1.6 消费entity内容
消费Entity内容的推荐方式是使用 HttpEntity#getContent() 或者 HttpEntity#writeTo(OuptputStream) 方法,也可以使用EntityUtils类,该类提供了一些静态方法来简化读取Entity内容或者其他信息。通过使用该类的某些方法,你可以以String /byte[] 方式来获取整个content body,从而替代直接读取 InputStream 的方式。但是,除非你知道response entity来自于可信HTTP server 并且其长度有限,否则不推荐使用EntityUtils。
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(); }
某些情况下你可能读取entity content不止一次,这时就需要将内容通过内存或者磁盘的方式缓存起来,最简单的实现方式就是使用BufferedHttpEntity包装源Entity,该类会将源 Entity 的内容读取到内存中。如果通过其他方式来实现,那么就必须要保存一个源entity了。
CloseableHttpResponse response = <...> HttpEntity entity = response.getEntity(); if (entity != null) { entity = new BufferedHttpEntity(entity); }
1.1.7 生产 entity content
HttpClient提供多个可以通过HTTP连接高效的输出内容的类,这些类的实例可以跟POST和PUT等request相关联并且为request封装entity内容,HttpClient提供了最常用的数据容器如string, byte array , input stream 和文件 : StringEntity , ByteArrayEntity,InputStreamEntity,和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);
注意 InputStreamEntity 不可以重复,因为它只能从底层数据流读取一次。通常建议用HttpEntity的实现(self-contained)来替代普通的InputStreamEntity,FileEntity是一个不错的起始点。
1.1.7.1 HTML表单
许多应用程序需要模拟表单提交的过程,比如登录web应用或者提交input 数据,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);
UrlEncodedFormEntity实例会使用URL encoding来编码参数并且产生出如下内容:
param1=value1¶m2=value2
1.1.7.2 内容分块
通常来说,建议让HttpClient选择最合适的传输编码,HttpClient选择的传输编码会基于被传输的HTTP消息的属性而定。你可以通过设置HttpEntity#setChunked()为true来通知HttpClient需要进行chunk编码。注意HttpClient只是把这一标识当做一个提示使用,该值在不支持chunk编码的HTTP协议版本中如HTTP/1.0中会被忽略。
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.1.8 Response handlers
最简单和方便处理response的方式是使用ResponseHandler,该类包含了handleResponse(HttpResponse response)方法。该方法使得用户完全不需要去操心连接管理的事情。当使用ResponseHandler时,HttpClient会自动确保将连接释放回连接管理器,无论request是否执行成功。
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);