前言
虽然HttpCore只是实现了Http协议基础方面的一系列组件,但是却能够使用最少的代码开发性能全面的客户端和服务器端Http服务。
1 HttpCore Scope
第一章 基础
1.1 Http消息
1.1.1 结构
一个Http message由消息头和可选择的消息体组成。Http请求的消息是由一个请求行和头部字段的集合组成。Http响应的消息则是由一个状态行和头部字段的集合组成。所有的Http消息必须包含Http协议的版本。一些Http协议还选择性的附带内容体。
1.1.2 基本操作
1.1.2.1 Http请求消息
Http Request是一由客户端发送给服务器端的消息。消息的第一行包含请求目标资源时使用的方法,统一资源定位符,以及使用的Http协议的版本。如下图:
HttpRequest request = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1); System.out.println(request.getRequestLine().getMethod()); System.out.println(request.getRequestLine().getUri()); System.out.println(request.getProtocolVersion()); System.out.println(request.getRequestLine().toString());输出如下:
GET / HTTP/1.1 GET / HTTP/1.1
1.1.2.2 Http响应消息
Http Response是服务器接收并处理客户端发送过来的请求后,由服务器返回给客户端的消息。响应的第一行是由Http协议的版本以及数字型的状态码和与状态码相关联的短语。如下图:
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "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.2.3 Http消息的一般属性和方法
一个Http消息包含许多用于描述Http消息属性的Headers字段,比如content-length,content-type等等。HttpCore提供了许多方法让我们去获取、添加、移除和列举除这些Headers字段。(可以回过头去再看下前面我贴的图)
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"HttpCore还提供了一些非常方便的方法,用于将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[] nvp = elem.getParameters(); for (int i = 0; i < nvp.length; i++) { System.out.println(" " + nvp[i]); } System.out.println("---------------"); }输出如下:
c1 = a path=/ domain=localhost --------------- c2 = b path=/ --------------- c3 = c domain=localhost ---------------只有我们在需要时,才会将Http headers字段转为单个的header元素。从Http链接中接收到的header字段,都在内部以字符数组的形式存储,而且只有当字段的属性被访问时才会进行转换。
1.1.3 Http实体
Http消息能够携带与请求或响应有关的内容实体。由于实体并不是必须存在的,因此只有在某些请求或响应中找到。使用实体的请求被称作为实体封装的请求。Http说明里定义了两种实体封装的方法:POST和PUT。通常我们期望响应封装着内容实体。但是也会发生异常情况,比如HEAD方法的响应和204 No Content,304 Not Modified以及205 Reset Content等。
HttpCore根据实体的内容是从哪获得的,将实体分为三种类型:
1.3.1.1 重复的实体
实体能够重复,也就是实体的内容能够被多次读取。只有当使用的是self-contained的实体时才可能被多次读取。(像ByteArrayEntity和StringEntity)
1.3.1.2 Http实体的应用
由于实体的内容既可以是二进制的也可以是字符型的,因此实体现在也支持字符编码。(为了支持后者,也就是字符型内容)
当执行封装了内容的请求或者请求成功,响应体被用于发送结果给客户端时,实体被创建。
要读取实体中内容,可以通过调用HttpEntity#getContent()方法,返回一个java.io.InputStream,从而获得输入流;或者提供一个输出流给HttpEntity#writeTo(OutputStream)方法,将写入到指定输出流的内容一次返回。
EntityUtils提供了几个读取实体内容或信息时更简单的方法。我们现在可以直接调用EntityUtils类的方法来获取实体完整的字符串形式或字节数组形式的内容,而不需直接调用java.io.InputStream来读取实体内容。
当实体伴随着传入的消息被接收时,HttpEntity#getContentType()和HttpEntity#getContentLength()方法就能够被用于读取元数据,比如Content-Type和Content-Length头字段(如果他们能够被读取的话)。由于Content-Type头字段包含MIME文本类型的字符编码,像text/plain或text/html,HttpEntity#getContentEncoding()方法可获取该信息。如果头字段不可用,长度会返回-1,内容则返回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(ContentType.getOrDefault(myEntity)); System.out.println(ContentType.get(myEntity)); System.out.println(EntityUtils.toString(myEntity)); System.out.println(EntityUtils.toByteArray(myEntity).length);输出如下:
Content-Type: text/plain; charset=UTF-8 17 text/plain; charset=UTF-8 text/plain; charset=UTF-8 important message 171.1.3.3 确保系统资源的释放
为了确保合适的释放系统资源,必须关闭与实体相关的流。
HttpResponse response; HttpEntity entity = response.getEntity(); if (entity != null) { InputStream is = entity.getContent(); try { //do something } finally { is.close(); } }一旦实体中的内容被完全写出的时候,HttpEntity#writeTo(OutputStream) 方法同样需要确保合理的释放资源。如果通过调用HttpEntity#getContent()方法获取到java.io.InputStream()的实例,也要在finally语句中将流关闭。
当我们处理streamed实体时,可以使用EntityUtils#consume(HttpEntity)方法来确保实体中的内容被完全的消耗掉,并且底层的流也被关闭掉啦!
1.1.4 创建实体
有少许的方法来创建实体,下面就是由HttpCore提供的几种实现方式:
BasicHttpEntity正如它的名字所表示,是一个代表底层流的基本实体。通常用于接收从Http消息中获取的实体。
BasicHttpEntity具有一个默认的构造方法。被构造之后,内容为空,内容的长度为负数。
如果需要设置内容或长度,可以使用BasicHttpEntity#setContent(InputStream)和BasicHttpEntity#setContentLength(long)完成。
BasicHttpEntity myEntity = new BasicHttpEntity(); myEntity.setContent(someInputStream); myEntity.setContentLength(340);1.1.4.2 ByteArrayEntity
ByteArrayEntity是self-contained,可重复的实体,它的内容是从给定的字节数组中获取的。这个字节数组被传递给ByteArrayEntity的构造器。
String myData = "Hello world on the other side!!"; ByteArrayEntity myEntity = new ByteArrayEntity(myData.getBytes());1.1.4.3 StringEntity
StringEntity是self-contained,可重复的实体,它的内容是从java.lang.String对象中获取的。它有三个构造方法,一个简单的使用java.lang.String对象构造;第二个需要指定字符串数据的字符编码;第三个需要指定MIME类型。
StringBuilder sb = new StringBuilder(); Map<String, String> env = System.getenv(): for (Entry<String, String> envEntry : env.entrySet()) { sb.append(envEntry.getKey()).append(": ").append(envEntry.getValue()).append("\n"); } HttpEntity myEntity1 = new StringEntity(sb.toString()); HttpEntity myEntity2 = new StringEntity(sb.toString(), "UTF-8"); HttpEntity myEntity3 = new StringEntity(sb.toString(), "text/html", "UTF-8");1.1.4.4 InputStreamEntity
InputStreamEntity是streamed,非重复的实体,它的内容是从输入流中获取的。要创建InputStreamEntity对象需要提供输入流和内容长度这两个参数。内容长度是用来限制从输入流中读取数据的数量大小。如果读取数据的长度和输入流上可用的内容长度相匹配的话,所有的数据都会被发送出去。另外当读取数据的长度小于内容的长度的时候也会从输入流中读取所有的数据,这种情况跟内容长度完全相同的情况类似,因此内容长度这个参数经常用来限制读取数据的长度。
InputStream instream = getSomeInputStream(); InputStreamEntity myEntity = new InputStreamEntity(instream, 16);1.1.4.5 FileEntity
FileEntity是self-contained,可重复的实体,它的内容是从文件中获取的。由于FileEntity经常用于读取大量不同类型的文件,因此在构造这样的实体时,需要指定实体的内容类型,如发送zip压缩文件时,需要将内容类型指定为application/zip,发送XML文件时则为application/xml.
FileEntity entity = new FileEntity(staticFile, "application/java-archive");1.1.4.6 HttpEntityWrapper
HttpEntityWrapper是创建包装实体的基类。这个包装实体持有被包装实体的引用,并且代表所有对它的调用。我们可以使用HttpEntityWrapper实现对实体的包装,而且只需重写那些不代表包装实体方法即可。
1.1.4.7 BufferedHttpEntity
BufferedHttpEntity是HttpEntityWrapper的子类。构造这种实体需要传递另外一个实体作为参数。它会从参数实体中读取数据,并缓存在内存中。
这样就能够实现非重复实体到重复实体的转换。如果作为参数的实体已经是重复的实体,那么只是简单的将调用传递给底层的实体。
myNonRepeatableEntity.setContent(someInputStream); BufferedHttpEntity myBufferedEntity = new BufferedHttpEntity(myNonRepeatableEntity);
1.2 阻塞式Http链接
Http链接负责对Http消息的序列化和反序列化。我们很少直接使用Http Connection对象,因为存在一些执行和处理Http请求的高级别的Http协议组件。但是,在一些情形下,直接操作Http Connection是很有必要的,例如:访问链接状态、socket超时以及本地和远程的地址等属性。
一定要牢记Http Connection是线程不安全的。我们强烈建议限制同一线程中与HttpConnection对象的所有操作。HttpConnection接口和其子接口中唯一能够被我们从另外一个线程中安全调用的方法是HttpConnection#shutdown()。
1.2.1 Working with blocking Http Connections
HttpCore并没有对创建链接提供完全的支持,这是因为建立新链接的过程--尤其是在客户端--当链接调用一个或多个有效和深层的代理时,会变得非常的复杂。相反,阻塞式Http链接可以绑定到任意的网络端口上。
Socket socket = new Socket(); BasicHttpParams params = new BasicHttpParams(); DefaultHttpClientConnection conn = new DefaultHttpClientConnection(); conn.bind(socket, params); conn.isOpen(); HttpConnectionMetrics metrics = conn.getMetrics(); metrics.getRequestCount(); metrics.getResponseCount(); metrics.getSentBytesCount(); metrics.getReceivedBytesCount();客户端和服务器端的Http链接接口,发送和接收消息时都分为两步。首先是传送消息头。消息体的传送取决于消息头的属性。关闭底层流是非常重要的,因为这意味着消息处理过程的结束。那些从底层链接的输入流中获取内容的Http实体,一定要确保消息体的内容已经被全部消耗掉,这是因为链接有可能会被再次使用。
下面是客户端请求执行最简单的一个过程:
Socket socket = new Socket(); HttpParams params = new BasicHttpParams(); DefaultHttpClientConnection conn = new DefaultHttpClientConnection(); conn.bind(socket, params); HttpRequest request = new BasicHttpRequest("GET", "/"); conn.sendRequestHeader(request); HttpResponse response = conn.receiveResponseHeader(); conn.receiveResponseEntity(response); HttpEntity entity = response.getEntity(); if (entity != null) { // Do something useful with the entity and, when done, ensure all // content has been consumed, so that the underlying connection // can be re-used EntityUtils.consume(entity); }下面是服务器端处理请求最简单的一个过程:
Socket socket = new Socket(); // Initialize socket HttpParams params = new BasicHttpParams(); DefaultHttpServerConnection conn = new DefaultHttpServerConnection(); conn.bind(socket, params); HttpRequest request = conn.receiveRequestHeader(); if (request instanceof HttpEntityEnclosingRequest) { conn.receiveRequestEntity((HttpEntityEnclosingRequest) request); HttpEntity entity = ((HttpEntityEnclosingRequest) request) .getEntity(); if (entity != null) { // Do something useful with the entity and, when done, ensure all // content has been consumed, so that the underlying connection // could be re-used EntityUtils.consume(entity); } } HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK"); response.setEntity(new StringEntity("Got it")); conn.sendResponseHeader(response); conn.sendResponseEntity(response);注意我们很少使用这些低级别的方法传送信息,相反一般我会使用合适的高级别的Http服务实现。
1.2.2 Content transfer with blocking I/O
Http链接使用HttpEntity接口来管理内容转换的过程。Http链接会生成一个把传入的信息封装成内容流的实体对象。注意HttpServerConnection#receiveRequestEntity()和HttpServerConnection#receiveResponseEntity()并不会恢复或缓存传入的数据。他们很少会根据传入数据的属性注入合适的编解码器。实体内容我们可以通过使用HttpEntity#getContent()方法从封装着实体的内容输入流中读取而获得。传入的数据会自动被反编码,并完整的传给数据消费者。同样,Http链接依赖HttpEntity#writeTo(OutputStream)方法生成输出信息的内容。如果输出的信息封装了实体,那么实体内容将会根据输入信息的属性自动编码!
1.2.3 Supported content transfer mechanisms
Http链接的默认实现方式支持Http/1.1说明中定义的三种数据传输机制:
合适的流对象会根据封装着信息的实体的属性被自动创建。
1.2.4 中断Http链接
Http链接既可以通过调用HttpConnection#close()方法优雅的关闭掉也可以通过调用HttpConnection#shutdown()方法强制关闭。前者会在链接关闭前尝试刷新缓冲区内的所有数据,有可能会导致无限期的阻塞。HttpConnection#close()是线程不安全的。后者在关闭链接时不会刷新缓冲区,而且会尽快的将控制权返回给调用者。HttpConnection#shutdown()方法是线程安全的。
1.3 Http异常处理
所有的HttpCore组件都有可能抛出两种类型的异常:在I/O失败的情况下,比如端口链接超时、端口重置时会抛出IOException;在Http失败时,比如违反Http协议时会抛出HttpException。
1.3.1 协议异常
ProtocolException代表着Http协议的严重违规,通常这种情况是由于突然中断了对Http消息的处理而导致的。
1.4 Http协议处理器
Http协议拦截器是指实现了Http协议某个特定方面的程序。Http拦截器针对某个特定的Header元素或者是与输入信息有关的一组Header预算,或是输出信息时,与之相关某个特定Header元素或一组元素。协议拦截器同样可以处理封装着消息的实体,很明显内容的压缩/解压就是非常好的一个例子。通常使用装饰模式,也就是包装类用于装饰原始实体的模式来完成。几个协议拦截器也可以结合成一个逻辑单元。
Http协议处理器是实现了责任链模式的协议拦截器的集合,每一个协议拦截器都负责Http协议的某个特定方面。
只要拦截器不依赖与执行上下文某个特定状态,那么他们执行的顺序如何并不重要。如果协议拦截器存在内部依赖,那么他们必须按照特定的顺序执行,他们添加到协议处理器的顺序也和执行的顺序相同。
协议拦截器必须以线程安全的形式来实现。与Servlet相同,协议拦截器不可以使用实例变量,除非要访问那些变量是同步。
1.4.1 标准协议拦截器
HttpCore为客户端和服务器端Http的处理提供了许多必要的协议拦截器。
1.4.1.1 RequestContent
RequestContent是发送请求时最重要的拦截器。它会根据封装的实体的属性和协议的版本来添加Content-Length或Transfer-Content Header字段来限定内容的长度。它是客户端协议处理器功能正确实现所必备的。
1.4.1.2 ResponseContent
ResponseContent是发送响应时最重要的拦截器。它会根据封装的实体的属性和协议的版本来添加Content-Length或Transfer-Content Header字段来限定内容的长度。它是服务器端协议处理器功能正确实现所必备的。
1.4.1.3 RequestConnControl
RequestConnControl负责对发送的请求添加Connection 头字段,这个字段是管理Http/1.0链接的持久性必不可少的。这个拦截器建议存在于客户端的协议处理器中。
1.4.1.4 ResponseConnControl
ResponseConnControl负责对发送的响应添加Connection 头字段,这个字段是管理Http/1.0链接的持久性必不可少的。这个拦截器建议存在于服务器端的协议处理器中。
1.4.1.5 RequestDate
RequestDate负责对发送的请求添加Date头字段。这个拦截器可以选择性的添加到客户端协议处理器中。
1.4.1.6 ResponseDate
ResponseDate负责对发送的响应添加Date头字段。这个拦截器建议存在于服务器端的协议处理中。
1.4.1.7 RequestExpectContinue
RequestExpectContinue通过添加Expect头字段来使"expect-continue"实现握手。这个拦截器建议存在于客户端的协议处理器中。
1.4.1.8 RequestTargetHost
RequestTargetHost负责添加Host头字段。这个拦截器建议出现在客户端协议处理器中。
1.4.1.9 RequestUserAgent
RequestUserAgent负责添加User-Agent头字段。这个拦截器建议出现在客户端协议处理器中。
1.4.1.10 ResponseServer
ResponseServer负责添加Server头字段。这个拦截器建议出现在服务器端的协议处理器中。
1.4.2 Working with protocol processors
通常,Http协议处理器用于在执行特定的执行逻辑之前对传入的消息进行预处理,对发出的消息进行后处理。
BasicHttpProcessor httpproc = new BasicHttpProcessor(); httpproc.addInterceptor(new RequestContent()); httpproc.addInterceptor(new RequestTargetHost()); httpproc.addInterceptor(new RequestConnControl()); httpproc.addInterceptor(new RequestUserAgent()); httpproc.addInterceptor(new RequestExpectContinue()); HttpContext context = new BasicHttpContext(); HttpRequest request = new BasicHttpRequest("GET", "/"); httpproc.process(request, context); HttpResponse response = null;发送请求道目标主机并获取响应。
httpproc.process(response, context);注意BasicHttpProcessor类并不会同步访问它的内部结构,因此有可能是线程不安全。
1.4.3 HttpContext
协议拦截器可以通过分享信息来进行协调,比如处理状态就可以通过Http执行上下文来进行协调。Http Context是能够将属性名称和属性值进行映射的一种结构。本质上,Http Context通常由HashMap来实现。Http Context的主要目的是促进信息在众多逻辑相关的组件中的分享。HttpContext可以为一条或几条连续的消息储存处理状态。如果同一个上下文对象在多个连续的消息中使用,那么多个逻辑相关的消息就可以参与到一个逻辑回话中。
BasicHttpProcessor httpproc = new BasicHttpProcessor(); httpproc.addInterceptor(new HttpRequestInterceptor(){ @Override public void process(HttpRequest request, org.apache.http.protocol.HttpContext context) throws HttpException, IOException { String id = (String) context.getAttribute("session-id"); if (id != null) { request.addHeader("Session-ID", id); } } }); HttpRequest request = new BasicHttpRequest("GET", "/"); httpproc.process(request, context);HttpContext实例可以连接到一起形成一个体系。在最简单的体系里,一个context可以使用另一个context的内容,去获取本地没有属性默认值。
HttpContext parentContext = new BasicHttpContext(); parentContext.setAttribute("param1", Integer.valueOf(1)); parentContext.setAttribute("param2", Integer.valueOf(2)); HttpContext localContext = new BasicHttpContext(); localContext.setAttribute("param2", Integer.valueOf(0)); localContext.setAttribute("param3", Integer.valueOf(3)); HttpContext context = new DefaultedHttpContext(localContext, parentContext); System.out.println(context.getAttribute("param1")); System.out.println(context.getAttribute("param2")); System.out.println(context.getAttribute("param3")); System.out.println(context.getAttribute("param4"));输出如下:
1 0 3 null1.5 Http Parameters
HttpParams接口不可改变值的集合,这些不可改变值解释了一个组件的运行行为。在许多方面,HttpParams和HttpContext是相似的。这两者最大的区别是在他们各自的使用上。这两个接口都代表对象的集合,集合以文本名称和对象值进行映射,但是服务的目的不同:
HttpParams,像HttpContext一样,可以连接到一起形成体系。在最简单的格式中,一个参数集合可以使用另一个参数的内容去获取本地集合中没有的参数默认值。
HttpParams parentParams = new BasicHttpParams(); parentParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); parentParams.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8"); HttpParams localParams = new BasicHttpParams(); localParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); localParams.setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, Boolean.FALSE); HttpParams stack = new DefaultedHttpParams(localParams, parentParams); System.out.println(stack.getParameter(CoreProtocolPNames.PROTOCOL_VERSION)); System.out.println(stack.getParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET)); System.out.println(stack.getParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE)); System.out.println(stack.getParameter(CoreProtocolPNames.USER_AGENT));输出如下:
HTTP/1.1 UTF-8 false null注意BasicHttpParams类并不会同步访问其内部的结构,因此有可能会线程不安全。
1.5.1 HTTP Parameter beans
HttpParams接口在处理组件的配置时存在很大的灵活性。更重要的是,新的参数可以在不影响旧版本参数通用性的情况下引进进来。但是,HttpParams和常规的Java beans相比还是存在一定缺点:HttpParams不可以使用DI框架进行整合。为了减少限制,HttpCore引入了许多可以被使用bean类,主要是为了使用标准的JavaBean来实例化HttpParams对象。
HttpParams params = new BasicHttpParams(); HttpProtocolParamBean paramBean = new HttpProtocolParamBean(params); paramBean.setVersion(HttpVersion.HTTP_1_1); paramBean.setContentCharset("UTF-8"); paramBean.setUseExpectContinue(true); System.out.println(params.getParameter(CoreProtocolPNames.PROTOCOL_VERSION)); System.out.println(params.getParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET)); System.out.println(params.getParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE)); System.out.println(params.getParameter(CoreProtocolPNames.USER_AGENT));输出如下:
HTTP/1.1 UTF-8 true null