HTTP 是目前互联网上最重要的协议之一。随着 Web 服务、网络设备以及网络计算的不断发展,HTTP 协议的作用不断扩大,不仅限于 Web 浏览器的使用范围,也增加了需要 HTTP 协议支持的应用程序的数量。虽然 java.net 包提供了基本的通过 HTTP 访问资源的功能,但是它缺乏全面的灵活性和其他许多应用程序需要的功能。因此,HttpClient 组件就是一款旨在填补这一空白的软件包,通过提供有效、保持更新且功能丰富的功能,实现客户端最新的 HTTP 标准和建议。HttpClient 组件旨在为扩展而设计,提供强大的支持,使开发人员能够构建基于 HTTP 客户端的应用程序,例如 Web 浏览器、Web 服务端,以及利用或扩展 HTTP 协议进行分布式通信的系统。
这个客户端 HTTP 运输实现库是基于 HttpCore(http://hc.apache.org/httpcomponents-core/index.html)开发的。它使用经典(阻塞)I/O 操作来实现 HTTP 传输。
HttpClient 并非浏览器,而是一个客户端 HTTP 通讯实现库。它的主要目的是发送和接收 HTTP 报文。和浏览器不同的是,HttpClient 不会缓存内容、执行 HTML 页面中的 JavaScript 代码、猜测内容类型、重新格式化请求/重定向 URI 或实现和 HTTP 通讯无关的功能。
HttpClient 最主要的功能是执行 HTTP 方法,该过程包含一个或多个 HTTP 请求/响应的交互,通常由 HttpClient 内部处理。用户需要提供请求对象,而 HttpClient 则传输请求到目标服务器并返回对应的响应对象,或在执行失败时抛出异常。因此,HttpClient API 的关键在于定义描述上述规约的 HttpClient 接口。
下面是一个简单的请求执行示例:
HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int l;
byte[] tmp = new byte[2048];
while ((l = instream.read(tmp)) != -1) {
}
}
HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient 提供很多工具方法来简化创建和修改执行 URI。URI 也可以编程来拼装:
URI uri = URIUtils.createURI("http", "www.google.com", -1, "/search",
"q=httpclient&btnG=Google+Search&aq=f&oq=", null);
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());
输出内容为:
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
List<NameValuePair> qparams = new ArrayList<NameValuePair>();
qparams.add(new BasicNameValuePair("q", "httpclient"));
qparams.add(new BasicNameValuePair("btnG", "Google Search"));
qparams.add(new BasicNameValuePair("aq", "f"));
qparams.add(new BasicNameValuePair("oq", null));
URI uri = URIUtils.createURI("http", "www.google.com", -1, "/search",
URLEncodedUtils.format(qparams, "UTF-8"), null);
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());
输出内容为:
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
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());
输出内容为:
HTTP/1.1
200
OK
HTTP/1.1 200 OK
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);
输出内容为:
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
如果要获得特定类型的所有头部信息,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 报文解析为各个独立的头部信息元素。
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
HTTP 报文能够携带与请求或响应相关的内容实体,但这些实体是可选的。在某些请求和响应中可能出现。使用了实体的请求被称为封闭实体请求,而HTTP规范定义了两种封装实体的方法:POST和PUT。响应通常会包含一个内容实体,但也有例外,例如 HEAD 方法的响应以及 204 No Content、304 Not Modified 和 205 Reset Content 响应。
流式实体:内容要么从流中获得,要么在运行时生成。特别是从 HTTP 响应中获取的实体属于此类。流式实体不可重复生成。
自我包含式实体:内容在内存中或通过独立的连接或其他实体中获得。此类实体可以重复生成。这种类型的实体通常用于封闭 HTTP 请求的实体。
包装式实体:内容从另一个实体中获得。
对于从 HTTP 响应中获取流式内容的情况,这种区分对于连接管理非常重要。但对于仅由 HttpClient 发送的请求实体,流式和自我包含式实体的不同就不那么重要了。在这种情况下,建议考虑使用不能重复生成的流式实体或者可以重复生成的自我包含式实体。
如果一个实体是自我包含式的(例如 ByteArrayEntity 或 StringEntity),那么它的内容可以被多次读取,这就意味着这种实体可以重复使用。
一个实体可以代表二进制内容或字符内容,因此支持字符编码(如果是字符内容)。实体是在执行请求时,或请求已成功执行时,或将响应体发送到客户端时创建的。要读取实体内容,可以通过HttpEntity#getContent()方法从输入流中获取内容。它将返回一个java.io.InputStream对象。您还可以通过调用HttpEntity#writeTo(OutputStream)方法,向给定的输出流写入实体中的内容,该方法将一次返回写入到流中的所有内容。
当使用一个收到的报文获取实体时,可使用HttpEntity#getContentType()方法和HttpEntity#getContentLength()方法读取常见的元数据,如Content-Type和Content-Length头信息(如果可用)。HttpEntity#getContentEncoding()方法用于读取Content-Type头信息中可能包含的文本MIME类型的字符编码,例如text/plain或text/html。如果头信息不可用,则长度返回-1,ContentType返回NULL。如果Content-Type头信息可用,则返回一个Header对象。
传出报文创建实体时,需要通过实体创建器提供元数据。
StringEntity myEntity = new StringEntity("important message","UTF-8");
System.out.println(myEntity.getContentType());
System.out.println(myEntity.getContentLength());
System.out.println(EntityUtils.getContentCharSet(myEntity));
System.out.println(EntityUtils.toString(myEntity));
System.out.println(EntityUtils.toByteArray(myEntity).length);
输出内容为
Content-Type: text/plain; charset=UTF-8
17
UTF-8
important message
17
完成响应实体后,务必完全消耗实体内容,以确保连接可以安全地返回到连接池中并由连接管理器重用。可以通过调用 HttpEntity#consumeContent()方法来方便地处理此操作。当 HttpClient 探测到内容流已经到达尾部时,会自动释放连接并返回到连接管理器。此外,多次调用 HttpEntity#consumeContent()方法也是安全的。
有时会出现特殊情况,例如只需要获取响应内容的一小部分,消耗剩余内容会导致性能损失,或者重用连接的代价太高。这种情况下,可以通过调用 HttpUriRequest#abort()方法来中止请求。
HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int byteOne = instream.read();
int byteTwo = instream.read();
// Do not need the rest
httpget.abort();
}
虽然连接不会被重用,但仍然会正确释放由它持有的所有级别的资源。
推荐使用 HttpEntity#getContent() 或 HttpEntity#writeTo(OutputStream) 方法来消耗实体内容。HttpClient 还自带 EntityUtils 类,其中包含一些静态方法,可更轻松地从实体中读取内容或信息。可以使用该类方法以字符串/字节数组的形式获取整个内容体,而不是直接读取 java.io.InputStream。需要注意的是,强烈不建议使用 EntityUtils,除非响应实体源自可靠的 HTTP 服务器并有已知的长度限制。
HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
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
}
}
在某些情况下,可能需要对实体内容进行多次读取。为此,需要使用某种方式将实体内容缓存到内存或磁盘中。最简单的方法是使用 BufferedHttpEntity 类来封装源实体,这样可以将源实体的内容读取到内存缓冲区中。在其他方式中,实体包装器会获得源实体的引用。
HttpGet httpget = new HttpGet("http://localhost/");
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
HttpClient 提供了多种类来生成通过 HTTP 连接获得内容的有效输出流。这些类的实例可以和封闭如 POST 和 PUT 请求的实体相关联,以封闭实体从 HTTP 请求中获得的输出内容。HttpClient 还提供了多种公用的数据容器,如字符串、字节数组、输入流和文件等,来保证数据的完整性和传输的可靠性。这些数据容器的具体类包括:StringEntity、ByteArrayEntity、InputStreamEntity 和 FileEntity。
File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file, "text/plain; charset=\"UTF-8\"");
HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);
注意,InputStreamEntity 无法重复读取内容,因为它只能从底层数据流中读取一次。为了避免此问题,建议使用自定义的 HttpEntity 类,这是一种自包含的实现,可以替代使用通用的 InputStreamEntity。另外,FileEntity 也是一个很好的选择。
通常,HTTP实体需要基于特定的执行上下文来动态生成。HttpClient通过使用EntityTemplate实体类和ContentProducer接口提供了动态实体的支持。内容生成器是生成它们内容的对象,将它们写入到一个输出流中。它们是每次请求时按需生成内容,所以由EntityTemplate创建的实体通常是自包含的并且可以重复使用。
ContentProducer cp = new ContentProducer() {
public void writeTo(OutputStream outstream) throws IOException {
Writer writer = new OutputStreamWriter(outstream, "UTF-8");
writer.write("" );
writer.write(" " );
writer.write(" important stuff");
writer.write(" ");
writer.write("");
writer.flush();
}
};
HttpEntity entity = new EntityTemplate(cp);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
许多应用程序经常需要模拟提交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, "UTF-8");
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
使用UrlEncodedFormEntity实例时,会使用URL编码来编码参数,生成如下的内容: param1=value1¶m2=value2
通常情况下,我们建议使用HttpClient自行选择最适合的编码转换方式,这是可能的。而设置HttpEntity#setChunked()方法为true可以告知HttpClient使用分块编码。需要注意的是,HttpClient会将这个标识作为提示。当HTTP协议版本(如HTTP/1.0)不支持分块编码时,这个属性值会被忽略。
StringEntity entity = new StringEntity("important message","text/plain; charset=\"UTF-8\"");
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);
使用ResponseHandler接口是最简单也是最方便控制响应的方式。使用该接口后,用户无需再担心连接管理的问题。HttpClient会自动管理连接,确保连接被释放到连接管理器中去,不管请求执行成功与否或引发了异常。
HttpClient httpclient = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://localhost/");
ResponseHandler<byte[]> handler = new ResponseHandler<byte[]>() {
public byte[] handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
HttpEntity entity = response.getEntity();
if (entity != null) {
return EntityUtils.toByteArray(entity);
} else {
return null;
}
}
};
byte[] response = httpclient.execute(httpget, handler);
此文介绍了HttpClient库中的几个重要概念和功能。