目前超文本传输协议(HTTP)也许是使用在互联网上的最重大的协议。在浏览器之后,Web服务、网络功能的设备和网络计算的持续发展扩展了HTTP协议的功能。同时也增加了大量需要由HTTP支持的应用程序。
虽然java.net包为通过HTTP来访问资源提供了基础的功能。但是他不能为应用程序提供完整的、灵活或实用的需求。而HttpClient试图去填充这个空白——通过有效的、最新的和功能丰富的包来实现客户端的HTTP新标准。
基于HttpCore的客户端HTTP传输库
基于经典(阻塞)I/O
不可知的网页内容
HttpClient并不是一个浏览器,它是客户端的HTTP传输库。HttpClient的作用是接收HTTP消息。如果‘不明确地设置’或‘重新格式化请求/重定向URI位置’或‘做其它与HTTP传输功能无关的事’,HttpClient不会试图过程连接、执行嵌入HTML页面里的Javascript、尝试猜测连接内容类别。
HttpClient大部分的基础函数都是在执行HTTP的方法。一个HTTP方法执行包括一个或多个HTTP请求/HTTP应答交换,通常这已经在HttpClient内部操作了。
用户提供一个请求对象,HttpClient将这个请求传输给目标服务器,服务器返回一个相应的应答结构,如果不成功则抛出一个异常。
很自然地,了解HttpClientAPI的切入点是上面阐述里规定的HttpClient接口。
这是一个最简单请求执行例子:
CloseableHttpClienthttpclient = HttpClients.createDefault();
HttpGethttpget = new HttpGet("http://localhost/");
CloseableHttpResponseresponse = httpclient.execute(httpget);
try{
<...>
}finally {
response.close();
}
所有的HTTP请求,都有这么一行包括了:方法名、请求URI和HTTP协议版本。
HttpClient支持来自所有HTTP/1.1规则定义的HTTP方法:GET,HEAD, POST, PUT, DELETE, TRACE and OPTIONS。相应类为::HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTrace, andHttpOptions.
Request-URI是用来标识申请请求资源的(UniformResource Identifier)标识符。HTTP请求URI包括:aprotocol scheme(协议方案),host name(主机名),optional port,(端口)resourcepath(资源路径),optional query, and optional fragment.
HttpClient提供URIBuilder功能类来简化创建和修改请求URI的过程:
URIuri = new URIBuilder()
.setScheme("http")
.setHost("www.google.com")
.setPath("/search")
.setParameter("q","httpclient")
.setParameter("btnG","Google Search")
.setParameter("aq","f")
.setParameter("oq","")
.build();
HttpGethttpget = new HttpGet(uri);
System.out.println(httpget.getURI());
输出:http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
HTTP应答是指服务器收到或解释了客户端发送的信息后返回的消息。这个消息的首行包括:状态码、协议版本和相关的文本短语。
HttpRespOnseresponse= 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.1200 OK
HTTP消息包含一些消息头的描述属性,如内容长度、内容类型等等。HttpClient提供了关于头的恢复、添加、移除和枚举方法。
HttpRespOnseresponse= 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\"");
Headerh1 = response.getFirstHeader("Set-COOKIE");
System.out.println(h1);
Headerh2 = 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
获取所有头的最高效的方法是使用HeaderIterator接口:
HttpRespOnseresponse= 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\"");
HeaderIteratorit = 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消息的便利方法:
HttpRespOnseresponse= 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\"");
HeaderElementIteratorit = new BasicHeaderElementIterator(
response.headerIterator("Set-COOKIE"));
while(it.hasNext()) {
HeaderElementelem = it.nextElement();
System.out.println(elem.getName()+ " = " + elem.getValue());
NameValuePair[]params = elem.getParameters();
for(int i = 0; i) {
System.out.println("" + params[i]);
}
}
HTTP消息可以携带一个与请求或应答关联的内容实体。实体可被选择性地包含在请求和应答里。“请求”是用实体提交上去的,同时实体会附上请求。HTTP规格定义了两种附上实体的请求方法:POST和PUT。“应答”通常会被希望附上一个内容实体。这是一些异常应答:HEAD
method和204No Content
,304 Not Modified
,205Reset Content
应答.
streamed(流):是指一个stream里收到内容,或在飞速写入时产生。它包含了实体的类型,可从HTTP应答里接收到。streamed实体通常是不能重复的。
self-contained(自控制):内容在内存里或可从连接或其他实体里获得。self-contained通常是可重复的。这种实体类型被广泛用在“HTTP请求附上实体”里。
wrapping(交换):内容从另一个实体获得。
对于连接管理来说区分HTTP应答输出流的内容十分重要。对于由应用创建并用HttpClient发送的请求实体,streamed和self-contained间的区分一点也不重要。建议考虑使用不可重复的实体如streamed,那些可重复的就用self-contained。
*译者:这段翻译不太好,看不懂没关系,请继续往下看。
一个实体可以repeatable(重复使用)意味着它的内容可以读取多次。这只有selfcontained实体才能做到(如ByteArrayEntity或者StringEntity)。
实体可以代理二进制和字符内容,支持字符编码(tosupport the latter, ie. character content)。
以下情况都会创建一个实体:
1)执行一个被附上内容的请求;
2)请求成功同时将应答文本发送回客户端。
从实体读取内容有2种方法:
1)使用HttpEntity#getContent()方法来检索输入流,返回java.io.InputStream。
2)为HttpEntity#writeTo(OutputStream)方法提供一个输出流,当所有内容被写入指定的流后才会返回一次。
当实体随着一个输入消息被接收到,HttpEntity#getContentType()和HttpEntity#getContentLength()这两个方法可以用来读公共的元数据如:Content-Type头和Content-Length头(如何它们可获得)。Content-Type头可以为文本类型(如text/plain或text/html)包含字符编码,而HttpEntity#getContentEncoding()方法是用来读取这个信息的。如果头不能使用,将返回-1长度,同时内容类型为NULL。如果Content-Type头可获得,将返回头对象。
当创建一个输出消息时,元数据必需由实体创造者提供。
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
importantmessage
17
为了确保系统资源的正确释放,必需要至少关闭以下两种情况的其中之一:
1)与实体关联的内容流;
2)自身的应答。
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();
}
关闭stream和关闭response的区别是,是否框架会由于消耗实体内容尝试保持底层连接,而后者会立即关闭和丢弃连接。
请注意HttpEntity#writeTo(OutputStream)方法也需要保证实体被完全写出后能正确地释放系统资源。如果这个方法是通过调用HttpEntity#getContent()来获得java.io.InputStream实例的,最后也是要关闭的。
使用流实体时,可以使用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();
intbyteOne = instream.read();
intbyteTwo = instream.read();
//Do not need the rest不需要等待
}
}finally {
response.close();
}
这个连接不能被重新使用,不过所有等级的资源会因为被正确分配而被保持。
推荐使用HttpEntity#getContent()或HttpEntity#writeTo(OutputStream)方法来消费实体的内容。HttpClient也会伴随EntityUtils类发生,EntityUtils提供了几个能轻松在实体里读内容或信息的静态方法。你可以通过使用这个类的方法来将整个内容体恢复为string/ byte array(字符串或字节阵列)。然而,强烈反对使用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) {
longlen = entity.getContentLength();
if(len != -1 && len <2048) {
System.out.println(EntityUtils.toString(entity));
}else {
//Stream content out
}
}
}finally {
response.close();
}
在某些情况,可能会需要多次读实体。既然这样,实体内容必需要保存在内存或磁盘里。最简单的方法是使用BufferedHttpEntity类将源实体包装起来。这样就能从内存缓冲区里读源实体的内容了。在所有其它的实体包装方式里都会有一个源。
CloseableHttpResponse respOnse= <...>
HttpEntity entity = response.getEntity();
if(entity != null) {
entity= new BufferedHttpEntity(entity);
}
HttpClient提供了几个类通过HTTP连接来高效输出’流’的内容。这些类的实例可以被实体联系上,并附上请求(如POST和PUT),从而为了在将实体内容附进输出的HTTP申请。HttpClient为大多数的普通数据容器如string,byte array, input stream,
和 file: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。
许多应用需要模拟HTML框架的过程,比如,为了从应用里登陆或提交输入数据。为帮助这个工作过程,HttpClient提供了实体类UrlEncodedFormEntity。
Listformparams = new ArrayList ();
formparams.add(newBasicNameValuePair("param1", "value1"));
formparams.add(newBasicNameValuePair("param2", "value2"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
UrlEncodedFormEntity实例使用这样调用URL编码方式来编码参数,并产生如下内容:param1=value1¶m2=value2
通常推荐让HttpClient选择最合适的基于HTTP消息特性的传输编码。这是可能的,然而,可以通过设置HttpEntity#setChunked()为true来通知HttpClient首先使用分块编码。请注意,HttpClient用这个flag仅作为一个暗示。当使用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);
最简单和最方便的操纵应答方法是使用ResponseHandler接口,它包括handleResponse(HttpResponseresponse)方法。这个方法完全地解除了用户对连接管理的麻烦。当使用ResponseHandler时,HttpClient会自动地去关心保证资源释放(连接管理者不顾后果地连接返回不管是否请求成功或引起异常)。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
ResponseHandlerrh = new ResponseHandler () {
@Override
public JsonObject handleResponse(
final HttpResponse response) throws IOException {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if(statusLine.getStatusCode() >= 300) {
thrownew HttpResponseException(
statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if(entity == null) {
thrownew ClientProtocolException("Response contains no content");
}
Gsongson = 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);
}
};
MyJsonObjectmyjson = client.execute(httpget, rh);
HttpClient接口为HTTP请求实行描绘了最基本了协议。它推行:
1)不受限制;
2)详细的请求执行过程;
3)指定连接管理员、正确的管理、鉴定和举起独特的执行情况。
这会将会更容易用额外的功能(如应答内容缓存)来装饰接口。
通常HttpClient执行情况担当许多特别意图处理器的外表或策略接口执行情况答应,为了对HTTP协议(如更改、鉴定处理、做关于连接持续连接决定和保持连接)的特别方面的进行处理。这能让使用者有选择性地替代那些定制的、应用程序明确的默认执行情况。
ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(
HttpResponse response,
HttpContext context) {
lOngkeepAlive= super.getKeepAliveDuration(response, context);
if(keepAlive == -1) {
//Keep connections alive 5 seconds if a keep-alive value
//has not be explicitly set by the server
keepAlive= 5000;
}
return keepAlive;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setKeepAliveStrategy(keepAliveStrat)
.build();
推荐在多请求执行的情况下重复使用同一个实例。
当一个CloseableHttpClient实例不再需要和即将离开管理员关联文件范围就必须使用CloseableHttpClient#close()方法来完全关闭。
CloseableHttpClienthttpclient = HttpClients.createDefault();
try{
<...>
}finally {
httpclient.close();
}
最初HTTP是被设计成无状态的,以应答-请求为导向的协议。然而,真实情况是应用往往需要通过几个逻辑上相关的请求-应答交换来确认状态信息。为了让应用保持一个处理中的状态,HttpClient允许HTTP请求被执行在一个特别的上下文里,被称为HTTP上下文。如果同样的上下文在连续的请求之间被复用,多个逻辑关系请求会加入逻辑session。HTTP上下文功能类似于java.util.Map
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
对象代表一个在请求执行过程中被收到的所有的
重定向位置集合。
可以使用HttpClientContext
适配器类简化与上下文状态的互动。
HttpContext cOntext= <...> //译者提醒:可这样创建HttpContext cOntext= new BasicHttpContext();
HttpClientContext clientCOntext= HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();
多个请求序列代表一个
logicallyrelated session 与
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();
}
异常处理
HttpClient可以抛出两种类型异常。
1)java.io.IOException:I/O失败时抛出,如:socket超时或重置socket;
2)HttpException:向HTTP发信号失败,如:违反HTTP协议。
I/O错误通常都要注重非致命性和可恢复性,然而HTTP协议错误要注意它是致命性并不能从错误里恢复。
要重点知道的是,HTTP协议并不是对于所有类型应用都适用的。HTTP是一个以请求/应答为导向的简单的协议。它起初被设计成支持静态或动态生成内容检索。它从没被刻意去支持事务操作。例如,HTTP服务器会注重它的协议处理部分,如成功接收并处理请求、生成应答和给客户端回送状态码。服务器不会回滚事务,即使客户端由于读超时、请求取消或系统崩溃而接受应答失败。如果客户端决定再尝试同样的请求,服务器必然会结束执行同样的事务。有时候,这可能会导致应用数据腐化或应用状态不一致。
既然HTTP没有被设计成支持事务处理,但它仍可以作为传输协议,为应用的关键任务提供确定的对话状态。为了保证HTTP传输层安全,系统必需保证HTTP方法在应用层上的幂等性。
HTTP/1.1这样定义幂等方法:方法可以也可以有幂等性,在N>0次请求和一次请求的副作用(除非错误或过期)一样。
就是说,应用应当保证愿意去处理多次执行同样方法的所引发的后果。这是可以实现的,例如,通过提供唯一的传输ID和使用其它手段避免执行同样的逻辑操作。
请注意这个问题不是HttpClient独有的。基于应用的浏览器为了与非幂等的HTTP方法相协调,限制了完全一样的问题。
HttpCient假如非实体装入方法如GET和HEAD将是幂等的,实体装入方法如POST和PUT就不是。
通过默认的HttpClient尝试从I/O异常里自动恢复。默认的自动恢复机制会被即将知道的少许异常所限制:
*HttpClient不会尝试从逻辑或HTTP协议错误(那些采自HttpException类)里恢复。
*HttpClient会自动重新尝试这些将成为幂等的假定的方法。
*HttpClient会自动重新尝试,当HTTP请求还在传输给服务器(请求没有完全传输给服务器)时,并随着传输异常而失败的方法。
为了使能自定义的异常恢复机构,你应该提供一个HttpRequestRetryHandler接口的实现。
HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
public boolean retryRequest(
IOException exception,
int executionCount,
HttpContext context) {
if(executionCount >= 5) {
//Do not retry if over max retry count
return false;
}
if(exception instanceof InterruptedIOException) {
//Timeout
return false;
}
if(exception instanceof UnknownHostException) {
//Unknown host
return false;
}
if(exception instanceof ConnectTimeoutException) {
//Connection refused
return false;
}
if(exception instanceof SSLException) {
//SSL handshake exception
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if(idempotent) {
/ /Retry if the request is considered idempotent
return true;
}
return false;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setRetryHandler(myRetryHandler)
.build();
在某些情况HTTP会请求执行失败:服务器端在预期的时间内超负载或当前太多客户端请求。在这样的情况下,可能需要停止请求,在一个I/O操作下贸然地解除执行的线程阻塞。HttpClient可以调用HttpUriRequest#abort()方法中止在何意的执行状态,以保证HTTP请求继续执行。这个方法是线程安全的并可以被任意的线程调用。以下情况里执行线程可中断一个HTTP请求:1)当前阻塞在I/O操作内;2)抛出InterruptedIOException异常而被担保去除阻塞。
HTTP协议拦截机是HTTP协议的一套具体的实现方法。通常协议拦截机被希望去对一个(或一群)明确的输入消息标头(或标头关系群)或用一个(或一群)明确的输出消息标头(或标头关系群)起作用。协议拦截机也可以操作被附上内容实体的消息——传输内容的压缩/解压缩就是一个好例子。通常,这要像“油漆工”的一样,通过封装实体类来装饰源实体。几个协议拦截机可以被结合形成一个逻辑单元。
协议拦截机可以通过分享信息来协作——如过程状态——通过HTTP执行上下文。协议拦截机可以使用HTTP上下文来为一个请求或几个连续的请求存储一个过程状态
HttpClient会自动地操作所有重定向,除了那些被HTTP规范明确禁止用户干预的请求。在HTTP规范里必需将状态码为303的POST和PUT请求重定向为GET请求。你可以使用一个自定义的重定向策略来放宽HTTP规范对POST方法的自动重定向限制。
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
HttpClient常常在执行过程中必须去重写请求消息。默认的HTTP/1.0和HTTP/1.1普遍都会使用关系请求URI。使用源请求和上下文可以建立最终被解释的绝对HTTP位置。URIUtils#resolve
可以被用来建立解释绝对
URI
并形成最终的请求。
这个方法包含来自重定向请求或源请求的最后片段标识。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpget = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response = httpclient.execute(httpget, context);
try {
HttpHost target = context.getTargetHost();
ListredirectLocatiOns= context.getRedirectLocations();
URI location = URIUtils.resolve(httpget.getURI(), target, redirectLocations);
System.out.println("Final HTTP location: " + location.toASCIIString());
// Expected to be an absolute URI
} finally {
response.close();
}