Http 协议应该是互联网中最重要的协议。持续增长的 web 服务、可联网的家用电器等都在继承并拓 展着 Http 协议,向着浏览器之外的方向发展。
虽然 jdk 中的 java.net 包中提供了一些基本的方法,通过 http 协议来访问网络资源,但是大多数场 景下,它都不够灵活和强大。HttpClient 致力于填补这个空白,它可以提供有效的、最新的、功能丰 富的包来实现 http 客户端
以下是一个简单的请求例子:
@Test public void test01() throws Exception { CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://www.baidu.com/"); CloseableHttpResponse response = httpclient.execute(httpget); try { System.out.println(response); } catch (Exception e) { } finally { response.close(); } }
1.1.1. HTTP 请求
所有的 Http 请求都有一个请求行(request line),包括方法名、请求的 URI 和 Http 版本号。
HttpClient 支持 HTTP/1.1 这个版本定义的所有 Http 方法:GET,HEAD,POST,PUT,DELETE,TRACE 和 OPTIONS。对于每一种 http 方法,HttpClient 都定义了一个相应的类:
HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTrace 和 HttpOpquertions。
Request-URI 即统一资源定位符,用来标明 Http 请求中的资源。Http request URIs 包含协议名、主 机名、主机端口(可选)、资源路径、query(可选)和片段信息(可选)。
HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq= f&oq=");
HttpClient 提供 URIBuilder 工具类来简化 URIs 的创建和修改过程。
@Test public void test02() throws Exception { 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响应
服务器收到客户端的http请求后,就会对其进行解析,然后把响应发给客户端,这个响应就是HTTP response.HTTP响应第一行是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.1.3. 消息头
一个Http消息可以包含一系列的消息头,用来对http消息进行描述,比如消息长度,消息类型等等。HttpClient提供了API来获取、添加、修改、遍历消息头。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); response.addHeader("Set-Cookie", "c1=a; path=/; domain=yeetrack.com"); 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);
最有效的获取指定类型的消息头的方法还是使用HeaderIterator
接口。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); response.addHeader("Set-Cookie", "c1=a; path=/; domain=yeetrack.com"); 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()); }
HeaderIterator也提供非常便捷的方式,将Http消息解析成单独的消息头元素。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); response.addHeader("Set-Cookie", "c1=a; path=/; domain=yeetrack.com"); response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"yeetrack.com\""); 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.1.4. HTTP实体
Http消息可以携带http实体,这个http实体既可以是http请求,也可以是http响应的。Http实体,可以在某些http请求或者响应中发现,但不是必须的。Http规范中定义了两种包含请求的方法:POST和PUT。HTTP响应一般会包含一个内容实体。当然这条规则也有异常情况,如Head方法的响应,204没有内容,304没有修改或者205内容资源重置。
HttpClient根据来源的不同,划分了三种不同的Http实体内容。
- streamed: Http内容是通过流来接受,streamed这一类包含从http响应中获取的实体内容。一般说来,streamed实体是不可重复的。
- self-contained: self-contained类型的实体内容通常是可重复的。这种类型的实体通常用于关闭http请求。
- wrapping: 这种类型的内容是从另外的http实体中获取的。
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-Type
和Content-Length
两个头消息(如果有的话)。由于Content-Type
包含mime-types的字符编码,比如text/plain或者text/html,HttpEntity
类的getContentEncoding()
方法就是读取这个编码的。如果头信息不存在,getContentLength()
会返回-1,getContentType()
会返回NULL。如果Content-Type
信息存在,就会返回一个Header
类。
当为发送消息创建Http实体时,需要同时附加meta信息。
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.1.5. 确保底层的资源连接被释放
为了确保系统资源被正确地释放,我们要么管理Http实体的内容流、要么关闭Http响应。
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://www.baidu.com/"); 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(); }
关闭Http实体内容流和关闭Http响应的区别在于,前者通过消耗掉Http实体内容来保持相关的http连接,然后后者会立即关闭、丢弃http连接。
1.1.6. 消耗HTTP实体内容
HttpClient推荐使用HttpEntity
的getConent()
方法或者HttpEntity
的writeTo(OutputStream)
方法来消耗掉Http实体内容。HttpClient也提供了EntityUtils
这个类,这个类提供一些静态方法可以更容易地读取Http实体的内容和信息。和以java.io.InputStream
流读取内容的方式相比,EntityUtils提供的方法可以以字符串或者字节数组的形式读取Http实体。但是,强烈不推荐使用EntityUtils
这个类,除非目标服务器发出的响应是可信任的,并且http响应实体的长度不会过大。
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://www.yeetrack.com/"); 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(); }
有些情况下,我们希望可以重复读取Http实体的内容。这就需要把Http实体内容缓存在内存或者磁盘上。最简单的方法就是把Http Entity转化成BufferedHttpEntity
,这样就把原Http实体的内容缓冲到了内存中。后面我们就可以重复读取BufferedHttpEntity中的内容。
CloseableHttpResponse response = <...> HttpEntity entity = response.getEntity(); if (entity != null) { entity = new BufferedHttpEntity(entity); }
1.1.7. 创建HTTP实体内容
HttpClient提供了一个类,这些类可以通过http连接高效地输出Http实体内容。HttpClient提供的这几个类涵盖的常见的数据类型,如String,byte数组,输入流,和文件类型: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://www.yeetrack.com/action.do"); httppost.setEntity(entity); }
请注意由于InputStreamEntity
只能从下层的数据流中读取一次,所以它是不能重复的。推荐,通过继承HttpEntity
这个自包含的类来自定义HttpEntity类,而不是直接使用InputStreamEntity
这个类。FileEntity
就是一个很好的起点(FileEntity就是继承的HttpEntity)。
1.7.1.1. HTML表单
很多应用程序需要模拟提交Html表单的过程,举个例子,登陆一个网站或者将输入内容提交给服务器。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://www.yeetrack.com/handler.do"); httppost.setEntity(entity);
UrlEncodedFormEntity
实例会使用所谓的Url编码的方式对我们的参数进行编码,产生的结果如下:
param1=value1¶m2=value2
1.1.7.2. 内容分块
一般来说,推荐让HttpClient自己根据Http消息传递的特征来选择最合适的传输编码。当然,如果非要手动控制也是可以的,可以通过设置HttpEntity
的setChunked()
为true。请注意:HttpClient仅会将这个参数看成是一个建议。如果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://www.yeetrack.com/acrtion.do"); httppost.setEntity(entity);