Tomcat剖析(四):Tomcat默认连接器(2)

第一部分:概述

这一节基于《深度剖析Tomcat》第四章:Tomcat的默认连接器 总结而成

最好先到我的github上下载本书相关的代码,同时去上网下载这本书。

经过上一节的讲解,你已经理解了请求和响应对象,并且知道 HttpConnector 对象是如何创建它们的。Tomcat通过多个HttpProcessor实例实现多请求的实例,算是拥有一个比较完整的连接器了。

这一节,我们看看Tomcat中默认连接器中剩下的未讲的功能:处理请求

这一节补充的内容有些内容是从书上copy下来的。

说到请求和响应,当然先要了解请求对象和响应对象

  • 默认连接器里 HTTP 请求对象实现org.apache.catalina.Request 接口。这个接口被类RequestBase 直接实现了,也是 HttpRequest 的父接口。最终的实现是继承于 HttpRequest 的HttpRequestImpl。像第 3 节的一样,有几个 facade 类:RequestFacade 和 HttpRequestFacade。需要注意的是,这里除了属于 javax.servlet和 javax.servlet.http 包的类,前缀 org.apache.catalina 已经被省略了。对HTTP响应对象也是类似的。

为什么要用Façade类?在第2节的安全性问题上已经说明。而这个类层次结构还是比较容易理解的。

图有些模糊,但还是可以看的。

在这节中我们关注 HttpProcessor 类的 process 方法。 处理请求是通过HttpProcessor类的process方法实现。 它是一个套接字赋给它之后,在 HttpProcessor 类的 run 方法中调用的,而run方法在处理器对象创建时就调用了,只是在等待请求。

process 方法的工作:

  • 解析连接
  • 解析请求
  • 解析头部

第二部分:代码讲解

处理请求

和第3节的一样,有一个 SocketInputStream 实例用来包装套接字的输入流。注意的是,SocketInputStream 的 构 造 方 法 同 样 传 递 了 从 连 接 器 获 得 的 缓 冲 区 大 小 , 而 不 是 从HttpProcessor 的本地变量获得(第3节中是指定的2048)。这是因为对于默认连接器的用户而言,HttpProcessor 是不可访问的。通过传递 Connector 接口的缓冲区大小,这就使得使用连接器的任何人都可以设置缓冲大小。

  1. SocketInputStream input = null;
  2. OutputStream output = null;
  3. // Construct and initialize the objects we will need
  4. try {
  5. input = new SocketInputStream(socket.getInputStream(),
  6. connector.getBufferSize());
  7. } catch (Exception e) {
  8. log("process.create", e);
  9. ok = false;
  10. }

process 方法使用布尔变量 ok 来指代在处理过程中是否发现错误,从代码中可以看到一旦catch到错误,就会设为false 并使用布尔变量finishResponse 来指代 Response 接口中的 finishResponse 方法是否应该被调用。

  1. boolean ok = true;
  2. boolean finishResponse = true;

另外, process 方法也使用了布尔变量 keepAlive,stopped 和 http11。 keepAlive 表示连接 是否是持久的, stopped 表示 HttpProcessor 实例是否已经被连接器终止来确认 process 是否也应该停止,http11 表示 从 web 客户端过来的 HTTP 请求是否支持 HTTP 1.1。

然后,有个 while 循环用来保持从输入流中读取,直到 HttpProcessor 被停止,一个异常被抛出或者连接给关闭为止。

  1. while (!stopped && ok && keepAlive) {
  2. //....
  3. }

在 while 循环的内部,process 方法首先把 finishResponse 设置为 true,并获得输出流,并对请求和响应对象做些初始化处理

如果初始化过程都catch到错误,解析连接和头部就不用做了,所以抛错时ok会设为false

  1. //初始化请求和响应对象
  2. request.setStream(input);
  3. request.setResponse(response);
  4. output = socket.getOutputStream();
  5. response.setStream(output);
  6. response.setRequest(request);
  7. ((HttpServletResponse) response.getResponse()).setHeader
  8. ("Server", SERVER_INFO);

接着,process 方法通过调用 parseConnection,parseRequest 和 parseHeaders 方法开始解析前来的 HTTP 请求,这些方法将在这节的后面讨论。

  1. parseConnection(socket);
  2. parseRequest(input, output);
  3. if (!request.getRequest().getProtocol()
  4. .startsWith("HTTP/0"))
  5. 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

  1. if (http11) {
  2. // Sending a request acknowledge back to the client if
  3. // requested.
  4. ackRequest(output);
  5. // If the protocol is HTTP/1.1, chunking is allowed.
  6. if (connector.isChunkingAllowed())
  7. response.setAllowChunking(true);
  8. }

在解析 HTTP 请求的过程中,有可能会抛出异常。任何异常将会把 ok 或者 finishResponse设置为 false。

解析过后,process 方法把请求和响应对象传递给容器的 invoke 方法

  1. ((HttpServletResponse) response).setHeader
  2. ("Date", FastHttpDateFormat.getCurrentDate());
  3. if (ok) {
  4. connector.getContainer().invoke(request, response);
  5. }

还记得上一节的ex04.pyrmont.startup.Bootstrap.java吗?

里面有这么一段:

  1. SimpleContainer container = new SimpleContainer();
  2. connector.setContainer(container);

SimpleContainer是什么? 它实现了Container接口,通过HttpConnector解析出请求和响应后传递给容器

容器通过请求和响应对象获得servletName,负责servlet的加载执行,第4节只是简单实现了invoke方法,其他方法未实现。

对应前面几节的ServletProcessor.java

所以为什么要有Container呢,就是将servlet加载执行过程分离出来,当然实际不止那么简单。

  1. public class SimpleContainer implements Container{
  2. public void invoke(Request request, Response response) throws IOException,
  3. ServletException {
  4. String servletName = ((HttpServletRequest) request).getRequestURI();
  5. servletName = servletName.substring(servletName.lastIndexOf("/") + 1);
  6. URLClassLoader loader = null;
  7. try {
  8. URL[] urls = new URL[1];
  9. URLStreamHandler streamHandler = null;
  10. File classPath = new File(WEB_ROOT);
  11. String repository = (new URL("file", null,
  12. classPath.getCanonicalPath() + File.separator)).toString();
  13. urls[0] = new URL(null, repository, streamHandler);
  14. loader = new URLClassLoader(urls);
  15. } catch (IOException e) {
  16. System.out.println(e.toString());
  17. }
  18. Class myClass = null;
  19. try {
  20. myClass = loader.loadClass(servletName);
  21. } catch (ClassNotFoundException e) {
  22. System.out.println(e.toString());
  23. }
  24. Servlet servlet = null;
  25. try {
  26. servlet = (Servlet) myClass.newInstance();
  27. servlet.service((HttpServletRequest) request,
  28. (HttpServletResponse) response);
  29. } catch (Exception e) {
  30. System.out.println(e.toString());
  31. } catch (Throwable e) {
  32. System.out.println(e.toString());
  33. }
  34. }
  35. }

接着,如果 finishResponse 仍然是 true响应对象的 finishResponse 方法和请求对象的finishRequest 方法将被调用,并且结束输出。 (只要能够正常解析就是true,状态码是在执行servlet过程中修改的,默认是200)

finishResponse处理的是判断响应状态,如果是正常的,则setHeader("Connection", "close"); 否则会将错误写到到页面上,可以自己查看代码

finishRequest主要就是关闭HttRequest中的流

  1. if (finishResponse) {
  2. //...
  3. response.finishResponse();
  4. //...
  5. request.finishRequest();
  6. }

while 循环的最后一部分检查响应的 Connection 头部是否已经在 servlet 内部设为 close,或者协议是 HTTP1.0.如果是这种情况的话,keepAlive 设置为 false。同样,请求和响应对象接着会被回收利用。

进入这两个方法,可以看到其实就是将HttpRequestImpl.java和它的基类HttpRquestBase等类的实例变量还原到原来的值,这样在下次请求如果再从处理器池中拿到这个处理器时,保证里面的请求和响应对象是初始值。

  1. if ( "close".equals(response.getHeader("Connection")) ) {
  2. keepAlive = false;
  3. }
  4. // End of request processing
  5. status = Constants.PROCESSOR_IDLE;
  6. // Recycling the request and the response objects
  7. request.recycle();
  8. response.recycle();

在这个场景中,如果 keepAlive 是 true 的话,while 循环将会在开头就启动。因为在前面的解析过程中和容器的 invoke 方法中没有出现错误,或者 HttpProcessor 实例没有被停止。否则,shutdownInput 方法将会调用,而套接字将被关闭.

  1. try {
  2. shutdownInput(input);
  3. socket.close();

shutdownInput 方法检查是否有未读取的字节。如果有的话,跳过那些字节。

解析连接

parseConnection 方法从套接字中获取到网络地址并把它赋予 HttpRequestImpl 对象。

它也检查是否使用代理并把套接字赋予请求对象。

  1. private void parseConnection(Socket socket)
  2. throws IOException, ServletException {
  3. ((HttpRequestImpl) request).setInet(socket.getInetAddress());
  4. if (proxyPort != 0)
  5. request.setServerPort(proxyPort);
  6. else
  7. request.setServerPort(serverPort);
  8. request.setSocket(socket);
  9. }

解析请求

parseRequest 方法是第 3 节中类似方法的完整版本。如果你阅读过上一节,你通过阅读这个方法应该可以理解这个方法是怎么运行的。

解析头部

默认链接器的 parseHeaders 方法使用包 org.apache.catalina.connector.http 里边的HttpHeader 和 DefaultHeaders 类。类 HttpHeader 指代一个 HTTP 请求头部。类 HttpHeader 不是像第3节那样使用字符串,而是使用字符数据用来避免昂贵的字符串操作。类 DefaultHeaders是一个 final 类,在字符数组中包含了标准的 HTTP 请求头部

下面是DefaultHeaders.java部分代码

  1. static final char[] AUTHORIZATION_NAME = "authorization".toCharArray();
  2. static final char[] ACCEPT_LANGUAGE_NAME = "accept-language".toCharArray();
  3. static final char[] COOKIE_NAME = "cookie".toCharArray();
  4. static final char[] CONTENT_LENGTH_NAME = "content-length".toCharArray();

parseHeaders 方法包含一个 while 循环,可以持续读取 HTTP 请求直到再也没有更多的头部可以读取到。

while 循环首先调用请求对象的 allocateHeader 方法来获取一个空的 HttpHead 实例,如果看这个方法,发现HttpRequestImpl中以HttpHeader数组形式保存,如果,默认规定头部大小为10个,如果超过,则通过复制给新数组实现新Header对象的分配。

这个实例被传递给SocketInputStream 的 readHeader 方法。

  1. HttpHeader header = request.allocateHeader();
  2. // Read the next header
  3. input.readHeader(header);

假如所有的头部都被已经被读取的话, readHeader 方法将不会赋值给 HttpHeader 实例,这个时候 parseHeaders 方法将会返回。

  1. if (header.nameEnd == 0) {
  2. if (header.valueEnd == 0) {
  3. return;
  4. } else {
  5. throw new ServletException
  6. (sm.getString("httpProcessor.parseHeaders.colon"));
  7. }
  8. }

如果存在一个头部的名称的话,这里必须同样会有一个头部的值:String value = new String(header.value, 0, header.valueEnd); 接下去,像第 3 节那样, parseHeaders 方法将会把头部名称和 DefaultHeaders 里边的名称做对比。

注意的是,这样的对比是基于两个字符数组之间,而不是两个字符串之间的。

  1. if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) {
  2. request.setAuthorization(value);
  3. } else if (header.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) {
  4. parseAcceptLanguage(value);
  5. //...

第三部分:小结

这一个补充的其实就是为了将整个默认连接器的内容比较完整的过一遍, 理解一下就行。当然,自己觉得有用的也可以吸收。

下一节开始讲容器。

相应代码可以在我的github上找到下载,拷贝到eclipse,然后打开对应包的代码即可。

如发现编译错误,可能是由于jdk不同版本对编译的要求不同导致的,可以不管,供学习研究使用。

Tomcat剖析(四):Tomcat默认连接器(2)的更多相关文章

  1. 攻城狮在路上(伍)How tomcat works(四)Tomcat的默认连接器

     在第4章中将通过剖析Tomcat4的默认连接器的代码,讨论需要什么来创建一个真实的Tomcat连接器.     注意:本章中提及的“默认连接器”是指Tomcat4的默认连接器.即使默认的连机器已经被 ...

  2. Tomcat剖析(四):Tomcat默认连接器(1)

    Tomcat剖析(四):Tomcat默认连接器(1) 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三): ...

  3. how tomcat works 读书笔记四 tomcat的默认连接器

    事实上在第三章,就已经有了连接器的样子了,只是那仅仅是一个学习工具,在这一章我们会開始分析tomcat4里面的默认连接器. 连接器 Tomcat连接器必须满足下面几个要求 1 实现org.apache ...

  4. Tomcat剖析(三):连接器(2)

    Tomcat剖析(三):连接器(2) 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器(1) 4 ...

  5. Tomcat剖析(三):连接器(1)

    Tomcat剖析(三):连接器(1) 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器(1) 4 ...

  6. tomcat源码阅读之默认连接器

    默认连接器 一.UML图: 1.所有的连接器都要实现Connector接口,必须创建Request对象和Response对象,httpConnector作为默认连接器,肯定也是要实现Connector ...

  7. Tomcat剖析(二):一个简单的Servlet服务器

    Tomcat剖析(二):一个简单的Servlet服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三) ...

  8. Tomcat剖析(一):一个简单的Web服务器

    Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...

  9. Tomcat剖析(五):Tomcat 容器

    Tomcat剖析(五):Tomcat 容器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器(1 ...

随机推荐

  1. 1.cocos2dx 3.2环境结构

    1        所需软件 jdk-7u25-windows-i586.exe python-2.7.8.amd64.msi cocos2d-x-3.2.zip apache-ant-1.9.4.zi ...

  2. Windows Auzre 微软的云计算产品的后台操作界面

    Windows Auzre 微软的云计算产品的后台操作界面,试用期,相比于阿里云后台操作不是人. watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTmFvbG ...

  3. 概率统计(DP)

    问题叙述性说明 生成n个月∈[a,b]随机整数.并且将它们输出到x概率. 输入格式 输入线跟四个整数n.a,b,x,用空格分隔. 输出格式 输出一行包括一个小数位和为x的概率.小数点后保留四位小数 例 ...

  4. POJ 2155 Matrix (D区段树)

    http://poj.org/problem?id=2155 Matrix Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 1 ...

  5. ASP.NET 5 Beta8 发布

    ASP.NET 5 Beta8 发布 ASP.NET 5 的路线图(详见 ASP.NET 5 Schedule and Roadmap : https://github.com/aspnet/home ...

  6. linux下一个php未找到php型材php.ini解决方案

    编译并安装自己php经常会遇到这样的问题.我找不到php.ini.对于根据下面的方法可以解决例: 首先是需要解释.假设你php它被编译并安装,那么默认是没有php.ini的,你必须得去复制源代码包内. ...

  7. 它们的定义TextView使之具有跑马灯的效果

    一.引入问题 使用通用textview快乐效应,焦点事件不启动滚动,button目前的焦点事件,但丑,因此,需要定制TextView 天生焦点 个textview FocusedTextView.ja ...

  8. [LeetCode] Longest Palindrome Substring 具体分析

    Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...

  9. S关于使用QL声明 找出同时满足多个tag拍摄条件设置算法

    表结构 Tag Table:{tag_id, tag_name}  #标签表 News Table:{news_id, title,......}  #新闻列表 NewsTags Table:{tag ...

  10. hibernate几个纯sql查询

    几个hibernate纯SQL询问,查询和表没有映射pojo实体和表有映射关系的实体...... 1.getSession().createSQLQuery(sql).query.addScalar( ...