前言
HttpCore是一套实现了HTTP协议最基础方面的组件,尽管HTTP协议在使用最小占用来开发全功能的客户端和服务器的HTTP服务是足够的。
HttpCore有如下的范围和目标:
1. HttpCore范围
- 构建客户端/代理/服务器端HTTP服务一致的API
- 构建同步和异步HTTP服务一致的API
- 基于阻塞(经典的)和非阻塞(NIO)I/O模型的一套低等级组件
2. HttpCore目标
- 实现最基本的HTTP传输方面
- 良好性能和清晰度&表现力之间的平衡
- 小的(预测)内存占用
- 自我包含的类库(没有超越JRE的额外依赖)
3. 什么是HttpCore不能做的
- HttpClient的替代
- Servlet容器或Servlet API竞争对手的替代
1.1 HTTP报文
1.1.1 结构
HTTP报文由头部和可选的内容体构成。HTTP请求报文的头由请求行和头部字段的集合构成。HTTP响应报文的头部由状态行和头部字段的集合构成。所有HTTP报文必须包含协议版本。一些HTTP报文可选地可以包含内容体。
HttpCore定义了HTTP报文对象模型,它紧跟定义,而且提供对HTTP报文元素进行序列化(格式化)和反序列化(解析)的支持。
1.1.2 基本操作
1.1.2.1 HTTP请求报文
HTTP请求是由客户端向服务器端发送的报文。报文的第一行包含应用于资源的方法,资源的标识符,和使用的协议版本。
1
2
3
4
5
|
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响应是由服务器在收到和解释请求报文之后发回客户端的报文。报文的第一行包含了协议的版本,之后是数字状态码和相关的文本段。
1
2
3
4
5
|
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提供方法来获取,添加,移除和枚举头部信息。
1
2
3
4
5
6
7
8
9
|
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接口。
1
2
3
4
5
6
7
|
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报文到独立头部元素。
1
2
3
4
5
6
7
8
9
10
11
12
|
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对象。
当为一个传出报文创建实体时,这个元数据不得不通过实体创建器来提供。
1
2
3
4
5
6
|
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
1.1.3.3 确保系统资源被释放
为了保证适当释放系统资源,我们必须关闭和实体相关的内容流。
1
2
3
4
5
6
7
8
9
10
|
HttpResponse response;
HttpEntity entity = response.getEntity();
if
(entity !=
null
) {
InputStream instream = entity.getContent();
try
{
// 做些有意义的事情
}
finally
{
instream.close();
}
}
|
要注意一旦实体被完全写出后,HttpEntity#writeTo(OutputStream)方法也要求保证适当的释放系统资源。如果这个方法通过调用HttpEntity#getContent()获得了一个java.io.InputStream实例,也期望在finally语句中关闭流。
当处理流式实体时,我们可以使用EntityUtils#consume(HttpEntity)方法来保证实体内容被完全消耗而且低层的流被关闭。
1.1.4 创建实体
创建实体有一些方法。下面的实现是由HttpCore提供的:
- BasicHttpEntity
- ByteArrayEntity
- StringEntity
- InputStreamEntity
- FileEntity
- EntityTemplate
- HttpEntityWrapper
- BufferedHttpEntity
1.1.4.1 BasicHttpEntity 基本HTTP实体
这就像它的名字所描述的一样,代表低层流的基本实体。它通常用于从HTTP报文中获取的实体。
这个实体有一个空的构造方法。再创建之后它代表没有内容,而且有内容长度是一个负数。
我们需要来设置内容流,和可选的长度值。这分别可以使用BasicHttpEntity#setContent(InputStream)和BasicHttpEntity#setContentLength(long)方法来完成。
1
2
3
|
BasicHttpEntity myEntity =
new
BasicHttpEntity();
myEntity.setContent(someInputStream);
myEntity.setContentLength(
340
);
// 把长度设置为340
|
1.1.4.2 ByteArrayEntity 字节数组实体
ByteArrayEntity是自我包含的,可重复的,从给定的字节数组中获取内容的实体。字节数组是供给构造方法的。
1
2
|
String myData =
"Hello world on the other side!!"
;
ByteArrayEntity myEntity =
new
ByteArrayEntity(myData.getBytes());
|
1.1.4.3 StringEntity 字符串实体
StringEntity是自我包含的,可重复的,从java.lang.String对象中获取内容的实体。它有两个构造方法,一个使用给定的java.lang.String对象来构造;另外那个也对字符串中的数据使用字符编码。
1
2
3
4
5
6
7
8
9
|
StringBuffer sb =
new
StringBuffer();
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"
);
|
1.1.4.4 InputStreamEntity 输入流实体
InputStreamEntity是流式的,不能重复的实体,它获得输入流中的内容。它通过提供输入流和内容长度来构建。内容长度用来限制从java.io.InputStream对象中读取数据的数量。如果长度匹配输入流中可用的内容长度,那么所有数据都会被发送。另外负数的内容长度将会从输入流中读取所有数据,这和提供精确的内容长度是一样的,所以长度通常是用来限制长度的。
1
2
|
InputStream instream = getSomeInputStream();
InputStreamEntity myEntity =
new
InputStreamEntity(instream,
16
);
|
1.1.4.5 FileEntity 文件实体
FileEntity是自我包含式,可以重复的实体,它从文件中获取内容。因为这通常是用于不同类型的大文件流,我们需要提供文件的类型,比如,发送一个zip文件需要application/zip类型,对于XML就是application/xml。
1
|
HttpEntity entity =
new
FileEntity(staticFile,
"application/java-archive"
);
|
1.1.4.6 EntityTemplate 实体模板
这是从ContentProducer接口接收内容的实体。内容生产者是生成它们想要内容的对象,将它们写入到输出流中。它们被期望每次被请求时来生成内容。所以创建EntityTemplate后,我们要提供给内容生产者提供一个引用,这可以很有效地创建可重复实体。
在HttpCore中没有标准的内容生产者。基本上只是允许包装复杂逻辑到实体的方便的接口。要使用实体,我们需要创建实现ContentProducer接口的类,并覆盖ContentProducer#writeTo(OutputStream)方法。之后,自定义ContentProducer的实例会被用来将全部内容体写入到输出流中。比如,HTTP服务器会使用FileEntity来服务静态文件,但是运行CGI程序可以通过ContentProducer完成,在其中可以实现自定义逻辑来提供可用的内容。这个方式我们不需要在字符串中来缓冲,之后使用StringEntity或ByteArrayEntity。
1
2
3
4
5
6
7
8
|
ContentProducer myContentProducer =
new
ContentProducer() {
public
void
writeTo(OutputStream out)
throws
IOException {
out.write(
"ContentProducer rocks! "
.getBytes());
out.write((
"Time requested: "
+
new
Date()).getBytes());
}
};
HttpEntity myEntity =
new
EntityTemplate(myContentProducer);
myEntity.writeTo(System.out);
|
输出内容为:
ContentProducer rocks! Time requested: Fri Sep 05 12:20:22 CEST 2008
1.1.4.7 HttpEntityWrapper HTTP实体包装器
这是创建被包装实体的基类。包装实体持有被包装实体的引用,而且将所有调用都委派给它。包装实体的实现可以从这个类派生,而且需要覆盖仅仅那些不能被委派给被包装实体的方法。
1.1.4.8 BufferedHttpEntity 缓冲HTTP实体
BufferedHttpEntity是HttpEntityWrapper的子类。它由提供另外一个实体来创建。它从所提供实体内读取内容,然后缓冲在内存中。
这使得从不可重复实体中生成可重复的实体成为可能。如果所提供的实体已经是可重复的,那么调用仅仅是传递给底层实体。
1
2
|
myNonRepeatableEntity.setContent(someInputStream);
BufferedHttpEntity myBufferedEntity =
new
BufferedHttpEntity(myNonRepeatableEntity);
|
1.2 阻塞HTTP连接
HTTP连接是负责HTTP报文序列化和反序列化的。我们应该极少直接使用HTTP连接对象。有高级协议组件来执行HTTP请求的处理。而在某些情况下直和HTTP连接直接交互是需要的,比如,访问如连接状态,套接字超时时间或本地和远程地址属性。
牢记HTTP连接是线程不安全的,这点非常重要。强烈建议限制所有HTTP连接对象的交互到一个线程中。HttpConnection接口唯一的方法和它的从另外一个线程中安全调用子接口的HttpConnection#shutdown()方法。
1.2.1 使用阻塞的HTTP连接
HttpCore不提供打开连接的完全支持,因为创建一个新连接的过程-特别是在客户端创建,当涉及到一个或多个认证和或/和通道代理时,这个过程可能是非常复杂的。相反,阻塞HTTP连接可以绑定到任意的网络套接字上。
1
2
3
4
5
6
7
8
9
10
11
|
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.getReceivedBytesCount();
metrics.getSentBytesCount();
|
在客户端和服务器端的HTTP连接接口都可以在两个阶段发送和接收报文。报文头部首先传送。根据报文头部的属性,它可能跟着报文体。请注意通常关闭低层的内容流来给报文处理完毕发送信号是非常重要的。从底层连接输入流中直接获取内容流出的HTTP实体必须要保证报文体的内容被潜在的可重用的连接完全消耗。
简化客户端请求执行的程序可能看起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
) {
// 用实体做点有用的事情,当完成时,报省所有内容都被消耗掉
// 所以底层连接可能被重用。
EntityUtils.consume(entity);
}
|
简化服务器端请求处理的程序可能看起来像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Socket socket =
new
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
) {
// 用实体做点有用的事情,当完成时,报省所有内容都被消耗掉
// 所以底层连接可能被重用。
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 内容传输与阻塞I/O
HTTP连接报文的内容传输处理使用HttpEntity接口。HTTP连接生成封装了收到报文的内容流的实体对象。请注意HttpServerConnection#receiveRequestEntity()和HttpClientConnection#receiveResponseEntity()方法不会取出或缓冲任何收到的数据。它们仅仅是注入合适的依赖于收到报文属性的内容解码器。内容可以使用HttpEntity#getContent()方法,通过从包含实体的内容输入流中读取来获得。收到的数据将会完全自动被解码,透明地给数据消费者。同样,HTTP连接依HttpEntity#writeTo(OutputStream)方法来生成输出报文的内容。如果输出报文包含实体,那么内容将会基于报文属性自动编码。
1.2.3 支持的内容传输机制
默认的HTTP连接支持的实现支持三种内容传机制输由HTTP/1.1规范定义:
- Content-Length分隔:内容实体的结束是由头部信息Content-Length的值来决定的。最大的实体长度:Long#MAX_VALUE。
- 身份编码:内容实体的结束是通过关闭底层连接(流结束的条件)来划定的。由于显而易见的原因,身份编码只能在服务器端使用。最大实体长度:不限制。
- 块编码:内容是以小块发送的。最大实体长度:不限制。
合适的内容流类将会依赖于报文内包含实体的属性来自动创建。
1.2.4 终止HTTP连接
HTTP连接的终止可以通过优雅地调用HttpConnection#close()方法或强制地调用HttpConnection#shutdown()方法。前者试图刷新所有缓冲数据,优先于终止连接也许会无限期阻塞。HttpConnection#close()方法是线程不安全的。后者终止连接不会去刷新内部缓冲而会把控制权尽快返回给调用者不会阻塞太长时间。HttpConnection#shutdown()方法是线程安全的。
1.3 HTTP异常处理
所有的HttpCore组件会潜在地抛出两种类型的异常:在如套接字超时或重置的I/O失败时的IOException异常,还有如违反HTTP协议标识着HTTP失败的HttpException异常。通常I/O错误被认为是非致命的而且可以恢复的,而HTTP协议错误被认为是致命的而且是不能自动恢复的。
1.3.1 协议异常
ProtocolException异常标识着致命的违反HTTP协议,通常会导致立即终止HTTP报文的处理。
1.4 HTTP协议处理器
HTTP协议拦截器是实现了HTTP协议特定方面的规则。通常情况下,协议拦截器期望作用于特定的头部信息或收到的报文的一组相关的头部信息,或者是使用特定的头部信息或一组相关的头部信息填充发出的报文。协议拦截器也可以操作包含在报文中的内容实体,透明的内容压缩/解压就是一个很好的示例。通常这可以使用“装饰者”模式来完成,其中一个包装器实体类可以用于装饰原实体。一些协议拦截器可以联合起来成为一个逻辑单元。
HTTP协议处理器是一组协议拦截器的集合,并且实现了“责任链”模式,其中每个独立的协议拦截器期望处理它负责的HTTP协议的特定方面。
通常情况下,拦截器执行的顺序应该和依赖于特定的内容执行状态没有关系。如果协议拦截器有相互依赖的关系,那么就必须按照特定的顺序来执行,它们应该以被期望执行顺序的相同的序列被加入到协议处理器中。
协议拦截器必须实现为线程安全的。和Servlet相似,协议拦截器不应该使用实例变量,除非访问的变量是同步的。
1.4.1 标准协议拦截器
HttpCore附带了一些很重要的协议拦截器,用于客户端和服务器端的HTTP处理。
1.4.1.1 RequestContent 请求内容
RequestContent是对发出请求最重要的拦截器。它负责划定内容的长度,通过添加基于被包含实体和协议版本属性的Content-Length或Transfer-Content头部信息来实现。这个拦截器需要正确的客户端协议处理器的运行。
1.4.1.2 ResponseContent 响应内容
ResponseContent是对发出响应最重要的拦截器。它负责划定内容的长度,通过添加基于被包含实体和协议版本属性的Content-Length或Transfer-Content头部信息来实现。这个拦截器需要正确的服务器端协议处理器的运行。
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-continue”(希望继续)握手,这是通过添加头部信息Expect来完成的。这个拦截器建议用于客户端协议处理器。
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 使用协议处理器
通常情况下,HTTP协议处理器用于在执行应用特定处理逻辑之前预处理接收的报文,在发送出报文之后也要一些后续处理工作。
1
2
3
4
5
6
7
8
9
10
11
12
|
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
;
|
将请求发送到目标服务器,获取响应。
1
|
httpproc.process(response, context);
|
请注意BasicHttpProcessor类没有同步对它内部结构的访问,因此可能是线程不安全的。
1.4.3 HTTP上下文
协议拦截器可以通过共享信息来合作-比如处理状态-通过HTTP执行上下文。HTTP上下文是一个用于映射属性名称到属性值的结构。从内部来说,HTTP上下文实现通常是用一个HashMap。HTTP上下文主要的作用是促进信息在多个逻辑相关组件之间的共享。HTTP上下文。HTTP上下文可以用来存储一个或几个连续报文的处理状态。如果相同的上下文在连续报文之间被重用,多个逻辑相关报文可以参与到一个逻辑会话中。
1
2
3
4
5
6
7
8
9
10
11
|
BasicHttpProcessor httpproc =
new
BasicHttpProcessor();
httpproc.addInterceptor(
new
HttpRequestInterceptor() {
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);
}
}
});
HttpRequest request =
new
BasicHttpRequest(
"GET"
,
"/"
);
httpproc.process(request, context);
|
HttpContext的实例可以连接起来形成一个层次结构。在最简单的形式中,一个上下文可以使用另外一个上下文的内容来获取本地上下文中没有表现出的属性的默认值。
1
2
3
4
5
6
7
8
9
10
11
12
|
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 stack =
new
DefaultedHttpContext(localContext,
parentContext);
System.out.println(stack.getAttribute(
"param1"
));
System.out.println(stack.getAttribute(
"param2"
));
System.out.println(stack.getAttribute(
"param3"
));
System.out.println(stack.getAttribute(
"param4"
));
|
输出内容为:
1
0
3
null
1.5 HTTP参数
HttpParams接口代表定义组件运行时行为不变的值的集合。在很多方面,HttpParams和HttpContext是很相似的。二者之间的主要区别是它们在运行时的使用。两个接口都代表了被组织为文本名称和对象值映射的对象集合,但是服务于不同的目的:
- HttpParams意在包含简单对象:整数,双精度浮点数,字符串,集合和在运行时保持不变的对象。HttpParams期望用于“一次编写-多处准备”模式。HttpContext意在包含在HTTP报文处理期间可以轻易改变的复杂对象。
- HttpParams的目标是定义其它组件的行为。通常情况下,每个复杂组件都有它自己的HttpParams对象。HttpContext的目标是用来代表一个HTTP处理的执行状态。通常情况下,相同执行上下文在很多合作的对象中是共享的。
就像HttpContext,HttpParams也可以连接在一起形成层次结构。在最简单的形式中,参数的集合可以使用另外一个的内容来获取没有在这个集合中出现的参数的默认值。
1
2
3
4
5
6
7
8
9
10
11
|
HttpParams parentParams =
new
BasicHttpParams();
parentParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_0);
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参数bean
HttpParams接口允许在处理组件配置时的很大的灵活性。很重要的是,新的参数可以被引入而不影响老版本的二进制组件。但是和传统的Java bean相比,HttpParams也有一个确定的缺点:HttpParams不能使用DI(Dependency Injection,依赖注入,译者注)框架来组装。为了环节限制,HttpCore包含了很多bean类,它们可以使用标准的Java bean约定来初始化对象。
1
2
3
4
5
6
7
8
9
|
HttpParams params =
new
BasicHttpParams();
HttpProtocolParamBean paramsBean =
new
HttpProtocolParamBean(params);
paramsBean.setVersion(HttpVersion.HTTP_1_1);
paramsBean.setContentCharset(
"UTF-8"
);
paramsBean.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
false
null
1.6 阻塞HTTP协议处理程序
1.6.1 HTTP服务
HttpService是基于阻塞I/O模型来为由RFC 2616(HTTP/1.1规范定义的RFC文档,译者注)描述的服务器端报文处理,实现HTTP协议的基本要求的服务器端HTTP协议处理程序。
HttpService依赖于HttpProcessor实例对所有发出的报文来生成强制协议头部并对所有收到的和发出的报文应用通用的,交叉消息转换,而HTTP请求处理程序期望保护应用程序特定的内容生成和处理。
1
2
3
4
5
6
7
8
|
HttpParams params;
// 初始化HTTP参数
HttpProcessor httpproc;
// 初始化HTTP处理器
HttpService httpService =
new
HttpService(
httpproc,
new
DefaultConnectionReuseStrategy(),
new
DefaultHttpResponseFactory());
httpService.setParams(params);
|
1.6.1.1 HTTP请求处理程序
HttpRequestHandler接口代表了处理指定HTTP请求组的规则。HttpService被设计用来关注协议的指定方面,而独立的请求处理程序期望去关注应用程序特定的HTTP处理。请求处理程序的主要目的是使用内容实体生成响应对象送回客户端来响应给定的请求。
1
2
3
4
5
6
7
8
|
HttpRequestHandler myRequestHandler =
new
HttpRequestHandler() {
public
void
handle(HttpRequest request,HttpResponse response, HttpContext context)
throws
HttpException, IOException {
response.setStatusCode(HttpStatus.SC_OK);
response.addHeader(
"Content-Type"
,
"text/plain"
);
response.setEntity(
new
StringEntity(
"some important message"
));
}
};
|
1.6.1.2 请求处理程序解析器
HTTP请求处理程序通常是由HttpRequestHandlerResolver来管理的,它匹配请求URI到请求处理程序。HttpCore包含一个基于琐碎模式匹配算法:HttpRequestHandlerRegistry支持仅仅3种格式:*,<uri>*和*<uri>的请求处理程序解析器的非常简单的实现
1
2
3
4
5
6
7
8
9
|
HttpService httpService;
// 初始化HTTP服务
HttpRequestHandlerRegistry handlerResolver =
new
HttpRequestHandlerRegistry();
handlerReqistry.register(
"/service/*"
, myRequestHandler1);
handlerReqistry.register(
"*.do"
, myRequestHandler2);
handlerReqistry.register(
"*"
, myRequestHandler3);
// 注入处理程序解析器
httpService.setHandlerResolver(handlerResolver);
|
我们鼓励用户提供更复杂的HttpRequestHandlerResolver实现,比如,基于正则表达式的实现。
1.6.1.3 使用HTTP服务来处理请求
当完全加载和配置后,HttpService可以用于执行和处理对活动HTTP连接的请求。HttpService#handleRequest()方法读取传入的请求,生成响应并发回客户端。这个方法可以在循环中执行来处理一个持久化连接中的多个请求。HttpService#handleRequest()方法在多线程中执行是安全的。这就允许同时对多个连接的请求进行处理,只要所有的协议拦截器和请求处理程序由HttpService使用是线程安全的就行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
HttpService httpService;
// 初始化HTTP服务
HttpServerConnection conn;
// 初始化连接
HttpContext context;
// 初始化HTTP上下文
boolean
active =
true
;
try
{
while
(active && conn.isOpen()) {
httpService.handleRequest(conn, context);
}
}
finally
{
conn.shutdown();
}
|
1.6.2 HTTP请求执行器
HttpRequestExecutor是基于阻塞I/O模型的客户端HTTP协议处理程序,它实现了HTTP协议对客户端报文处理的基本要求,这在RFC 2616中被描述出来了。HttpRequestExecutor依赖于HttpProcessor实例对所有发送的报文来生成强制性的协议头部信息,而且对所有收到和发送的报文应用通用的,交叉报文转换。应用程序指定的处理可以当请求被执行和收到响应时在HttpRequestExecutor外部实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
HttpClientConnection conn;
// 创建连接
HttpParams params;
// 初始化HTTP参数
HttpProcessor httpproc;
// 初始化HTTP处理器
HttpContext context;
// 初始化HTTP上下文
HttpRequestExecutor httpexecutor =
new
HttpRequestExecutor();
BasicHttpRequest request =
new
BasicHttpRequest(
"GET"
,
"/"
);
request.setParams(params);
httpexecutor.preProcess(request, httpproc, context);
HttpResponse response = httpexecutor.execute(request, conn, context);
response.setParams(params);
httpexecutor.postProcess(response, httpproc, context);
HttpEntity entity = response.getEntity();
EntityUtils.consume(entity);
|
HttpRequestExecutor的方法在多线程中执行是安全的。这就允许同时对多个连接的请求进行处理,只要所有的协议拦截器由HttpRequestExecutor使用是线程安全的就行。
1.6.3 连接持久化/重用
ConnectionReuseStrategy接口意在决定是否底层的连接可以对将来在当前报文完成传输之后报文的处理继续重用。默认的连接重用策略无论何时都尝试保持连接活动。首先,它会检查用于传输报文的HTTP协议的版本。HTTP/1.1连接默认是持久性的,而HTTP/1.0连接则不是。其次,它会检查Connection头部信息的值。它的相同作用的实例也可以通过在Connection头部信息中发送Keep-Alive或值指示是否在另一端重用连接。第三,这个策略基于被包含实体的属性(如果属性可用时)决定连接是否安全地重用。