第二章 阻塞式I/O模型
Java中的阻塞式I/O,或者叫经典I/O,代表一个高效的、方便的I/O模型,特别适合于高性能要求的应用,并发连接的数量相对稳定。现代JVM能够处理高效的上下文切换,只要连接数低于1000且所有连接都忙于传送数据,那么阻塞式I/O模型应该为原始数据的吞吐量提供最佳性能。然而对于那些大部分时间处于空闲状态的连接,上下文切换的开销是巨大的,非阻塞式I/O可能是一个更好的选择。
2.1. 阻塞式HTTP连接
HTTP连接负责HTTP消息的序列化和反序列化。用户应该很少有需要直接使用HTTP连接对象。有更高级的协议组件用于执行和处理HTTP请求。然而,在一些场景中,直接与HTTP连接交互可能是有必要的。例如,访问属性,如连接状态、socket超时时间或者本地地址和远端地址。
请牢牢记住HTTP连接不是线程安全的。我们强烈建议限制所有与HTTP连接对象的交互都在一个线程里。HttpConnection接口和子接口里唯一一个线程安全的方法是HttpConnection#shutdown()。
2.1.1. 使用阻塞式HTTP连接
HttpCore不提供开放连接的所有支持,因为建立一个新连接的过程-尤其是在客户端-是非常复杂的,它涉及到一个或多个鉴权或者隧道代理。相反,阻塞式HTTP连接可以绑定到任意的网络socket上。
Socket socket = <...> DefaultBHttpClientConnection conn = new DefaultBHttpClientConnection(8 * 1024); conn.bind(socket); System.out.println(conn.isOpen()); HttpConnectionMetrics metrics = conn.getMetrics(); System.out.println(metrics.getRequestCount()); System.out.println(metrics.getResponseCount()); System.out.println(metrics.getReceivedBytesCount()); System.out.println(metrics.getSentBytesCount()); |
不管客户端还是服务端,HTTP连接接口发送和接收消息都分为两个阶段。首先发送的是消息头。依赖消息头的属性,后面可能会跟着消息体。请注意,为了通知消息已经处理完成了,总是关闭底层的内容流式很重要的。直接从底层连接的输入流发送内容的HTTP实体必须确保完全消费消息体的内容,因为这个连接要被复用。
客户端请求执行的简化处理可能看起来是这样的:
Socket socket = <...> DefaultBHttpClientConnection conn = new DefaultBHttpClientConnection(8 * 1024); conn.bind(socket); 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 = <...> DefaultBHttpServerConnection conn = new DefaultBHttpServerConnection(8 * 1024); conn.bind(socket); 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); |
请注意,用户应该很少需要用到这些低级的方法,正常情况下应该使用更高级的服务实现代替。
2.1.2. 使用阻塞式I/O传送内容
HTTP连接使用HttpEntity接口管理内容传送的处理过程。HTTP连接生成一个实体对象,来封装输入消息的内容流。请注意HttpServerConnection#receiveRequestEntity()和HttpClientConnection#receiveResponseEntity() 不会获取或者缓存任何输入的数据。它们仅仅基于输入消息的属性注入一个合适的内容编码器。使用HttpEntity#getContent(),就可以通过读取封装实体的内容输入流来获取内容。输入的数据会自动被解码,对于数据消费者来说是完全透明的。同样地,HTTP连接依赖于HttpEntity#writeTo(OutputStream)方法产生输出消息的内容。如果输出消息封装了实体,那么基于这个消息的属性会自动对内容编码。
2.1.3. 支持的内容传送机制
HTTP连接的默认实现支持HTTP/1.1规范中定义的三种内容传送机制:
限定Content-Length:内容实体的终止由 Content-Length 头的值决定。最大的实体长度是:Long#MAX_VALUE。
恒等编码Identity coding:内容实体的终止又关闭底层连接来限定(流终止的条件)。由于显而易见的原因,恒等编码只能用于服务端。最大的实体长度是无限的。
块编码Chunk coding:以小块发送内容。最大实体长度:无限。
依赖于消息中封装的实体的属性会自动创建合适的内容流。
2.1.4. 终止HTTP连接
HTTP连接可以调用HttpConnection#close()方法优雅关闭,或者调用HttpConnection#shutdown()强制关闭。前一个方法尝试冲刷所有缓存的数据,然后终止连接,可能会不确定地阻塞。HttpConnection#close()方法不是线程安全的。后一个方法不冲刷内部的缓冲区终止连接,将控制权尽快返回给调用者而无需阻塞很长时间。HttpConnection#shutdown()方法是线程安全的。
2.2. HTTP异常处理
所有的HttpCore组件都潜在地可能抛出两种异常IOException和HttpException。如果I/O失败,例如socket超时或者重置,则抛出IOException。如果HTTP失败,例如违反HTTP协议,就会抛出HttpException。通常I/O错误都不认为是致命的,且是可恢复的,而HTTP协议错误通常被认为是致命的且是不能自动恢复。
2.2.1. 协议异常
ProtocolException 表示严重违反HTTP协议,通常导致HTTP消息的处理立刻终止。
2.3. 阻塞式HTTP协议处理器
2.3.1. HTTP服务
HttpService是服务端HTTP协议处理器,它基于阻塞式I/O模型,实现了RFC 2616中描述的服务端消息处理的基本要求。
HttpService依赖于HttpProcessor实例来生成所有输出消息的强制的协议头,适用于通用的、交叉消息转换到所有的输入和输出消息,而HTTP请求处理器只关心应用特定的内容生成和处理。
HttpProcessor httpproc = HttpProcessorBuilder.create() .add(new ResponseDate()) .add(new ResponseServer("MyServer-HTTP/1.1")) .add(new ResponseContent()) .add(new ResponseConnControl()) .build(); HttpService httpService = new HttpService(httpproc, null); |
2.3.1.1. HTTP请求处理器
HttpRequestHandler接口表示处理一组特定的HTTP请求的例行程序。HttpService被设计为关系关心协议特定的方面,而独立的请求处理器关心的是应用特定的HTTP处理。请求处理器的目的就是要生成一个具有内容实体的响应对象,并被发送回给客户端。
HttpRequestHandler myRequestHandler = new HttpRequestHandler() { public void handle( HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { response.setStatusCode(HttpStatus.SC_OK); response.setEntity( new StringEntity("some important message", ContentType.TEXT_PLAIN)); } }; |
2.3.1.2. 请求处理器解析器
HTTP请求处理器通常由HttpRequestHandlerResolver管理,每一个请求URI都匹配一个请求处理器。HttpCore实现了非常简单的请求处理器解析器,基于琐碎的模式匹配算法:HttpRequestHandlerRegistry只支持三种格式:*, <uri>* and *<uri>。
HttpProcessor httpproc = <...> HttpRequestHandler myRequestHandler1 = <...> HttpRequestHandler myRequestHandler2 = <...> HttpRequestHandler myRequestHandler3 = <...> UriHttpRequestHandlerMapper handlerMapper = new UriHttpRequestHandlerMapper(); handlerMapper.register("/service/*", myRequestHandler1); handlerMapper.register("*.do", myRequestHandler2); handlerMapper.register("*", myRequestHandler3); HttpService httpService = new HttpService(httpproc, handlerMapper); |
鼓励用户提供更加复杂的HttpRequestHandlerResolver实现,例如基于正则表达式。
2.3.1.3. 使用HTTP服务处理请求
当完全初始化并配置时,HttpService可以用于执行和处理活动的HTTP连接的请求。HttpService#handleRequest()方法读取输入的请求,并生成响应,发送回给客户端。这个方法可以在循环中执行以处理持久连接中的多个请求。HttpService#handleRequest()方法在多个线程中执行是线程安全的。这允许同时在多个连接上处理请求,只要HTTPService使用的所有协议拦截器和请求处理器是线程安全的。
HttpService httpService = <...> HttpServerConnection conn = <...> HttpContext context = <...> boolean active = true; try { while (active && conn.isOpen()) { httpService.handleRequest(conn, context); } } finally { conn.shutdown(); } |
2.3.2. HTTP请求执行器
HttpRequestExecutor是基于阻塞式I/O模型的客户端协议处理器,它实现了客户端消息处理的HTTP协议基本要求(在RFC2616中描述)。HttpRequestExecutor依赖于HttpProcessor实例,为了所有的外发消息生成强制的协议头,对于所有接收消息和外发消息应用通用的、交叉消息转换。一旦执行请求,和收到响应,应用特定的处理可以再HttpRequestExecutor外面实习。
HttpClientConnection conn = <...> HttpProcessor httpproc = HttpProcessorBuilder.create() .add(new RequestContent()) .add(new RequestTargetHost()) .add(new RequestConnControl()) .add(new RequestUserAgent("MyClient/1.1")) .add(new RequestExpectContinue(true)) .build(); HttpRequestExecutor httpexecutor = new HttpRequestExecutor(); HttpRequest request = new BasicHttpRequest("GET", "/"); HttpCoreContext context = HttpCoreContext.create(); httpexecutor.preProcess(request, httpproc, context); HttpResponse response = httpexecutor.execute(request, conn, context); httpexecutor.postProcess(response, httpproc, context); HttpEntity entity = response.getEntity(); EntityUtils.consume(entity); |
HttpRequestExecutor从多个线程同时执行是安全的。这允许同时执行多个连接的请求,只要HttpRequestExecutor使用的所有协议拦截器是线程安全的。
2.3.3. 连接持久化/复用
ConnectionReuseStrategy接口用于确定是否底层的连接可以在当前的消息处理完之后被复用,以处理将来接收的消息。默认的连接复用策略尝试尽可能地保持连接存活。首先,它会检查用于发送消息的HTTP协议版本。HTTP/1.1默认是持久连接,而c HTTP/1.0则不是。其次,它会检查Connection头的值。表明是否在对端重用连接,可以在Connection头信息中发送Keep-Alive或者Close值。之后,如果有封装实体的话,基于封装实体的属性,这个策略可以决定连接是否可安全重用。
2.4. 连接池
高效的客户端HTTP传输经常需要有效的复用持久连接。HttpCore提供了持久HTTP连接的管理池以帮助实现连接的复用处理。连接池的实现是线程安全的,目前可以被多个消费者使用。
默认情况下,连接池只允许总共20条并发连接,每一个路由2条并发连接。两条连接限制是基于HTTP规范的要求。然而,在实践中,这有很多限制。用户可以修改连接池配置,允许更多的并发连接(依赖于特定的应用上下文)。
HttpHost target = new HttpHost("localhost"); BasicConnPool connpool = new BasicConnPool(); connpool.setMaxTotal(200); connpool.setDefaultMaxPerRoute(10); connpool.setMaxPerRoute(target, 20); Future<BasicPoolEntry> future = connpool.lease(target, null); BasicPoolEntry poolEntry = future.get(); HttpClientConnection conn = poolEntry.getConnection(); |
请注意,连接池无法知道一个专线连接是否仍然在使用中。一旦连接不再使用,即使连接可以被复用,连接池的用户也要负责将连接释放回连接池。
BasicConnPool connpool = <...> Future<BasicPoolEntry> future = connpool.lease(target, null); BasicPoolEntry poolEntry = future.get(); try { HttpClientConnection conn = poolEntry.getConnection(); } finally { connpool.release(poolEntry, true); } |
连接池的状态可以在运行时查询:
HttpHost target = new HttpHost("localhost"); BasicConnPool connpool = <...> PoolStats totalStats = connpool.getTotalStats(); System.out.println("total available: " + totalStats.getAvailable()); System.out.println("total leased: " + totalStats.getLeased()); System.out.println("total pending: " + totalStats.getPending()); PoolStats targetStats = connpool.getStats(target); System.out.println("target available: " + targetStats.getAvailable()); System.out.println("target leased: " + targetStats.getLeased()); System.out.println("target pending: " + targetStats.getPending()); |
2.5. 支持TLS/SSL
阻塞连接可以绑定到任意的socket上。这使得SSL非常容易。任何SSLSocket实例都可以绑定到一个阻塞连接上,以实现所有的消息都通过安全的TLS/SSL连接传送。
SSLContext sslcontext = SSLContexts.createSystemDefault(); SocketFactory sf = sslcontext.getSocketFactory(); SSLSocket socket = (SSLSocket) sf.createSocket("somehost", 443); // Enforce TLS and disable SSL socket.setEnabledProtocols(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }); // Enforce strong ciphers socket.setEnabledCipherSuites(new String[] { "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_DSS_WITH_AES_256_CBC_SHA" }); DefaultBHttpClientConnection conn = new DefaultBHttpClientConnection(8 * 1204); conn.bind(socket); |
2.6. 内嵌的HTTP服务端
从4.4版本开始,HttpCore装配了一个基于上述的阻塞I/O组件的内嵌HTTP服务端。
HttpRequestHandler requestHandler = <...> HttpProcessor httpProcessor = <...> SocketConfig socketConfig = SocketConfig.custom() .setSoTimeout(15000) .setTcpNoDelay(true) .build(); final HttpServer server = ServerBootstrap.bootstrap() .setListenerPort(8080) .setHttpProcessor(httpProcessor) .setSocketConfig(socketConfig) .setExceptionLogger(new StdErrorExceptionLogger()) .registerHandler("*", requestHandler) .create(); server.start(); server.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { server.shutdown(5, TimeUnit.SECONDS); } }); |