这一节基于《深度剖析Tomcat》第四章:Tomcat的默认链接器 总结而成html
最好先到个人github上下载本书相关的代码,同时去上网下载这本书。java
通过上一节的讲解,你已经理解了请求和响应对象,而且知道 HttpConnector 对象是如何建立它们的。Tomcat经过多个HttpProcessor实例实现多请求的实例,算是拥有一个比较完整的链接器了。git
这一节,咱们看看Tomcat中默认链接器中剩下的未讲的功能:处理请求。github
这一节补充的内容有些内容是从书上copy下来的。web
说到请求和响应,固然先要了解请求对象和响应对象apache
为何要用Façade类?在第2节的安全性问题上已经说明。而这个类层次结构仍是比较容易理解的。数组
图有些模糊,但仍是能够看的。安全
在这节中咱们关注 HttpProcessor 类的 process 方法。 处理请求是经过HttpProcessor类的process方法实现。 它是一个套接字赋给它以后,在 HttpProcessor 类的 run 方法中调用的,而run方法在处理器对象建立时就调用了,只是在等待请求。服务器
process 方法的工做:cookie
和第3节的同样,有一个 SocketInputStream 实例用来包装套接字的输入流。注意的是,SocketInputStream 的 构 造 方 法 同 样 传 递 了 从 连 接 器 获 得 的 缓 冲 区 大 小 , 而 不 是 从HttpProcessor 的本地变量得到(第3节中是指定的2048)。这是由于对于默认链接器的用户而言,HttpProcessor 是不可访问的。经过传递 Connector 接口的缓冲区大小,这就使得使用链接器的任何人均可以设置缓冲大小。
SocketInputStream input = null; OutputStream output = null; // Construct and initialize the objects we will need try { input = new SocketInputStream(socket.getInputStream(), connector.getBufferSize()); } catch (Exception e) { log("process.create", e); ok = false; }
process 方法使用布尔变量 ok 来指代在处理过程当中是否发现错误,从代码中能够看到一旦catch到错误,就会设为false 并使用布尔变量finishResponse 来指代 Response 接口中的 finishResponse 方法是否应该被调用。
boolean ok = true; boolean finishResponse = true;
另外, process 方法也使用了布尔变量 keepAlive,stopped 和 http11。 keepAlive 表示链接 是不是持久的, stopped 表示 HttpProcessor 实例是否已经被链接器终止来确认 process 是否也应该中止,http11 表示 从 web 客户端过来的 HTTP 请求是否支持 HTTP 1.1。
而后,有个 while 循环用来保持从输入流中读取,直到 HttpProcessor 被中止,一个异常被抛出或者链接给关闭为止。
while (!stopped && ok && keepAlive) { //.... }
在 while 循环的内部,process 方法首先把 finishResponse 设置为 true,并得到输出流,并对请求和响应对象作些初始化处理。
若是初始化过程都catch到错误,解析链接和头部就不用作了,因此抛错时ok会设为false
//初始化请求和响应对象 request.setStream(input); request.setResponse(response); output = socket.getOutputStream(); response.setStream(output); response.setRequest(request); ((HttpServletResponse) response.getResponse()).setHeader ("Server", SERVER_INFO);
接着,process 方法经过调用 parseConnection,parseRequest 和 parseHeaders 方法开始解析前来的 HTTP 请求,这些方法将在这节的后面讨论。
parseConnection(socket); parseRequest(input, output); if (!request.getRequest().getProtocol() .startsWith("HTTP/0")) parseHeaders(input);
parseConnection 方法得到协议的值,像 HTTP0.9, HTTP1.0 或 HTTP1.1。
若是协议是 HTTP1.0,keepAlive 设置为 false,由于 HTTP1.0 不支持持久链接。
若是在 HTTP 请求里边找到 Expect: 100-continue 的头部信息,则 parseHeaders 方法将把 sendAck 设置为 true。
若是协议是 HTTP1.1,而且 web 客户端发送头部 Expect: 100-continue 的话,经过调用ackRequest 方法它将响应这个头部。它将会测试组块是不是容许的。
getProtocol()获取的协议值是在parseConnection时设置的
ackRequest 方法测试 sendAck 的值,并在 sendAck 为 true 的时候发送下面的字符串:HTTP/1.1 100 Continue\r\n\r\n
if (http11) { // Sending a request acknowledge back to the client if // requested. ackRequest(output); // If the protocol is HTTP/1.1, chunking is allowed. if (connector.isChunkingAllowed()) response.setAllowChunking(true); }
在解析 HTTP 请求的过程当中,有可能会抛出异常。任何异常将会把 ok 或者 finishResponse设置为 false。
在解析事后,process 方法把请求和响应对象传递给容器的 invoke 方法:
((HttpServletResponse) response).setHeader ("Date", FastHttpDateFormat.getCurrentDate()); if (ok) { connector.getContainer().invoke(request, response); }
还记得上一节的ex04.pyrmont.startup.Bootstrap.java吗?
里面有这么一段:
SimpleContainer container = new SimpleContainer(); connector.setContainer(container);
SimpleContainer是什么? 它实现了Container接口,经过HttpConnector解析出请求和响应后传递给容器
容器经过请求和响应对象得到servletName,负责servlet的加载执行,第4节只是简单实现了invoke方法,其余方法未实现。
对应前面几节的ServletProcessor.java
因此为何要有Container呢,就是将servlet加载执行过程分离出来,固然实际不止那么简单。
public class SimpleContainer implements Container{ public void invoke(Request request, Response response) throws IOException, ServletException { String servletName = ((HttpServletRequest) request).getRequestURI(); servletName = servletName.substring(servletName.lastIndexOf("/") + 1); URLClassLoader loader = null; try { URL[] urls = new URL[1]; URLStreamHandler streamHandler = null; File classPath = new File(WEB_ROOT); String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString(); urls[0] = new URL(null, repository, streamHandler); loader = new URLClassLoader(urls); } catch (IOException e) { System.out.println(e.toString()); } Class myClass = null; try { myClass = loader.loadClass(servletName); } catch (ClassNotFoundException e) { System.out.println(e.toString()); } Servlet servlet = null; try { servlet = (Servlet) myClass.newInstance(); servlet.service((HttpServletRequest) request, (HttpServletResponse) response); } catch (Exception e) { System.out.println(e.toString()); } catch (Throwable e) { System.out.println(e.toString()); } } }
接着,若是 finishResponse 仍然是 true响应对象的 finishResponse 方法和请求对象的finishRequest 方法将被调用,而且结束输出。 (只要可以正常解析就是true,状态码是在执行servlet过程当中修改的,默认是200)
finishResponse处理的是判断响应状态,若是是正常的,则setHeader("Connection", "close"); 不然会将错误写到到页面上,能够本身查看代码
finishRequest主要就是关闭HttRequest中的流
if (finishResponse) { //... response.finishResponse(); //... request.finishRequest(); }
while 循环的最后一部分检查响应的 Connection 头部是否已经在 servlet 内部设为 close,或者协议是 HTTP1.0.若是是这种状况的话,keepAlive 设置为 false。一样,请求和响应对象接着会被回收利用。
进入这两个方法,能够看到其实就是将HttpRequestImpl.java和它的基类HttpRquestBase等类的实例变量还原到原来的值,这样在下次请求若是再从处理器池中拿到这个处理器时,保证里面的请求和响应对象是初始值。
if ( "close".equals(response.getHeader("Connection")) ) { keepAlive = false; } // End of request processing status = Constants.PROCESSOR_IDLE; // Recycling the request and the response objects request.recycle(); response.recycle();
在这个场景中,若是 keepAlive 是 true 的话,while 循环将会在开头就启动。由于在前面的解析过程当中和容器的 invoke 方法中没有出现错误,或者 HttpProcessor 实例没有被中止。不然,shutdownInput 方法将会调用,而套接字将被关闭.
try { shutdownInput(input); socket.close();
shutdownInput 方法检查是否有未读取的字节。若是有的话,跳过那些字节。
parseConnection 方法从套接字中获取到网络地址并把它赋予 HttpRequestImpl 对象。
它也检查是否使用代理并把套接字赋予请求对象。
private void parseConnection(Socket socket) throws IOException, ServletException { ((HttpRequestImpl) request).setInet(socket.getInetAddress()); if (proxyPort != 0) request.setServerPort(proxyPort); else request.setServerPort(serverPort); request.setSocket(socket); }
parseRequest 方法是第 3 节中相似方法的完整版本。若是你阅读过上一节,你经过阅读这个方法应该能够理解这个方法是怎么运行的。
默认连接器的 parseHeaders 方法使用包 org.apache.catalina.connector.http 里边的HttpHeader 和 DefaultHeaders 类。类 HttpHeader 指代一个 HTTP 请求头部。类 HttpHeader 不是像第3节那样使用字符串,而是使用字符数据用来避免昂贵的字符串操做。类 DefaultHeaders是一个 final 类,在字符数组中包含了标准的 HTTP 请求头部
下面是DefaultHeaders.java部分代码
static final char[] AUTHORIZATION_NAME = "authorization".toCharArray(); static final char[] ACCEPT_LANGUAGE_NAME = "accept-language".toCharArray(); static final char[] COOKIE_NAME = "cookie".toCharArray(); static final char[] CONTENT_LENGTH_NAME = "content-length".toCharArray();
parseHeaders 方法包含一个 while 循环,能够持续读取 HTTP 请求直到再也没有更多的头部能够读取到。
while 循环首先调用请求对象的 allocateHeader 方法来获取一个空的 HttpHead 实例,若是看这个方法,发现HttpRequestImpl中以HttpHeader数组形式保存,若是,默认规定头部大小为10个,若是超过,则经过复制给新数组实现新Header对象的分配。
这个实例被传递给SocketInputStream 的 readHeader 方法。
HttpHeader header = request.allocateHeader(); // Read the next header input.readHeader(header);
假如全部的头部都被已经被读取的话, readHeader 方法将不会赋值给 HttpHeader 实例,这个时候 parseHeaders 方法将会返回。
if (header.nameEnd == 0) { if (header.valueEnd == 0) { return; } else { throw new ServletException (sm.getString("httpProcessor.parseHeaders.colon")); } }
若是存在一个头部的名称的话,这里必须一样会有一个头部的值:String value = new String(header.value, 0, header.valueEnd); 接下去,像第 3 节那样, parseHeaders 方法将会把头部名称和 DefaultHeaders 里边的名称作对比。
注意的是,这样的对比是基于两个字符数组之间,而不是两个字符串之间的。
if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) { request.setAuthorization(value); } else if (header.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) { parseAcceptLanguage(value); //...
这一个补充的其实就是为了将整个默认链接器的内容比较完整的过一遍, 理解一下就行。固然,本身以为有用的也能够吸取。
下一节开始讲容器。
附
相应代码能够在个人github上找到下载,拷贝到eclipse,而后打开对应包的代码便可。
如发现编译错误,多是因为jdk不一样版本对编译的要求不一样致使的,能够无论,供学习研究使用。