第一章基础知识
1.1 HTTP消息
1.1.1 结构
HTTP消息由一个消息头和可选的消息体构成。HTTP请求的消息头包含一个请求行和头字段的集合。HTTP响应的消息头包含一个状态行和头字段的集合。所有的HTTP消息必须包含协议版本号。一些HTTP消息可以包含消息体。HttpCore定义了HTTP消息对象模型,紧密地遵守这个定义,并为HTTP消息元素提供了大量的序列化(格式化)和反序列化(解析)的支持。
1.1.2 基本操作
1.1.2.1 HTTP请求消息
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响应是在接收到并解析了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 |
1.1.2.3 HTTP消息通用属性和方法
HTTP消息可以包含一些描述消息属性的头部信息,例如内容长度、内容类型等等。HttpCore提供了获取、添加、删除和枚举头部信息的方法。
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 |
有一个获得所有给定类型头部信息的高效途径,是使用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连接中获取的HTTP头部信息被作为字符数组存储在内部而且仅当它们的属性被访问时才延迟解析。
1.1.3 HTTP实体
HTTP消息可以携带和请求或响应相关的内容实体。实体可以在一些请求和响应中发现,因为它们是可选的。使用了实体的请求被称为包含请求的实体。HTTP规范定义了两种包含实体的方法:POST和PUT。响应通常期望是包含内容实体的。这个规则也有一些例外,比如对HEAD方法的响应,204没有内容,304没有修改,205重置内容响应。HttpCore区分三种类型的实体,这是基于它们的内容是在哪里生成的:
- streamed流式:内容从流中获得,或者在运行中产生。特别是这种分类包含从HTTP响应中获取的实体。流式实体是不可重复生成的。
- self-contained自我包含式:内容在内存中或通过独立的连接或其它实体中获得。自我包含式的实体是可以重复生成的。这种类型的实体会经常用于封闭HTTP请求的实体。
- wrapping包装式:内容从另外一个实体中获得。对于获得不同实体的连接管理的区别是很重要的。对于由应用程序创建的实体仅仅使用HttpCore框架来发送,流式和自我包含式之间的区别就没有多大的重要性了。那种情况下,建议将不可重复的实体视为流式的,而可以重复的视为自我包含式的。
1.1.3.1 可重复实体
任何可以重复的实体,就是说它的内容可以多次读取。这仅仅是对自我包含式实体来说的(比如ByteArrayEntity或StringEntity)。
1.1.3.2 使用HTTP实体
由于实体既可以表示二进制内容,也可以表示字符内容,它对字符编码就有支持(对于
后者来说的,也就是字符内容)。
当执行封装了内容的请求,或当请求已经执行成功,用响应体将结果发送回客户端时,就会创建实体。
要从实体中读取内容,可以通过HttpEntity#getContent()方法从输入流中获取,这会返回一个java.io.InputStream对象,或者提供一个输出流到HttpEntity#writeTo(OutputStream)方法中,这会一次返回所有写入到给定流中的内容。
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", Consts.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 important message 17 |
1.1.3.3 确保释放系统资源
为了确保适当的释放系统资源,我们必须关闭与实体有关的内容流。
HttpResponse response; HttpEntity entity = response.getEntity(); if (entity != null) { InputStream instream = entity.getContent(); try { // do something useful } finally { instream.close(); } } |
当使用流实体时,可以用EntityUtils#consume(HttpEntity)方法确保实体内容完全被消费且底层的流被关闭。
1.1.4 创建实体
有几种方式来创建实体。HttpCore提供了下面的实现:
- BasicHttpEntity
- ByteArrayEntity
- StringEntity
- InputStreamEntity
- FileEntity
- EntityTemplate
- HttpEntityWrapper
- BufferedHttpEntity
1.1.4.1. BasicHttpEntity
正如这个类名暗示的那样,这个基本实体表示底层的流。通常,从HTTP消息中接收的实体使用这个类。
这个实体类有一个空的构造器。在构造之后,它表示没有内容,而且内容长度为负值。
我们需要设置内容流,和可选的长度。这个分别使用BasicHttpEntity#setContent(InputStream)和BasicHttpEntity#setContentLength(long)方法完成。
BasicHttpEntity myEntity = new BasicHttpEntity(); myEntity.setContent(someInputStream); myEntity.setContentLength(340); // sets the length to 340 |
1.1.4.2. ByteArrayEntity
ByteArrayEntity 是一个自包含、可重复的实体,它从一个字节数组获取内容。为构造器提供一个字节数组:
ByteArrayEntity myEntity = new ByteArrayEntity(new byte[] {1,2,3}, ContentType.APPLICATION_OCTET_STREAM); |
1.1.4.3. StringEntity
StringEntity 是一个自包含、可重复的实体,它从String对象获取内容。它有三个构造器,一个是具有String参数的简单构造器,第二个是指定String字符串编码格式的构造器,第三个是允许指定mime类型。
StringBuilder sb = new StringBuilder(); Map<String, String> env = System.getenv(); for (Map.Entry<String, String> envEntry : env.entrySet()) { sb.append(envEntry.getKey()).append(": ").append(envEntry.getValue()).append("\r\n"); } // construct without a character encoding (defaults to ISO-8859-1) HttpEntity myEntity1 = new StringEntity(sb.toString()); // alternatively construct with an encoding (mime type defaults to // "text/plain") HttpEntity myEntity2 = new StringEntity(sb.toString(), Consts.UTF_8); // alternatively construct with an encoding and a mime type HttpEntity myEntity3 = new StringEntity(sb.toString(), ContentType.create("text/plain", Consts.UTF_8)); |
1.1.4.4. InputStreamEntity
InputStreamEntity 是流式的、不能重复的实体,它从输入流获取内容。构造它需要提供输入流和内容长度。使用内容长度限制从java.io.InputStream读取的数据数量。如果这个长度匹配输入流中可用的内容长度,那么所有数据都会被发送。作为选择,也可指定一个负的内容长度,它会读取输入流中所有的数据,效果与提供一个精确的内容长度是一样的,所以使用长度来限制读取的数据的数量。
InputStream instream = getSomeInputStream(); InputStreamEntity myEntity = new InputStreamEntity(instream, 16); |
1.1.4.5. FileEntity
FileEntity 是一个自包含、可重复的实体,它从文件中获取内容。用这个实体,大多是为了不同类型的大文件的流化。例如,发送一个zip文件需要内容类型为application/zip,XML的话为application/xml。
HttpEntity entity = new FileEntity(staticFile, ContentType.create("application/java-archive"));
1.1.4.6. HttpEntityWrapper
这是创建包装实体的基类。包装实体支持被包装实体的引用,并代理被包装实体的所有调用。包装实体的实现可以从这个类派生,只需要覆写那些不应该被委派给被包装实体的方法。
1.1.4.7. BufferedHttpEntity
BufferedHttpEntity是HttpEntityWrapper的子类。要构造它需要提供另一个实体。它从提供的实体中读取内容,在缓存到内存里。
这使得从不可重复的实体生成可重复的实体成为可能。如果提供的实体已经是可重复的了,它仅仅将调用传递给底层实体。
myNonRepeatableEntity.setContent(someInputStream); BufferedHttpEntity myBufferedEntity = new BufferedHttpEntity(myNonRepeatableEntity); |
1.2. HTTP协议处理器
HTTP协议拦截器是实现了HTTP协议特定方面的规则。通常情况下,协议拦截器期望作用于特定的头部信息或收到的消息的一组相关的头部信息,或者是使用特定的头部信息或一组相关的头部信息填充发出的消息。协议拦截器也可以操作包含在报文中的内容实体,透明的内容压缩/解压就是一个很好的示例。通常这可以使用“装饰器”模式来完成,其中一个包装器实体类可以用于装饰原实体。一些协议拦截器可以联合起来成为一个逻辑单元。
HTTP协议处理器是一组协议拦截器的集合,并且实现了“职责链”模式,其中每个独立的协议拦截器期望处理它负责的HTTP协议的特定方面。
通常情况下,拦截器执行的顺序应该和依赖于特定的内容执行状态没有关系。如果协议拦截器有相互依赖的关系,那么就必须按照特定的顺序来执行,它们应该以被期望执行顺序的相同的序列被加入到协议处理器中。
协议拦截器必须实现为线程安全的。和Servlet相似,协议拦截器不应该使用实例变量,除非访问的变量是同步的。
1.2.1. 标准协议拦截器
HttpCore附带了一些很重要的协议拦截器,用于客户端和服务器端的HTTP处理。
1.2.1.1. RequestContent
对外发请求来说,RequestContent是最重要的拦截器。它负责划定内容的长度,通过添加基于被包含实体和协议版本属性的Content-Length或Transfer-Content头部信息来实现。这个拦截器需要正确的客户端协议处理器的运行。
1.2.1.2. ResponseContent
对于外发响应来说,ResponseContent是最重要的拦截器。它负责划定内容的长度,通过添加基于被包含实体和协议版本属性的Content-LengthTransfer-Content头部信息来实现。这个拦截器需要正确的服务器端协议处理器的运行。
1.2.1.3. RequestConnControl
RequestConnControl负责添加Connection头到外发的请求中,这对于管理HTTP/1.0的持久化是必须的。这个拦截器建议用于客户端协议处理器。
1.2.1.4. ResponseConnControl
ResponseConnControl 负责添加Connection头到外发的响应中,这对于管理HTTP1.0的持久化是必须的。这个拦截器建议用于服务端协议处理器。
1.2.1.5. RequestDate
RequestDate负责添加Date头到外发的请求中。这个拦截器对于客户端协议处理器来是可选的。
1.2.1.6. ResponseDate
ResponseDate负责添加Date头到外发的响应中。这个拦截器推荐用于服务端协议处理器。
1.2.1.7. RequestExpectContinue
RequestExpectContinue负责使能“expect-continue”握手,这个需要添加Expect头。这个拦截器推荐用于客户端协议处理器。
1.2.1.8. RequestTargetHost
RequestTargetHost负责添加Host头。这个拦截器对于客户端协议处理器是必需的。
1.2.1.9. RequestUserAgent
RequestUserAgent负责添加User-Agent头。这个拦截器推荐用于客户端协议处理器。
1.2.1.10. ResponseServer
ResponseServer负责添加Server头。这个拦截器推荐用于服务端协议处理器。
1.2.2. 使用协议拦截器
通常情况下,HTTP拦截器用于在处理特定的应用逻辑之前执行接收消息的预处理,在发送消息之后的后续处理。
HttpProcessor httpproc = HttpProcessorBuilder.create() // Required protocol interceptors .add(new RequestContent()) .add(new RequestTargetHost()) // Recommended protocol interceptors .add(new RequestConnControl()) .add(new RequestUserAgent("MyAgent-HTTP/1.1")) // Optional protocol interceptors .add(new RequestExpectContinue(true)) .build(); HttpCoreContext context = HttpCoreContext.create(); HttpRequest request = new BasicHttpRequest("GET", "/"); httpproc.process(request, context); |
将这个请求发送到目标主机,并得到一个响应:
HttpResponse = <...>
httpproc.process(response, context);
请注意BasicHttpProcessor类不会同步访问内部的结构,因此它们不是线程安全的。
1.3. HTTP执行上下文
最初,HTTP被设计为无状态的、面向请求-响应的协议。然而,真实世界中许多应用经常需要通过几个逻辑上相关的请求-响应交互来持久化状态信息。为了然该应用能够维护一个处理状态,HttpCore允许HTTP消息在一个特定的执行上下文中执行。多个逻辑上相关的消息可以参与到同一个逻辑会话中,如果在连续的请求中间复用同一个上下文。HTTP上下文功能类似于java.util.Map<String, Object>。它只是逻辑上相关的名字值对(name-value)的集合。
请注意,HttpContext可以包含任意的对象,因此在多个线程间共享是不安全的。必须确保在同一时间只有一个线程访问HttpContext。
1.3.1. 上下文共享
协议拦截器可以通过共享信息来协作-比如处理状态-通过HTTP执行上下文。HTTP上下文是一个用于映射属性名称到属性值的结构。从内部来说,HTTP上下文实现通常是用一个HashMap。HTTP上下文主要的目的是促进信息在多个逻辑相关组件之间的共享。HTTP上下文可以用来存储一个或几个连续消息的处理状态。如果相同的上下文在连续消息之间被重用,多个逻辑相关消息可以参与到一个逻辑会话。
HttpProcessor httpproc = HttpProcessorBuilder.create().add(new HttpRequestInterceptor() { @Override public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { String id = (String) context.getAttribute("session-id"); if (id != null) { request.addHeader("Session-ID", id); } } }).build(); HttpCoreContext context = HttpCoreContext.create(); HttpRequest request = new BasicHttpRequest("GET", "/"); httpproc.process(request, context); |