Http协议应该是互联网中最重要的协议。持续增长的web服务、可联网的家用电器等都在继承并拓展着Http协议,向着浏览器之外的方向发展。
虽然jdk中的java.net包中提供了一些基本的方法,通过http协议来访问网络资源,但是大多数场景下,它都不够灵活和强大。HttpClient致力于填补这个空白,它可以提供有效的、最新的、功能丰富的包来实现http客户端。
为了拓展,HttpClient即支持基本的http协议,还支持http-aware客户端程序,如web浏览器,Webservice客户端,以及利用or拓展http协议的分布式系统。
1、HttpClient的范围/特性
2、HttpClient不能做的事情
HttpClient最基本的功能就是执行Http方法。一个Http方法的执行涉及到一个或者多个Http请求/Http响应的交互,通常这个过程都会自动被HttpClient处理,对用户透明。用户只需要提供Http请求对象,HttpClient就会将http请求发送给目标服务器,并且接收服务器的响应,如果http请求执行不成功,httpclient就会抛出异样。
下面是个很简单的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(可选)和片段信息(可选)。
HttpClient提供URIBuilder
工具类来简化URIs的创建和修改过程。
上述代码会在控制台输出:
服务器收到客户端的http请求后,就会对其进行解析,然后把响应发给客户端,这个响应就是HTTP response.HTTP响应第一行是协议版本,之后是数字状态码和相关联的文本段。
一个Http消息可以包含一系列的消息头,用来对http消息进行描述,比如消息长度,消息类型等等。HttpClient提供了方法来获取、添加、移除、枚举消息头。
最有效的获取指定类型的消息头的方法还是使用HeaderIterator
接口。
上述代码会在控制台输出:
HeaderIterator也提供非常便捷的方式,将Http消息解析成单独的消息头元素。
上述代码会在控制台输出:
Http消息可以携带http实体,这个http实体既可以是http请求,也可以是http响应的。Http实体,可以在某些http请求或者响应中发现,但不是必须的。Http规范中定义了两种包含请求的方法:POST和PUT。HTTP响应一般会包含一个内容实体。当然这条规则也有异常情况,如Head方法的响应,204没有内容,304没有修改或者205内容资源重置。
HttpClient根据来源的不同,划分了三种不同的Http实体内容。
当从Http响应中读取内容时,上面的三种区分对于连接管理器来说是非常重要的。对于由应用程序创建而且只使用HttpClient发送的请求实体,streamed和self-contained两种类型的不同就不那么重要了。这种情况下,建议考虑如streamed流式这种不能重复的实体,和可以重复的self-contained自我包含式实体。
一个实体是可重复的,也就是说它的包含的内容可以被多次读取。这种多次读取只有self contained(自包含)的实体能做到(比如ByteArrayEntity
或者StringEntity
)。
由于一个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信息。
为了确保系统资源被正确地释放,我们要么管理Http实体的内容流、要么关闭Http响应。
请注意HttpEntity
的writeTo(OutputStream)
方法,当Http实体被写入到OutputStream后,也要确保释放系统资源。如果这个方法内调用了HttpEntity
的getContent()
方法,那么它会有一个java.io.InpputStream
的实例,我们需要在finally中关闭这个流。
但是也有这样的情况,我们只需要获取Http响应内容的一小部分,而获取整个内容并、实现连接的可重复性代价太大,这时我们可以通过关闭响应的方式来关闭内容输入、输出流。
HttpClient推荐使用HttpEntity
的getConent()
方法或者HttpEntity
的writeTo(OutputStream)
方法来消耗掉Http实体内容。HttpClient也提供了EntityUtils
这个类,这个类提供一些静态方法可以更容易地读取Http实体的内容和信息。和以java.io.InputStream
流读取内容的方式相比,EntityUtils提供的方法可以以字符串或者字节数组的形式读取Http实体。但是,强烈不推荐使用EntityUtils
这个类,除非目标服务器发出的响应是可信任的,并且http响应实体的长度不会过大。
有些情况下,我们希望可以重复读取Http实体的内容。这就需要把Http实体内容缓存在内存或者磁盘上。最简单的方法就是把Http Entity转化成BufferedHttpEntity
,这样就把原Http实体的内容缓冲到了内存中。后面我们就可以重复读取BufferedHttpEntity中的内容。
HttpClient提供了一些类,这些类可以通过http连接高效地输出Http实体内容。HttpClient提供的这几个类涵盖的常见的数据类型,如String,byte数组,输入流,和文件类型:StringEntity
,ByteArrayEntity
,InputStreamEntity
,FileEntity
。
InputStreamEntity
只能从下层的数据流中读取一次,所以它是不能重复的。推荐,通过继承
HttpEntity
这个自包含的类来自定义HttpEntity类,而不是直接使用
InputStreamEntity
这个类。
FileEntity
就是一个很好的起点(FileEntity就是继承的HttpEntity)。
很多应用程序需要模拟提交Html表单的过程,举个例子,登陆一个网站或者将输入内容提交给服务器。HttpClient提供了UrlEncodedFormEntity
这个类来帮助实现这一过程。
UrlEncodedFormEntity
实例会使用所谓的Url编码的方式对我们的参数进行编码,产生的结果如下:
一般来说,推荐让HttpClient自己根据Http消息传递的特征来选择最合适的传输编码。当然,如果非要手动控制也是可以的,可以通过设置HttpEntity
的setChunked()
为true。请注意:HttpClient仅会将这个参数看成是一个建议。如果Http的版本(如http 1.0)不支持内容分块,那么这个参数就会被忽略。
最简单也是最方便的处理http响应的方法就是使用ResponseHandler
接口,这个接口中有handleResponse(HttpResponse response)
方法。使用这个方法,用户完全不用关心http连接管理器。当使用ResponseHandler
时,HttpClient会自动地将Http连接释放给Http管理器,即使http请求失败了或者抛出了异常。
对于Http请求执行过程来说,HttpClient
的接口有着必不可少的作用。HttpClient
接口没有对Http请求的过程做特别的限制和详细的规定,连接管理、状态管理、授权信息和重定向处理这些功能都单独实现。这样用户就可以更简单地拓展接口的功能(比如缓存响应内容)。
一般说来,HttpClient实际上就是一系列特殊的handler或者说策略接口的实现,这些handler(测试接口)负责着处理Http协议的某一方面,比如重定向、认证处理、有关连接持久性和keep alive持续时间的决策。这样就允许用户使用自定义的参数来代替默认配置,实现个性化的功能。
HttpClient
已经实现了线程安全。所以希望用户在实例化HttpClient时,也要支持为多个请求使用。
当一个CloseableHttpClient
的实例不再被使用,并且它的作用范围即将失效,和它相关的连接必须被关闭,关闭方法可以调用CloseableHttpClient
的close()
方法。
最初,Http被设计成一种无状态的、面向请求-响应的协议。然而,在实际使用中,我们希望能够在一些逻辑相关的请求-响应中,保持状态信息。为了使应用程序可以保持Http的持续状态,HttpClient允许http连接在特定的Http上下文中执行。如果在持续的http请求中使用了同样的上下文,那么这些请求就可以被分配到一个逻辑会话中。HTTP上下文就和一个java.util.Map
功能类似。它实际上就是一个任意命名的值的集合。应用程序可以在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
对象,表示Http响应中的所有重定向地址我们可以使用HttpClientContext
这个适配器来简化和上下文交互的过程。
同一个逻辑会话中的多个Http请求,应该使用相同的Http上下文来执行,这样就可以自动地在http请求中传递会话上下文和状态信息。
在下面的例子中,我们在开头设置的参数,会被保存在上下文中,并且会应用到后续的http请求中。
HttpClient会被抛出两种类型的异常,一种是java.io.IOException
,当遇到I/O异常时抛出(socket超时,或者socket被重置);另一种是HttpException
,表示Http失败,如Http协议使用不正确。通常认为,I/O错误时不致命、可修复的,而Http协议错误是致命了,不能自动修复的错误。
Http协议不能满足所有类型的应用场景,我们需要知道这点。Http是个简单的面向协议的请求/响应的协议,当初它被设计用来支持静态或者动态生成的内容检索,之前从来没有人想过让它支持事务性操作。例如,Http服务器成功接收、处理请求后,生成响应消息,并且把状态码发送给客户端,这个过程是Http协议应该保证的。但是,如果客户端由于读取超时、取消请求或者系统崩溃导致接收响应失败,服务器不会回滚这一事务。如果客户端重新发送这个请求,服务器就会重复的解析、执行这个事务。在一些情况下,这会导致应用程序的数据损坏和应用程序的状态不一致。
即使Http当初设计是不支持事务操作,但是它仍旧可以作为传输协议为某些关键程序提供服务。为了保证Http传输层的安全性,系统必须保证应用层上的http方法的幂等性。
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默认把非实体方法get
、head
方法看做幂等方法,把实体方法post
、put
方法看做非幂等方法。
默认情况下,HttpClient会尝试自动修复I/O异常。这种自动修复仅限于修复几个公认安全的异常。
如果要自定义异常处理机制,我们需要实现HttpRequestRetryHandler
接口。
有时候由于目标服务器负载过高或者客户端目前有太多请求积压,http请求不能在指定时间内执行完毕。这时候终止这个请求,释放阻塞I/O的进程,就显得很必要。通过HttpClient执行的Http请求,在任何状态下都能通过调用HttpUriRequest
的abort()
方法来终止。这个方法是线程安全的,并且能在任何线程中调用。当Http请求被终止了,本线程(即使现在正在阻塞I/O)也会通过抛出一个InterruptedIOException
异常,来释放资源。
HTTP协议拦截器是一种实现一个特定的方面的HTTP协议的代码程序。通常情况下,协议拦截器会将一个或多个头消息加入到接受或者发送的消息中。协议拦截器也可以操作消息的内容实体—消息内容的压缩/解压缩就是个很好的例子。通常,这是通过使用“装饰”开发模式,一个包装实体类用于装饰原来的实体来实现。一个拦截器可以合并,形成一个逻辑单元。
协议拦截器可以通过共享信息协作——比如处理状态——通过HTTP执行上下文。协议拦截器可以使用Http上下文存储一个或者多个连续请求的处理状态。
通常,只要拦截器不依赖于一个特定状态的http上下文,那么拦截执行的顺序就无所谓。如果协议拦截器有相互依赖关系,必须以特定的顺序执行,那么它们应该按照特定的顺序加入到协议处理器中。
协议处理器必须是线程安全的。类似于servlets,协议拦截器不应该使用变量实体,除非访问这些变量是同步的(线程安全的)。
下面是个例子,讲述了本地的上下文时如何在连续请求中记录处理状态的:
上面代码在发送http请求时,会自动添加Count这个header,可以使用wireshark抓包查看。
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方法重定向的限制。
HttpClient在请求执行过程中,经常需要重写请求的消息。 HTTP/1.0和HTTP/1.1都默认使用相对的uri路径。同样,原始的请求可能会被一次或者多次的重定向。最终结对路径的解释可以使用最初的请求和上下文。URIUtils
类的resolve
方法可以用于将拦截的绝对路径构建成最终的请求。这个方法包含了最后一个分片标识符或者原始请求。
两个主机建立连接的过程是很复杂的一个过程,涉及到多个数据包的交换,并且也很耗时间。Http连接需要的三次握手开销很大,这一开销对于比较小的http消息来说更大。但是如果我们直接使用已经建立好的http连接,这样花费就比较小,吞吐率更大。
HTTP/1.1默认就支持Http连接复用。兼容HTTP/1.0的终端也可以通过声明来保持连接,实现连接复用。HTTP代理也可以在一定时间内保持连接不释放,方便后续向这个主机发送http请求。这种保持连接不释放的情况实际上是建立的持久连接。HttpClient也支持持久连接。
HttpClient既可以直接、又可以通过多个中转路由(hops)和目标服务器建立连接。HttpClient把路由分为三种plain(明文 ),tunneled(隧道)和layered(分层)。隧道连接中使用的多个中间代理被称作代理链。
客户端直接连接到目标主机或者只通过了一个中间代理,这种就是Plain路由。客户端通过第一个代理建立连接,通过代理链tunnelling,这种情况就是Tunneled路由。不通过中间代理的路由不可能时tunneled路由。客户端在一个已经存在的连接上进行协议分层,这样建立起来的路由就是layered路由。协议只能在隧道—>目标主机,或者直接连接(没有代理),这两种链路上进行分层。
RouteInfo
接口包含了数据包发送到目标主机过程中,经过的路由信息。HttpRoute
类继承了RouteInfo
接口,是RouteInfo
的具体实现,这个类是不允许修改的。HttpTracker
类也实现了RouteInfo
接口,它是可变的,HttpClient会在内部使用这个类来探测到目标主机的剩余路由。HttpRouteDirector
是个辅助类,可以帮助计算数据包的下一步路由信息。这个类也是在HttpClient内部使用的。
HttpRoutePlanner
接口可以用来表示基于http上下文情况下,客户端到服务器的路由计算策略。HttpClient有两个HttpRoutePlanner
的实现类。SystemDefaultRoutePlanner
这个类基于java.net.ProxySelector
,它默认使用jvm的代理配置信息,这个配置信息一般来自系统配置或者浏览器配置。DefaultProxyRoutePlanner
这个类既不使用java本身的配置,也不使用系统或者浏览器的配置。它通常通过默认代理来计算路由信息。
为了防止通过Http消息传递的信息不被未授权的第三方获取、截获,Http可以使用SSL/TLS协议来保证http传输安全,这个协议是当前使用最广的。当然也可以使用其他的加密技术。但是通常情况下,Http信息会在加密的SSL/TLS连接上进行传输。
Http连接是复杂,有状态的,线程不安全的对象,所以它必须被妥善管理。一个Http连接在同一时间只能被一个线程访问。HttpClient使用一个叫做Http连接管理器的特殊实体类来管理Http连接,这个实体类要实现HttpClientConnectionManager
接口。Http连接管理器在新建http连接时,作为工厂类;管理持久http连接的生命周期;同步持久连接(确保线程安全,即一个http连接同一时间只能被一个线程访问)。Http连接管理器和ManagedHttpClientConnection
的实例类一起发挥作用,ManagedHttpClientConnection
实体类可以看做http连接的一个代理服务器,管理着I/O操作。如果一个Http连接被释放或者被它的消费者明确表示要关闭,那么底层的连接就会和它的代理进行分离,并且该连接会被交还给连接管理器。这是,即使服务消费者仍然持有代理的引用,它也不能再执行I/O操作,或者更改Http连接的状态。
下面的代码展示了如何从连接管理器中取得一个http连接:
如果要终止连接,可以调用ConnectionRequest
的cancel()
方法。这个方法会解锁被ConnectionRequest
类get()
方法阻塞的线程。
BasicHttpClientConnectionManager
是个简单的连接管理器,它一次只能管理一个连接。尽管这个类是线程安全的,它在同一时间也只能被一个线程使用。BasicHttpClientConnectionManager
会尽量重用旧的连接来发送后续的请求,并且使用相同的路由。如果后续请求的路由和旧连接中的路由不匹配,BasicHttpClientConnectionManager
就会关闭当前连接,使用请求中的路由重新建立连接。如果当前的连接正在被占用,会抛出java.lang.IllegalStateException
异常。
相对BasicHttpClientConnectionManager
来说,PoolingHttpClientConnectionManager
是个更复杂的类,它管理着连接池,可以同时为很多线程提供http连接请求。Connections are pooled on a per route basis.当请求一个新的连接时,如果连接池有有可用的持久连接,连接管理器就会使用其中的一个,而不是再创建一个新的连接。
PoolingHttpClientConnectionManager
维护的连接数在每个路由基础和总数上都有限制。默认,每个路由基础上的连接不超过2个,总连接数不能超过20。在实际应用中,这个限制可能会太小了,尤其是当服务器也使用Http协议时。
下面的例子演示了如果调整连接池的参数:
当一个HttpClient的实例不在使用,或者已经脱离它的作用范围,我们需要关掉它的连接管理器,来关闭掉所有的连接,释放掉这些连接占用的系统资源。
当使用了请求连接池管理器(比如PoolingClientConnectionManager
)后,HttpClient就可以同时执行多个线程的请求了。
PoolingClientConnectionManager
会根据它的配置来分配请求连接。如果连接池中的所有连接都被占用了,那么后续的请求就会被阻塞,直到有连接被释放回连接池中。为了防止永远阻塞的情况发生,我们可以把http.conn-manager.timeout
的值设置成一个整数。如果在超时时间内,没有可用连接,就会抛出ConnectionPoolTimeoutException
异常。
即使HttpClient的实例是线程安全的,可以被多个线程共享访问,但是仍旧推荐每个线程都要有自己专用实例的HttpContext。
下面是GetThread类的定义:
经典阻塞I/O模型的一个主要缺点就是只有当组侧I/O时,socket才能对I/O事件做出反应。当连接被管理器收回后,这个连接仍然存活,但是却无法监控socket的状态,也无法对I/O事件做出反馈。如果连接被服务器端关闭了,客户端监测不到连接的状态变化(也就无法根据连接状态的变化,关闭本地的socket)。
HttpClient为了缓解这一问题造成的影响,会在使用某个连接前,监测这个连接是否已经过时,如果服务器端关闭了连接,那么连接就会失效。这种过时检查并不是100%有效,并且会给每个请求增加10到30毫秒额外开销。唯一一个可行的,且does not involve a one thread per socket model for idle connections的解决办法,是建立一个监控线程,来专门回收由于长时间不活动而被判定为失效的连接。这个监控线程可以周期性的调用ClientConnectionManager
类的closeExpiredConnections()
方法来关闭过期的连接,回收连接池中被关闭的连接。它也可以选择性的调用ClientConnectionManager
类的closeIdleConnections()
方法来关闭一段时间内不活动的连接。
Http规范没有规定一个持久连接应该保持存活多久。有些Http服务器使用非标准的Keep-Alive
头消息和客户端进行交互,服务器端会保持数秒时间内保持连接。HttpClient也会利用这个头消息。如果服务器返回的响应中没有包含Keep-Alive
头消息,HttpClient会认为这个连接可以永远保持。然而,很多服务器都会在不通知客户端的情况下,关闭一定时间内不活动的连接,来节省服务器资源。在某些情况下默认的策略显得太乐观,我们可能需要自定义连接存活策略。
Http连接使用java.net.Socket
类来传输数据。这依赖于ConnectionSocketFactory
接口来创建、初始化和连接socket。这样也就允许HttpClient的用户在代码运行时,指定socket初始化的代码。PlainConnectionSocketFactory
是默认的创建、初始化明文socket(不加密)的工厂类。
创建socket和使用socket连接到目标主机这两个过程是分离的,所以我们可以在连接发生阻塞时,关闭socket连接。
LayeredConnectionSocketFactory
是ConnectionSocketFactory
的拓展接口。分层socket工厂类可以在明文socket的基础上创建socket连接。分层socket主要用于在代理服务器之间创建安全socket。HttpClient使用SSLSocketFactory
这个类实现安全socket,SSLSocketFactory
实现了SSL/TLS分层。请知晓,HttpClient没有自定义任何加密算法。它完全依赖于Java加密标准(JCE)和安全套接字(JSEE)拓展。
自定义的socket工厂类可以和指定的协议(Http、Https)联系起来,用来创建自定义的连接管理器。
HttpClient使用SSLSocketFactory
来创建ssl连接。SSLSocketFactory
允许用户高度定制。它可以接受javax.net.ssl.SSLContext
这个类的实例作为参数,来创建自定义的ssl连接。
除了信任验证和在ssl/tls协议层上进行客户端认证,HttpClient一旦建立起连接,就可以选择性验证目标域名和存储在X.509证书中的域名是否一致。这种验证可以为服务器信任提供额外的保障。X509HostnameVerifier
接口代表主机名验证的策略。在HttpClient中,X509HostnameVerifier
有三个实现类。重要提示:主机名有效性验证不应该和ssl信任验证混为一谈。
StrictHostnameVerifier
: 严格的主机名验证方法和java 1.4,1.5,1.6验证方法相同。和IE6的方式也大致相同。这种验证方式符合RFC 2818通配符。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.BrowserCompatHostnameVerifier
: 这种验证主机名的方法,和Curl及firefox一致。The hostname must match either the first CN, or any of the subject-alts. A wildcard can occur in the CN, and in any of the subject-alts.StrictHostnameVerifier
和BrowserCompatHostnameVerifier
方式唯一不同的地方就是,带有通配符的域名(比如*.yeetrack.com),BrowserCompatHostnameVerifier
方式在匹配时会匹配所有的的子域名,包括 a.b.yeetrack.com .AllowAllHostnameVerifier
: 这种方式不对主机名进行验证,验证功能被关闭,是个空操作,所以它不会抛出javax.net.ssl.SSLException
异常。HttpClient默认使用BrowserCompatHostnameVerifier
的验证方式。如果需要,我们可以手动执行验证方式。
尽管,HttpClient支持复杂的路由方案和代理链,它同样也支持直接连接或者只通过一跳的连接。
使用代理服务器最简单的方式就是,指定一个默认的proxy参数。
我们也可以让HttpClient去使用jre的代理服务器。
又或者,我们也可以手动配置RoutePlanner
,这样就可以完全控制Http路由的过程。
最初,Http被设计成一个无状态的,面向请求/响应的协议,所以它不能在逻辑相关的http请求/响应中保持状态会话。由于越来越多的系统使用http协议,其中包括http从来没有想支持的系统,比如电子商务系统。因此,http支持状态管理就很必要了。
当时的web客户端和服务器软件领先者,网景(netscape)公司,最先在他们的产品中支持http状态管理,并且制定了一些专有规范。后来,网景通过发规范草案,规范了这一机制。这些努力促成 RFC standard track制定了标准的规范。但是,现在多数的应用的状态管理机制都在使用网景公司的规范,而网景的规范和官方规定是不兼容的。因此所有的浏览器开发这都被迫兼容这两种协议,从而导致协议的不统一。
所谓的Http cookie就是一个token或者很短的报文信息,http代理和服务器可以通过cookie来维持会话状态。网景的工程师把它们称作“magic cookie”。
HttpClient使用Cookie
接口来代表cookie。简单说来,cookie就是一个键值对。一般,cookie也会包含版本号、域名、路径和cookie有效期。
SetCookie
接口可以代表服务器发给http代理的一个set-cookie响应头,在浏览器中,这个set-cookie响应头可以写入cookie,以便保持会话状态。SetCookie2
接口对SetCookie
接口进行了拓展,添加了Set-Cookie2
方法。
ClientCookie
接口继承了Cookie
接口,并进行了功能拓展,比如它可以取出服务器发送过来的原始cookie的值。生成头消息是很重要的,因为只有当cookie被指定为Set-Cookie
或者Set-Cookie2
时,它才需要包括一些特定的属性。
兼容网景的规范,但是不兼容官方规范的cookie,是版本0. 兼容官方规范的版本,将会是版本1。版本1中的Cookie可能和版本0工作机制有差异。
下面的代码,创建了网景版本的Cookie:
下面的代码,创建标准版本的Cookie。注意,标准版本的Cookie必须保留服务器发送过来的Cookie所有属性。
下面的代码,创建了Set-Cookie2
兼容cookie。
CookieSpec
接口代表了Cookie管理规范。Cookie管理规范规定了:
Set-Cookie
和Set-Cookie2
(可选)头消息的规则HttpClient有下面几种CookieSpec
规范:
我们可以在创建Http client的时候指定Cookie测试,如果需要,也可以在执行http请求的时候,进行覆盖指定。
如果我们要自定义Cookie测试,就要自己实现CookieSpec
接口,然后创建一个CookieSpecProvider
接口来新建、初始化自定义CookieSpec
接口,最后把CookieSpecProvider
注册到HttpClient中。一旦我们注册了自定义策略,就可以像其他标准策略一样使用了。
HttpClient可以使用任何存储方式的cookie store,只要这个cookie store实现了CookieStore
接口。默认的CookieStore通过java.util.ArrayList
简单实现了BasicCookieStore
。存在在BasicCookieStore
中的Cookie,当载体对象被当做垃圾回收掉后,就会丢失。如果必要,用户可以自己实现更为复杂的方式。
在Http请求执行过程中,HttpClient会自动向执行上下文中添加下面的状态管理对象:
Lookup
对象 代表实际的cookie规范registry。在当前上下文中的这个值优先于默认值。CookieSpec
对象 代表实际的Cookie规范。CookieOrigin
对象 代表实际的origin server的详细信息。CookieStore
对象 表示Cookie store。这个属性集中的值会取代默认值。本地的HttpContext
对象可以用来在Http请求执行前,自定义Http状态管理上下文;或者测试http请求执行完毕后上下文的状态。我们也可以在不同的线程中使用不同的执行上下文。我们在http请求层指定的cookie规范集和cookie store会覆盖在http Client层级的默认值。
HttpClient既支持HTTP标准规范定义的认证模式,又支持一些广泛使用的非标准认证模式,比如NTLM和SPNEGO。
任何用户认证的过程,都需要一系列的凭证来确定用户的身份。最简单的用户凭证可以是用户名和密码这种形式。UsernamePasswordCredentials
这个类可以用来表示这种情况,这种凭据包含明文的用户名和密码。
这个类对于HTTP标准规范中定义的认证模式来说已经足够了。
上述代码会在控制台输出:
NTCredentials
是微软的windows系统使用的一种凭据,包含username、password,还包括一系列其他的属性,比如用户所在的域名。在Microsoft Windows的网络环境中,同一个用户可以属于不同的域,所以他也就有不同的凭据。
上述代码输出:
AutoScheme
接口表示一个抽象的面向挑战/响应的认证方案。一个认证方案要支持下面的功能:
请注意:一个认证方案可能是有状态的,因为它可能涉及到一系列的挑战/响应。
HttpClient实现了下面几种AutoScheme
:
凭证providers旨在维护一套用户的凭证,当需要某种特定的凭证时,providers就应该能产生这种凭证。认证的具体内容包括主机名、端口号、realm name和认证方案名。当使用凭据provider的时候,我们可以很模糊的指定主机名、端口号、realm和认证方案,不用写的很精确。因为,凭据provider会根据我们指定的内容,筛选出一个最匹配的方案。
只要我们自定义的凭据provider实现了CredentialsProvider
这个接口,就可以在HttpClient中使用。默认的凭据provider叫做BasicCredentialsProvider
,它使用java.util.HashMap
对CredentialsProvider
进行了简单的实现。
上面代码输出:
HttpClient依赖AuthState
类去跟踪认证过程中的状态的详细信息。在Http请求过程中,HttpClient创建两个AuthState
实例:一个用于目标服务器认证,一个用于代理服务器认证。如果服务器或者代理服务器需要用户的授权信息,AuthScope
、AutoScheme
和认证信息就会被填充到两个AuthScope
实例中。通过对AutoState
的检测,我们可以确定请求的授权类型,确定是否有匹配的AuthScheme
,确定凭据provider根据指定的授权类型是否成功生成了用户的授权信息。
在Http请求执行过程中,HttpClient会向执行上下文中添加下面的授权对象:
Lookup
对象,表示使用的认证方案。这个对象的值可以在本地上下文中进行设置,来覆盖默认值。CredentialsProvider
对象,表示认证方案provider,这个对象的值可以在本地上下文中进行设置,来覆盖默认值。AuthState
对象,表示目标服务器的认证状态,这个对象的值可以在本地上下文中进行设置,来覆盖默认值。AuthState
对象,表示代理服务器的认证状态,这个对象的值可以在本地上下文中进行设置,来覆盖默认值。AuthCache
对象,表示认证数据的缓存,这个对象的值可以在本地上下文中进行设置,来覆盖默认值。我们可以在请求执行前,自定义本地HttpContext
对象来设置需要的http认证上下文;也可以在请求执行后,再检测HttpContext
的状态,来查看授权是否成功。
从版本4.1开始,HttpClient就会自动缓存验证通过的认证信息。但是为了使用这个缓存的认证信息,我们必须在同一个上下文中执行逻辑相关的请求。一旦超出该上下文的作用范围,缓存的认证信息就会失效。
HttpClient默认不支持抢先认证,因为一旦抢先认证被误用或者错用,会导致一系列的安全问题,比如会把用户的认证信息以明文的方式发送给未授权的第三方服务器。因此,需要用户自己根据自己应用的具体环境来评估抢先认证带来的好处和带来的风险。
即使如此,HttpClient还是允许我们通过配置来启用抢先认证,方法是提前填充认证信息缓存到上下文中,这样,以这个上下文执行的方法,就会使用抢先认证。
从版本4.1开始,HttpClient就全面支持NTLMv1、NTLMv2和NTLM2认证。当人我们可以仍旧使用外部的NTLM引擎(比如Samba开发的JCIFS库)作为与Windows互操作性程序的一部分。
相比Basic
和Digest
认证,NTLM认证要明显需要更多的计算开销,性能影响也比较大。这也可能是微软把NTLM协议设计成有状态连接的主要原因之一。也就是说,NTLM连接一旦建立,用户的身份就会在其整个生命周期和它相关联。NTLM连接的状态性使得连接持久性更加复杂,The stateful nature of NTLM connections makes connection persistence more complex, as for the obvious reason persistent NTLM connections may not be re-used by users with a different user identity. HttpClient中标准的连接管理器就可以管理有状态的连接。但是,同一会话中逻辑相关的请求,必须使用相同的执行上下文,这样才能使用用户的身份信息。否则,HttpClient就会结束旧的连接,为了获取被NTLM协议保护的资源,而为每个HTTP请求,创建一个新的Http连接。更新关于Http状态连接的信息,点击此处。
由于NTLM连接是有状态的,一般推荐使用比较轻量级的方法来处罚NTLM认证(如GET、Head方法),然后使用这个已经建立的连接在执行相对重量级的方法,尤其是需要附件请求实体的请求(如POST、PUT请求)。
SPNEGO(Simple and Protected GSSAPI Megotiation Mechanism),当双方均不知道对方能使用/提供什么协议的情况下,可以使用SP认证协议。这种协议在Kerberos认证方案中经常使用。It can wrap other mechanisms, however the current version in HttpClient is designed solely with Kerberos in mind.
SPNEGO认证方案兼容Sun java 1.5及以上版本。但是强烈推荐jdk1.6以上。Sun的JRE提供的类就已经几乎完全可以处理Kerberos和SPNEGO token。这就意味着,需要设置很多的GSS类。SpnegoScheme
是个很简单的类,可以用它来handle marshalling the tokens and 读写正确的头消息。
最好的开始方法就是从示例程序中找到KerberosHttpClient.java
这个文件,尝试让它运行起来。运行过程有可能会出现很多问题,但是如果人品比较高可能会顺利一点。这个文件会提供一些输出,来帮我们调试。
在Windows系统中,应该默认使用用户的登陆凭据;当然我们也可以使用kinit
来覆盖这个凭据,比如$JAVA_HOME\bin\kinit [email protected]
,这在我们测试和调试的时候就显得很有用了。如果想用回Windows默认的登陆凭据,删除kinit创建的缓存文件即可。
确保在krb5.conf文件中列出domain_realms
。这能解决很多不必要的问题。
下面的这份文档是针对Windows系统的,但是很多信息同样适合Unix。
org.ietf.jgss
这个类有很多的配置参数,这些参数大部分都在krb5.conf/krb5.ini
文件中配置。更多的信息,参考此处。
下面是一个基本的login.conf文件,使用于Windows平台的IIS和JBoss Negotiation模块。
系统配置文件java.security.auth.login.config
可以指定login.conf
文件的路径。
login.conf
的内容可能会是下面的样子:
如果没有手动指定,系统会使用默认配置。如果要手动指定,可以在java.security.krb5.conf
中设置系统变量,指定krb5.conf
的路径。krb5.conf
的内容可能是下面的样子:
为了允许Windows使用当前用户的tickets,javax.security.auth.useSubjectCredsOnly
这个系统变量应该设置成false
,并且需要在Windows注册表中添加allowtgtsessionkey
这个项,而且要allow session keys to be sent in the Kerberos Ticket-Granting Ticket.
Windows Server 2003和Windows 2000 SP4,配置如下:
Windows XP SP2 配置如下:
HttpClient从4.2开始支持快速api。快速api仅仅实现了HttpClient的基本功能,它只要用于一些不需要灵活性的简单场景。例如,快速api不需要用户处理连接管理和资源释放。
下面是几个使用快速api的例子:
一般情况下,HttpClient的快速api不用用户处理连接管理和资源释放。但是,这样的话,就必须在内存中缓存这些响应消息。为了避免这一情况,建议使用使用ResponseHandler来处理Http响应。
HttpClient的缓存机制提供一个与HTTP/1.1标准兼容的缓存层 – 相当于Java的浏览器缓存。HttpClient缓存机制的实现遵循责任链(Chain of Responsibility)设计原则,默认的HttpClient是没有缓存的,有缓存机制的HttpClient可以用来临时替代默认的HttpClient,如果开启了缓存,我们的请求结果就会从缓存中获取,而不是从目标服务器中获取。如果在Get请求头中设置了If-Modified-Since
或者If-None-Match
参数,那么HttpClient会自动向服务器校验缓存是否过期。
HTTP/1.1版本的缓存是语义透明的,意思是无论怎样,缓存都不应该修改客户端与服务器之间传输的请求/响应数据包。因此,在existing compliant client-server relationship中使用带有缓存的HttpClient也应该是安全的。虽然缓存是客户端的一部分,但是从Http协议的角度来看,缓存机制是为了兼容透明的缓存代理。
最后,HttpClient缓存也支持RFC 5861规定的Cache-Control拓展(stale-if-error'和
stale-while-revalidate`)。
当开启缓存的HttpClient执行一个Http请求时,会经过下面的步骤:
ByteArrayEntity
的BasicHttpResponse
对象,并将它返回给http请求。否则,HttpClient会向服务器重新校验缓存。HttpClient的缓存机制和RFC-2626文档规定是无条件兼容的。也就是说,只要指定了MUST
,MUST NOT
,SHOULD
或者SHOULD NOT
这些Http缓存规范,HttpClient的缓存层就会按照指定的方式进行缓存。即当我们使用HttpClient的缓存机制时,HttpClient的缓存模块不会产生异常动作。
下面的例子讲述了如何创建一个基本的开启缓存的HttpClient。并且配置了最大缓存1000个Object对象,每个对象最大占用8192字节数据。代码中出现的数据,只是为了做演示,而过不是推荐使用的配置。
有缓存的HttpClient继承了非缓存HttpClient的所有配置项和参数(包括超时时间,连接池大小等配置项)。如果需要对缓存进行具体配置,可以初始化一个CacheConfig
对象来自定义下面的参数:
Cache size
(缓存大小). 如果后台存储支持,我们可以指定缓存的最大条数,和每个缓存中存储的response的最大size。Public/private cacheing
(公用/私有 缓存). 默认情况下,缓存模块会把缓存当做公用的缓存,所以缓存机制不会缓存带有授权头消息或者指定Cache-Control:private
的响应。但是如果缓存只会被一个逻辑上的用户使用(和浏览器饿缓存类似),我们可能希望关闭缓存共享机制。Heuristic caching
(启发式缓存)。即使服务器没有明确设置缓存控制headers信息,每个RFC2616缓存也会存储一定数目的缓存。这个特征在HttpClient中默认是关闭的,如果服务器不设置控制缓存的header信息,但是我们仍然希望对响应进行缓存,就需要在HttpClient中打开这个功能。激活启发式缓存,然后使用默认的刷新时间或者自定义刷新时间。更多启发式缓存的信息,可以参考Http/1.1 RFC文档的13.2.2小节,13.2.4小节。Background validation
(后台校验)。HttpClient的缓存机制支持RFC5861的stale-while-revalidate
指令,它允许一定数目的缓存在后台校验是否过期。我们可能需要调整可以在后台工作的最大和最小的线程数,以及设置线程在回收前最大的空闲时间。当没有足够线程来校验缓存是否过期时,我们可以指定排队队列的大小。默认,HttpClient缓存机制将缓存条目和缓存的response放在本地程序的jvm内存中。这样虽然提供高性能,但是当我们的程序内存有大小限制的时候,这就会变得不太合理。因为缓存的生命中期很短,如果程序重启,缓存就会失效。当前版本的HttpClient使用EhCache和memchached来存储缓存,这样就支持将缓存放到本地磁盘或者其他存储介质上。如果内存、本地磁盘、外地磁盘,都不适合你的应用程序,HttpClient也支持自定义存储介质,只需要实现HttpCacheStorage
接口,然后在创建HttpClient时,使用这个接口的配置。这种情况,缓存会存储在自定义的介质中,但是you will get to reuse all of the logic surrounding HTTP/1.1 compliance and cache handling. 一般来说,可以创建出支持任何键值对指定存储(类似Java Map接口)的HttpCacheStorage
,用于进行原子更新。
最后,通过一些额外的工作,还可以建立起多层次的缓存结构;磁盘中的缓存,远程memcached中的缓存,虚拟内存中的缓存,L1/L2处理器中的缓存等。
在特定条件下,也许需要来定制HTTP报文通过线路传递,越过了可能使用的HTTP参数来处理非标准不兼容行为的方式。比如,对于Web爬虫,它可能需要强制HttpClient接受格式错误的响应头部信息,来抢救报文的内容。
通常插入一个自定义的报文解析器的过程或定制连接实现需要几个步骤:
提供一个自定义LineParser/LineFormatter接口实现。如果需要,实现报文解析/格式化逻辑。
提过一个自定义的 HttpConnectionFactory 实现。替换需要自定义的默认请求/响应解析器,请求/响应格式化器。如果需要,实现不同的报文写入/读取代码。
为了创建新类的连接,提供一个自定义的ClientConnectionOperator接口实现。如果需要,实现不同的套接字初始化代码。
如果它可以从给定的执行上下文中来获得,UserTokenHandler接口的默认实现是使用主类的一个实例来代表HTTP连接的状态对象。UserTokenHandler将会使用基于如NTLM或开启的客户端认证SSL会话认证模式的用户的主连接。如果二者都不可用,那么就不会返回令牌。
FutureRequestExecutionService用HttpRequestFutureTask(继承FutureTask)包装request。这个类允许你取消Task以及保持跟踪各项指标,如request duration。
futureRequestExecutionService的构造方法包括两个参数:httpClient实例和ExecutorService实例。当配置两个参数的时候,您要使用的线程数等于最大连接数是很重要的。当线程比连接多的时候,连接可能会开始超时,因为没有可用的连接。当连接多于线程时,futureRequestExecutionService不会使用所有的连接。
要安排一个请求,只需提供一个HttpUriRequest,HttpContext和ResponseHandler。因为request是由executor service处理的,而ResponseHandler的是强制性的。
预定的任务可能会被取消。如果任务尚未执行,但仅仅是排队等待执行,它根本就不会执行。如果任务在执行中且mayInterruptIfRunning参数被设置为true,请求中的abort()函数将被调用;否则response会简单地忽略,但该请求将被允许正常完成。任何后续调用task.get()会产生一个IllegalStateException。应当注意到,取消任务仅可以释放客户端的资源。该请求可能实际上是在服务器端正常处理。
不用手动调用task.get(),您也可以在请求完成时使用FutureCallback实例获取回调。这里采用的是和HttpAsyncClient相同的接口
FutureRequestExecutionService通常用于大量Web服务调用的应用程序之中。为了便于例如监视或配置调整,FutureRequestExecutionService跟踪了几个指标。
HttpRequestFutureTask会提供一些方法来获得任务时间:从被安排,开始,直到结束。此外,请求和任务持续时间也是可用的。这些指标都聚集在FutureRequestExecutionService中的FutureRequestExecutionMetrics实例,可以通过FutureRequestExecutionService.metrics()获取。
参考:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/index.html