How Tomcat Works - Connector
在前两章实现的WebServer还有很多问题,比如:
1)最后一个out.print("xxx")没有生效。
2)没有解析请求头,请求方法,协议,uri,参数等,而这些内容在servlet里面是需要用到的。
在这一章中,增加了一个新的servlet:ModerServlet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ModernServlet extends HttpServlet { public void init(ServletConfig config) { System.out.println( "ModernServlet -- init" ); } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType( "text/html" ); PrintWriter out = response.getWriter(); out.println( "<html>" ); out.println( "<head>" ); out.println( "<title>Modern Servlet</title>" ); out.println( "</head>" ); out.println( "<body>" ); out.println( "<h2>Headers</h2" ); Enumeration headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String header = (String) headers.nextElement(); out.println( "<br>" + header + " : " + request.getHeader(header)); } out.println( "<br><h2>Method</h2" ); out.println( "<br>" + request.getMethod()); out.println( "<br><h2>Parameters</h2" ); Enumeration parameters = request.getParameterNames(); while (parameters.hasMoreElements()) { String parameter = (String) parameters.nextElement(); out.println( "<br>" + parameter + " : " + request.getParameter(parameter)); } out.println( "<br><h2>Query String</h2" ); out.println( "<br>" + request.getQueryString()); out.println( "<br><h2>Request URI</h2" ); out.println( "<br>" + request.getRequestURI()); out.println( "</body>" ); out.println( "</html>" ); } } |
这个Servlet和之前两章的PrimitiveServlet不同的是,这个Servlet是继承了HttpServlet而不是实现了Servlet接口。HttpServlet又继承了GenericServlet,实现了Servlet, ServletConfig这两个接口。所以ModerServlet也实现了Servlet接口,而且除此之外还复用了基类的功能。
WebServer的启动和业务逻辑分成了三个类:Bootstrap,HttpProcessor和HttpConnector。HttpConnector实现了Runnable接口,这样就可以把自身的实例通过线程去去执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class HttpConnector implements Runnable { boolean stopped; private String scheme = "http" ; public String getScheme() { return scheme; } public void start() { Thread thread = new Thread( this ); thread.start(); } @Override public void run() { ServerSocket serverSocket = null ; int port = 8080 ; try { serverSocket = new ServerSocket(port, 1 , InetAddress.getByName( "127.0.0.1" )); } catch (IOException e) { e.printStackTrace(); System.exit( 1 ); } while (!stopped) { Socket socket = null ; try { socket = serverSocket.accept(); } catch (IOException e) { continue ; } HttpProcessor processor = new HttpProcessor( this ); processor.process(socket); } } } |
这部分逻辑和前两章类似,除了新增了一个HttpProcessor,这个类是处理http请求的关键部分。首先来看process函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public void process(Socket socket) { SocketInputStream input = null ; OutputStream output = null ; try { input = new SocketInputStream(socket.getInputStream(), 2048 ); output = socket.getOutputStream(); // create HttpRequest object and parse request = new HttpRequest(input); // create HttpResponse object response = new HttpResponse(output); response.setRequest(request); response.setHeader( "Server" , "Pyrmont Servlet Container" ); parseRequest(input, output); parseHeaders(input); //check if this is a request for a servlet or a static resource //a request for a servlet begins with "/servlet/" if (request.getRequestURI().startsWith( "/servlet/" )) { ServletProcessor processor = new ServletProcessor(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); } // Close the socket socket.close(); // no shutdown for this application } catch (Exception e) { e.printStackTrace(); } } |
和HttpServer不一样的是:
1)input类型又InputStream变成了SocketInputStream,这个类tomcat4里面对InputStream的封装类,在现在的tomcat源代码里面这个类已经被移除了。这里直接把这个类的源代码和它依赖的HttpRequestLine和HttpHeader拷贝进来了。同时SocketInputStream这个类还使用了org.apache.catalina.util.RequestUtil 和org.apache.catalina.util.StringManager这两个不包含在源代码里的类,可以去这里下载http://archive.apache.org/dist/tomcat/tomcat-4/v4.1.40/bin/ tomcat的zip版本,在server/lib下找到catalina.jar并加到classpath里面。
2)增加了解析request和header的逻辑:
1 2 3 4 | response.setHeader( "Server" , "Pyrmont Servlet Container" ); parseRequest(input, output); parseHeaders(input) |
parseRequest就是解析Http请求的第一部分,比如:
1 | GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/ 1.1 |
然后把解析到的结果保存到HttpRequest对象中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | private void parseRequest(SocketInputStream input, OutputStream output) throws IOException, ServletException { // Parse the incoming request line input.readRequestLine(requestLine);<br> // http请求方法,这里是GET String method = new String(requestLine.method, 0 , requestLine.methodEnd); String uri = null ;<br> // http请求协议,这里是http/1.1 String protocol = new String(requestLine.protocol, 0 , requestLine.protocolEnd); // Validate the incoming request line if (method.length() < 1 ) { throw new ServletException( "Missing HTTP request method" ); } else if (requestLine.uriEnd < 1 ) { throw new ServletException( "Missing HTTP request URI" ); }<br> // 解析问号后面的部分,比如/myApp/ModernServlet?userName=tarzan&password=pwd 解析之后uri成了/myApp/ModernServlet<br> // ,userName=tarzan&password=pwd保存在HttpRequest的queryString中 // Parse any query parameters out of the request URI int question = requestLine.indexOf( "?" ); if (question >= 0 ) { request.setQueryString( new String(requestLine.uri, question + 1 , requestLine.uriEnd - question - 1 )); uri = new String(requestLine.uri, 0 , question); } else { request.setQueryString( null ); uri = new String(requestLine.uri, 0 , requestLine.uriEnd); } // 如果uri是一个绝对路径,比如http://baidu.com/index.html?abc=xyz则把http://baidu.com这一部分去掉 // Checking for an absolute URI (with the HTTP protocol) if (!uri.startsWith( "/" )) { int pos = uri.indexOf( "://" ); // Parsing out protocol and host name if (pos != - 1 ) { pos = uri.indexOf( '/' , pos + 3 ); if (pos == - 1 ) { uri = "" ; } else { uri = uri.substring(pos); } } } // 从uri里解析jsessionid,如果有的话 // Parse any requested session ID out of the request URI String match = ";jsessionid=" ; int semicolon = uri.indexOf(match); if (semicolon >= 0 ) { String rest = uri.substring(semicolon + match.length()); int semicolon2 = rest.indexOf( ';' ); if (semicolon2 >= 0 ) { request.setRequestedSessionId(rest.substring( 0 , semicolon2)); rest = rest.substring(semicolon2); } else { request.setRequestedSessionId(rest); rest = "" ; } request.setRequestedSessionURL( true ); uri = uri.substring( 0 , semicolon) + rest; } else { request.setRequestedSessionId( null ); request.setRequestedSessionURL( false ); } // 规范化uri的内容 // Normalize URI (using String operations at the moment) String normalizedUri = normalize(uri); // Set the corresponding request properties ((HttpRequest) request).setMethod(method); request.setProtocol(protocol); if (normalizedUri != null ) { ((HttpRequest) request).setRequestURI(normalizedUri); } else { ((HttpRequest) request).setRequestURI(uri); } if (normalizedUri == null ) { throw new ServletException( "Invalid URI: " + uri + "'" ); } } |
parseHeaders通过一个while循环,每次通过SocketInputStream读入一个header项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | private void parseHeaders(SocketInputStream input) throws IOException, ServletException { while ( true ) { HttpHeader header = new HttpHeader(); ; // Read the next header input.readHeader(header); if (header.nameEnd == 0 ) { if (header.valueEnd == 0 ) { return ; } else { throw new ServletException (sm.getString( "httpProcessor.parseHeaders.colon" )); } } String name = new String(header.name, 0 , header.nameEnd); String value = new String(header.value, 0 , header.valueEnd); request.addHeader(name, value); // do something for some headers, ignore others. if (name.equals( "cookie" )) { Cookie cookies[] = RequestUtil.parseCookieHeader(value); for ( int i = 0 ; i < cookies.length; i++) { if (cookies[i].getName().equals( "jsessionid" )) { // Override anything requested in the URL if (!request.isRequestedSessionIdFromCookie()) { // Accept only the first session id cookie request.setRequestedSessionId(cookies[i].getValue()); request.setRequestedSessionCookie( true ); request.setRequestedSessionURL( false ); } } request.addCookie(cookies[i]); } } else if (name.equals( "content-length" )) { int n = - 1 ; try { n = Integer.parseInt(value); } catch (Exception e) { throw new ServletException(sm.getString( "httpProcessor.parseHeaders.contentLength" )); } request.setContentLength(n); } else if (name.equals( "content-type" )) { request.setContentType(value); } } //end while } |
如果header项的是cookie,使用RequestUtil.parseCookieHeader去解析cookie的值,如果cookie的值中包含jsessionid,就执行:
1 2 3 4 5 6 7 | // Override anything requested in the URL if (!request.isRequestedSessionIdFromCookie()) { // Accept only the first session id cookie request.setRequestedSessionId(cookies[i].getValue()); request.setRequestedSessionCookie( true ); request.setRequestedSessionURL( false ); } |
只有request.isRequestedSessionIdFromCookie()返回false的时候才保存jsessionid的值到sessionid,但是什么时候会返回false,我们可以看下这个接口的声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * * Checks whether the requested session ID came in as a cookie. * * @return <code>true</code> if the session ID * came in as a * cookie; otherwise, <code>false</code> * * * @see #getSession * */ public boolean isRequestedSessionIdFromCookie(); |
看起来是说sessionid是否已经从cookie中拿到了,再配合评论// Accept only the first session id cookie 所以我们知道就是一个标记位,在request.setRequestedSessionCookie(true);就是设成了true就是说只接受第一个sessionid。我们也可以去看下tommcat的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * Return <code>true</code> if the session identifier included in this * request came from a cookie. */ @Override public boolean isRequestedSessionIdFromCookie() { if (requestedSessionId == null ) { return false ; } return requestedSessionCookie; } |
然后我们可以看到请求参数的解析是在HttpRequest的parseParameters里面做的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | protected void parseParameters() { if (parsed) return ; ParameterMap results = parameters; if (results == null ) results = new ParameterMap(); results.setLocked( false ); String encoding = getCharacterEncoding(); if (encoding == null ) encoding = "ISO-8859-1" ; // Parse any parameters specified in the query string String queryString = getQueryString(); try { RequestUtil.parseParameters(results, queryString, encoding); } catch (UnsupportedEncodingException e) { ; } // Parse any parameters specified in the input stream String contentType = getContentType(); if (contentType == null ) contentType = "" ; int semicolon = contentType.indexOf( ';' ); if (semicolon >= 0 ) { contentType = contentType.substring( 0 , semicolon).trim(); } else { contentType = contentType.trim(); } if ( "POST" .equals(getMethod()) && (getContentLength() > 0 ) && "application/x-www-form-urlencoded" .equals(contentType)) { try { int max = getContentLength(); int len = 0 ; byte buf[] = new byte [getContentLength()]; ServletInputStream is = getInputStream(); while (len < max) { int next = is.read(buf, len, max - len); if (next < 0 ) { break ; } len += next; } is.close(); if (len < max) { throw new RuntimeException( "Content length mismatch" ); } RequestUtil.parseParameters(results, buf, encoding); } catch (UnsupportedEncodingException ue) { ; } catch (IOException e) { throw new RuntimeException( "Content read fail" ); } } // Store the final results results.setLocked( true ); parsed = true ; parameters = results; } |
最关键的部分就是通过RequestUtil.parseParameters 解析queryString并把结果保存到results。而且这个函数通过parsed这个变量来保证只会被执行一次。
最后我们访问http://localhost:8080/servlet/PrimitiveServlet,发现浏览器打印了:
1 2 | Hello, Roses are red Violets are blue |
说明上一章的那个最后的out.print("xxx")没有生效的问题解决了。但是访问http://localhost:8080/servlet/ModernServlet?abc=xyz,因为第一章说的问题,火狐浏览器打印了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <html> <head> <title>Modern Servlet</title> </head> <body> <h2>Headers</h2 <br>accept-language : en-US,en;q= 0.5 <br>host : localhost: 8080 <br>upgrade-insecure-requests : 1 <br>connection : keep-alive <br>accept-encoding : gzip, deflate <br>user-agent : Mozilla/ 5.0 (Macintosh; Intel Mac OS X 10.12 ; rv: 52.0 ) Gecko/ 20100101 Firefox/ 52.0 <br>accept : text/html,application/xhtml+xml,application/xml;q= 0.9 ,*/*;q= 0.8 <br><h2>Method</h2 <br>GET <br><h2>Parameters</h2 <br>abc : xyz <br><h2>Query String</h2 <br>abc=xyz <br><h2>Request URI</h2 <br>/servlet/ModernServlet </body> </html> |
如果需要达到想要的效果,可以通过IE浏览器访问。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 现代计算机视觉入门之:什么是图片特征编码
· .NET 9 new features-C#13新的锁类型和语义
· Linux系统下SQL Server数据库镜像配置全流程详解
· 现代计算机视觉入门之:什么是视频
· 你所不知道的 C/C++ 宏知识
· 不到万不得已,千万不要去外包
· C# WebAPI 插件热插拔(持续更新中)
· 会议真的有必要吗?我们产品开发9年了,但从来没开过会
· 【译】我们最喜欢的2024年的 Visual Studio 新功能
· 如何打造一个高并发系统?