Tomcat请求解析-请求行和请求头
一、前言
文章:https://www.cnblogs.com/runnable/p/12905401.html中介绍了Tomcat处理一次请求的大致过程,其中包括请求接收、请求数据处理以及请求响应。接下来用两篇文章详细分析请求数据解析:请求行和请求头的读取、请求体的读取。
在分析请求数据处理之前,再次回顾一下2个概念
1、Tomcat中用于读取socket数据的缓冲区buf。它是一个字节数组,默认长度8KB。有2个重要的位置下标:pos和lastValid,pos标记下次读取位置,lastValid标记有效数据最后位置。
图中4种情况分别对应:初始数组;刚从操作系统中读取数据到buf;Tomcat解析过程中,已经读取第一位字节;本次从操作系统读取的数据已经全部解析完。
Tomcat中对请求数据的处理,其实就是重复这四个这个过程,把数据从操作系统读取到Tomcat缓存,然后逐个字节进行解析。我们后面详细分析。
2、字节块(ByteChunk),一种数据结构。有三个重要属性:字节数组buff,start,end。我们从三个属性可以看出,字节块是利用两个下标,标记了一个字节数组中的一段字节。在数据被使用时才把标记的字节转换成字符串,且相同的字节段,如果已经有字符串对应,则会共用该字符串。这样做最大的好处是提高效率、减少内存使用。如下图标记了字节块下标1-4的字节。
3、HTTP请求数据格式如下
整个请求数据的解析过程实际就是根据HTTP规范逐个字节分析,最终转换成请求对象的过程,因此有必要对HTTP格式有了解
下面我们进入主题,通过源码分析请求行和请求头的解析过程
首先进入HTTP11处理器中处理请求的入口:
1 @Override 2 public SocketState process(SocketWrapper<S> socketWrapper) 3 throws IOException { 4 RequestInfo rp = request.getRequestProcessor(); 5 rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); 6 7 // Setting up the I/O 8 setSocketWrapper(socketWrapper); 9 /** 10 * 设置socket的InputStream和OutStream,供后面读取数据和响应使用 11 */ 12 getInputBuffer().init(socketWrapper, endpoint); 13 getOutputBuffer().init(socketWrapper, endpoint); 14 15 // Flags 16 keepAlive = true; 17 comet = false; 18 openSocket = false; 19 sendfileInProgress = false; 20 readComplete = true; 21 if (endpoint.getUsePolling()) { 22 keptAlive = false; 23 } else { 24 keptAlive = socketWrapper.isKeptAlive(); 25 } 26 27 /** 28 * 长连接相关,判断当前socket是否继续处理接下来的请求 29 */ 30 if (disableKeepAlive()) { 31 socketWrapper.setKeepAliveLeft(0); 32 } 33 34 /** 35 * 处理socket中的请求,在长连接的模式下,每次循环表示一个HTTP请求 36 */ 37 while (!getErrorState().isError() && keepAlive && !comet && !isAsync() && 38 upgradeInbound == null && 39 httpUpgradeHandler == null && !endpoint.isPaused()) { 40 41 // Parsing the request header 42 try { 43 /** 44 * 1、设置socket超时时间 45 * 2、第一次从socket中读取数据 46 */ 47 setRequestLineReadTimeout(); 48 49 /** 50 * 读取请求行 51 */ 52 if (!getInputBuffer().parseRequestLine(keptAlive)) { 53 if (handleIncompleteRequestLineRead()) { 54 break; 55 } 56 } 57 58 // Process the Protocol component of the request line 59 // Need to know if this is an HTTP 0.9 request before trying to 60 // parse headers. 61 prepareRequestProtocol(); 62 63 if (endpoint.isPaused()) { 64 // 503 - Service unavailable 65 response.setStatus(503); 66 setErrorState(ErrorState.CLOSE_CLEAN, null); 67 } else { 68 keptAlive = true; 69 // Set this every time in case limit has been changed via JMX 70 // 设置请求头数量 71 request.getMimeHeaders().setLimit(endpoint.getMaxHeaderCount()); 72 // 设置做多可设置cookie数量 73 request.getCookies().setLimit(getMaxCookieCount()); 74 // Currently only NIO will ever return false here 75 // Don't parse headers for HTTP/0.9 76 /** 77 * 读取请求头 78 */ 79 if (!http09 && !getInputBuffer().parseHeaders()) { 80 // We've read part of the request, don't recycle it 81 // instead associate it with the socket 82 openSocket = true; 83 readComplete = false; 84 break; 85 } 86 if (!disableUploadTimeout) { 87 setSocketTimeout(connectionUploadTimeout); 88 } 89 } 90 } catch (IOException e) { 91 if (getLog().isDebugEnabled()) { 92 getLog().debug( 93 sm.getString("http11processor.header.parse"), e); 94 } 95 setErrorState(ErrorState.CLOSE_NOW, e); 96 break; 97 } catch (Throwable t) { 98 ExceptionUtils.handleThrowable(t); 99 UserDataHelper.Mode logMode = userDataHelper.getNextMode(); 100 if (logMode != null) { 101 String message = sm.getString( 102 "http11processor.header.parse"); 103 switch (logMode) { 104 case INFO_THEN_DEBUG: 105 message += sm.getString( 106 "http11processor.fallToDebug"); 107 //$FALL-THROUGH$ 108 case INFO: 109 getLog().info(message, t); 110 break; 111 case DEBUG: 112 getLog().debug(message, t); 113 } 114 } 115 // 400 - Bad Request 116 response.setStatus(400); 117 setErrorState(ErrorState.CLOSE_CLEAN, t); 118 getAdapter().log(request, response, 0); 119 } 120 121 if (!getErrorState().isError()) { 122 // Setting up filters, and parse some request headers 123 rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); 124 try { 125 prepareRequest(); 126 } catch (Throwable t) { 127 ExceptionUtils.handleThrowable(t); 128 if (getLog().isDebugEnabled()) { 129 getLog().debug(sm.getString( 130 "http11processor.request.prepare"), t); 131 } 132 // 500 - Internal Server Error 133 response.setStatus(500); 134 setErrorState(ErrorState.CLOSE_CLEAN, t); 135 getAdapter().log(request, response, 0); 136 } 137 } 138 139 if (maxKeepAliveRequests == 1) { 140 keepAlive = false; 141 } else if (maxKeepAliveRequests > 0 && 142 socketWrapper.decrementKeepAlive() <= 0) { 143 keepAlive = false; 144 } 145 146 // Process the request in the adapter 147 if (!getErrorState().isError()) { 148 try { 149 rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); 150 /** 151 * 将封装好的请求和响应对象,交由容器处理 152 * service-->host-->context-->wrapper-->servlet 153 * 这里非常重要,我们所写的servlet代码正是这里在调用,它遵循了Servlet规范 154 * 这里处理完,代表程序员开发的servlet已经执行完毕 155 */ 156 adapter.service(request, response); 157 // Handle when the response was committed before a serious 158 // error occurred. Throwing a ServletException should both 159 // set the status to 500 and set the errorException. 160 // If we fail here, then the response is likely already 161 // committed, so we can't try and set headers. 162 if(keepAlive && !getErrorState().isError() && ( 163 response.getErrorException() != null || 164 (!isAsync() && 165 statusDropsConnection(response.getStatus())))) { 166 setErrorState(ErrorState.CLOSE_CLEAN, null); 167 } 168 setCometTimeouts(socketWrapper); 169 } catch (InterruptedIOException e) { 170 setErrorState(ErrorState.CLOSE_NOW, e); 171 } catch (HeadersTooLargeException e) { 172 getLog().error(sm.getString("http11processor.request.process"), e); 173 // The response should not have been committed but check it 174 // anyway to be safe 175 if (response.isCommitted()) { 176 setErrorState(ErrorState.CLOSE_NOW, e); 177 } else { 178 response.reset(); 179 response.setStatus(500); 180 setErrorState(ErrorState.CLOSE_CLEAN, e); 181 response.setHeader("Connection", "close"); // TODO: Remove 182 } 183 } catch (Throwable t) { 184 ExceptionUtils.handleThrowable(t); 185 getLog().error(sm.getString("http11processor.request.process"), t); 186 // 500 - Internal Server Error 187 response.setStatus(500); 188 setErrorState(ErrorState.CLOSE_CLEAN, t); 189 getAdapter().log(request, response, 0); 190 } 191 } 192 193 // Finish the handling of the request 194 rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT); 195 196 if (!isAsync() && !comet) { 197 if (getErrorState().isError()) { 198 // If we know we are closing the connection, don't drain 199 // input. This way uploading a 100GB file doesn't tie up the 200 // thread if the servlet has rejected it. 201 getInputBuffer().setSwallowInput(false); 202 } else { 203 // Need to check this again here in case the response was 204 // committed before the error that requires the connection 205 // to be closed occurred. 206 checkExpectationAndResponseStatus(); 207 } 208 /** 209 * 请求收尾工作 210 * 判断请求体是否读取完毕,没有则读取完毕,并修正pos 211 * 请求体读取分为两种: 212 * 1、程序员读取:在servlet中有程序员主动读取,这种方式读取数据不一定读取完整数据,取决于业务需求 213 * 2、Tomcat自己读取:如果servlet中没有读取,或者没有读取完全,则Tomcat负责读取剩余的请求体 214 * 1和2的差别在于,2中仅仅把数据从操作系统读取到buf中,尽管也用了字节块做标记,但是不会做其他的事情,而1中还会把字节块标记的数据拷贝到目标数组中 215 * 这个方法就是处理情况2中的请求体读取逻辑 216 */ 217 endRequest(); 218 } 219 220 rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT); 221 222 // If there was an error, make sure the request is counted as 223 // and error, and update the statistics counter 224 if (getErrorState().isError()) { 225 response.setStatus(500); 226 } 227 request.updateCounters(); 228 229 if (!isAsync() && !comet || getErrorState().isError()) { 230 if (getErrorState().isIoAllowed()) { 231 /** 232 * 根据修正完的pos和lastValid,初始化数组下标,以便继续处理下一次请求 233 * 两种情况 234 * 1、读取请求体刚好读取完,将pos=lastValid=0,即都指向buf数组第一个位置,重新读取数据 235 * 2、读取请求体多读出了下次请求的数据,这个时候需要将下个请求的数据移动到buf数组头,以便处理下个请求 236 * 注意,buf数组中的数据没有删除,是直接覆盖,从而达到对buf数组的重复使用 237 */ 238 getInputBuffer().nextRequest(); 239 getOutputBuffer().nextRequest(); 240 } 241 } 242 243 if (!disableUploadTimeout) { 244 if(endpoint.getSoTimeout() > 0) { 245 setSocketTimeout(endpoint.getSoTimeout()); 246 } else { 247 setSocketTimeout(0); 248 } 249 } 250 251 rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); 252 253 if (breakKeepAliveLoop(socketWrapper)) { 254 break; 255 } 256 } 257 258 rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); 259 260 if (getErrorState().isError() || endpoint.isPaused()) { 261 return SocketState.CLOSED; 262 } else if (isAsync() || comet) { 263 return SocketState.LONG; 264 } else if (isUpgrade()) { 265 return SocketState.UPGRADING; 266 } else if (getUpgradeInbound() != null) { 267 return SocketState.UPGRADING_TOMCAT; 268 } else { 269 if (sendfileInProgress) { 270 return SocketState.SENDFILE; 271 } else { 272 if (openSocket) { 273 if (readComplete) { 274 return SocketState.OPEN; 275 } else { 276 return SocketState.LONG; 277 } 278 } else { 279 return SocketState.CLOSED; 280 } 281 } 282 } 283 }
分析:
上述方法展示整个请求处理的核心过程,其中52行开始处理请求行:getInputBuffer().parseRequestLine(keptAlive)
二、请求行解析
具体方法如下:
1 /** 2 * Read the request line. This function is meant to be used during the 3 * HTTP request header parsing. Do NOT attempt to read the request body 4 * using it. 5 * 6 * @throws IOException If an exception occurs during the underlying socket 7 * read operations, or if the given buffer is not big enough to accommodate 8 * the whole line. 9 */ 10 /** 11 * 读取请求行方法 12 * 请求行格式如下: 13 * ======================================== 14 * 请求方法 空格 URL 空格 协议版本 回车换行 15 * ======================================== 16 * @param useAvailableDataOnly 17 * @return 18 * @throws IOException 19 */ 20 @Override 21 public boolean parseRequestLine(boolean useAvailableDataOnly) 22 23 throws IOException { 24 25 int start = 0; 26 27 // 28 // Skipping blank lines 29 // 30 31 /** 32 * 过滤掉回车(CR)换行(LF)符,确定start位置 33 */ 34 do { 35 36 // Read new bytes if needed 37 if (pos >= lastValid) { 38 if (!fill()) 39 throw new EOFException(sm.getString("iib.eof.error")); 40 } 41 // Set the start time once we start reading data (even if it is 42 // just skipping blank lines) 43 if (request.getStartTime() < 0) { 44 request.setStartTime(System.currentTimeMillis()); 45 } 46 /** 47 * chr记录第一个非CRLF字节,后面读取请求头的时候用到 48 */ 49 chr = buf[pos++]; 50 } while (chr == Constants.CR || chr == Constants.LF); 51 52 pos--; 53 54 // Mark the current buffer position 55 start = pos; 56 57 // 58 // Reading the method name 59 // Method name is a token 60 // 61 62 boolean space = false; 63 64 /** 65 * 读取HTT请求方法:get/post/put.... 66 */ 67 while (!space) { 68 69 // Read new bytes if needed 70 if (pos >= lastValid) { 71 if (!fill()) 72 throw new EOFException(sm.getString("iib.eof.error")); 73 } 74 75 // Spec says method name is a token followed by a single SP but 76 // also be tolerant of multiple SP and/or HT. 77 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) { 78 space = true; 79 /** 80 * 设置HTTP请求方法,这里没有直接设置字符串,而是用了字节块ByteChunk 81 * ByteChunk中包含一个字节数据类型的属性buff,此处的setBytes方法就是将buff指向Tomcat的缓存buf。然后start和end标记为 82 * 此处方法的后两个入参,也就是将请求方法在buf中标记了出来,但是没有转换成字符串,等到使用的时候再使用ByteBuffer.wap方法 83 * 转换成字符串,且标记hasStrValue=true,如果再次获取就直接拿转换好的字符串,不用再次转换。效率考虑?牛逼! 84 * 因此,就算后面由于请求体过长,Tomcat重新开辟新的数组buf读取请求体。原buf也不会被GC,因为ByteChunk中的buff引用了原buf数组 85 * 什么时候原数组才会被GC?本次请求结束,request对象被GC后。。。 86 */ 87 request.method().setBytes(buf, start, pos - start); 88 } else if (!HttpParser.isToken(buf[pos])) { 89 String invalidMethodValue = parseInvalid(start, buf); 90 throw new IllegalArgumentException(sm.getString("iib.invalidmethod", invalidMethodValue)); 91 } 92 93 pos++; 94 95 } 96 97 // Spec says single SP but also be tolerant of multiple SP and/or HT 98 /** 99 * 过滤请求方法后面的空格(SP或者HT) 100 */ 101 while (space) { 102 // Read new bytes if needed 103 if (pos >= lastValid) { 104 if (!fill()) 105 throw new EOFException(sm.getString("iib.eof.error")); 106 } 107 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) { 108 pos++; 109 } else { 110 space = false; 111 } 112 } 113 114 // Mark the current buffer position 115 start = pos; 116 int end = 0; 117 int questionPos = -1; 118 119 // 120 // Reading the URI 121 // 122 123 boolean eol = false; 124 125 /** 126 * 读取URL 127 */ 128 while (!space) { 129 130 // Read new bytes if needed 131 if (pos >= lastValid) { 132 if (!fill()) 133 throw new EOFException(sm.getString("iib.eof.error")); 134 } 135 136 /** 137 * CR后面没有LF,不是HTTP0.9,抛异常 138 */ 139 if (buf[pos -1] == Constants.CR && buf[pos] != Constants.LF) { 140 // CR not followed by LF so not an HTTP/0.9 request and 141 // therefore invalid. Trigger error handling. 142 // Avoid unknown protocol triggering an additional error 143 request.protocol().setString(Constants.HTTP_11); 144 String invalidRequestTarget = parseInvalid(start, buf); 145 throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); 146 } 147 148 // Spec says single SP but it also says be tolerant of HT 149 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) { 150 /** 151 * 遇到空格(SP或者HT),URL读取结束 152 */ 153 space = true; 154 end = pos; 155 } else if (buf[pos] == Constants.CR) { 156 // HTTP/0.9 style request. CR is optional. LF is not. 157 } else if (buf[pos] == Constants.LF) { 158 // HTTP/0.9 style request 159 // Stop this processing loop 160 space = true; 161 // Set blank protocol (indicates HTTP/0.9) 162 request.protocol().setString(""); 163 // Skip the protocol processing 164 eol = true; 165 if (buf[pos - 1] == Constants.CR) { 166 end = pos - 1; 167 } else { 168 end = pos; 169 } 170 } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) { 171 questionPos = pos; 172 } else if (questionPos != -1 && !httpParser.isQueryRelaxed(buf[pos])) { 173 // %nn decoding will be checked at the point of decoding 174 String invalidRequestTarget = parseInvalid(start, buf); 175 throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); 176 } else if (httpParser.isNotRequestTargetRelaxed(buf[pos])) { 177 // This is a general check that aims to catch problems early 178 // Detailed checking of each part of the request target will 179 // happen in AbstractHttp11Processor#prepareRequest() 180 String invalidRequestTarget = parseInvalid(start, buf); 181 throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget)); 182 } 183 pos++; 184 } 185 /** 186 * 读取HTTP URL 187 */ 188 request.unparsedURI().setBytes(buf, start, end - start); 189 if (questionPos >= 0) { 190 /** 191 * 当有请求入参的时候 192 * 读取入参字符串 193 * 读取URI 194 */ 195 request.queryString().setBytes(buf, questionPos + 1, 196 end - questionPos - 1); 197 request.requestURI().setBytes(buf, start, questionPos - start); 198 } else { 199 /** 200 * 没有请求入参的时候,直接读取URI 201 */ 202 request.requestURI().setBytes(buf, start, end - start); 203 } 204 205 // Spec says single SP but also says be tolerant of multiple SP and/or HT 206 while (space && !eol) { 207 // Read new bytes if needed 208 if (pos >= lastValid) { 209 if (!fill()) 210 throw new EOFException(sm.getString("iib.eof.error")); 211 } 212 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) { 213 pos++; 214 } else { 215 space = false; 216 } 217 } 218 219 // Mark the current buffer position 220 start = pos; 221 end = 0; 222 223 // 224 // Reading the protocol 225 // Protocol is always "HTTP/" DIGIT "." DIGIT 226 // 227 /** 228 * 读取HTTP协议版本 229 */ 230 while (!eol) { 231 232 // Read new bytes if needed 233 if (pos >= lastValid) { 234 if (!fill()) 235 throw new EOFException(sm.getString("iib.eof.error")); 236 } 237 238 if (buf[pos] == Constants.CR) { 239 // Possible end of request line. Need LF next. 240 } else if (buf[pos - 1] == Constants.CR && buf[pos] == Constants.LF) { 241 end = pos - 1; 242 eol = true; 243 } else if (!HttpParser.isHttpProtocol(buf[pos])) { 244 String invalidProtocol = parseInvalid(start, buf); 245 throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol", invalidProtocol)); 246 } 247 248 pos++; 249 250 } 251 252 /** 253 * 字节块标记协议版本 254 */ 255 if ((end - start) > 0) { 256 request.protocol().setBytes(buf, start, end - start); 257 } 258 259 /** 260 * 如果没有协议版本,无法处理请求,抛异常 261 */ 262 if (request.protocol().isNull()) { 263 throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol")); 264 } 265 266 return true; 267 }
在这个方法中,其实就是请求行请求方法、url、协议版本这几个部分的读取。
分析:
34-50行:这个while循环是过滤行首的回车换行符,只要是回车换行符下标pos就往后移动一位,直到不是回车换行符,跳出循环。由于这里是先执行pos++,所以如果不满足条件,pos需要后移一位,也就是真正开始读取请求方法的位置,标记为start。
37-40行:这里是非常关键的几行代码,几乎贯穿整个请求处理部分。Tomcat接收请求,就是在接收客户端的请求数据,数据经过网络传输到Tomcat所在的服务操作系统缓冲区,Tomcat从操作系统读取到自己的缓冲区buf中。这几行代码主要就是干这个事情的。前面我们介绍了字节数在buf是通过pos和lastValid控制读取的。37行判断当pos>=lastValid,表示buf数组中读取自操作系统的数据已经解析完毕,调用fill()方法再次从操作系统读取。代码如下:
1 @Override 2 protected boolean fill(boolean block) throws IOException { 3 4 int nRead = 0; 5 6 /** 7 * 这个核心就是读取socket中数据到缓冲区buf中,循环读取,2种情况 8 * 1、请求行和请求头:不能超过缓冲区大小(默认8kb),如果超过,则抛异常,读完后将parsingHeader设置为false 9 * 2、请求行:没有任何大小限制,循环读取,如果剩下的少于4500个字节,则会重新创建buf数组,从头开始读取,直到读完位置,注意!buf原先引用的数组们,等待GC 10 */ 11 if (parsingHeader) { 12 13 /** 14 * 从socket中读取数据大于tomcat中缓冲区buf的长度,直接抛异常,这里有两点 15 * 1、这个就是我们很多时候很多人说的,get请求url不能过长的原因,其实是header和url等总大小不能超过8kb 16 * 2、这里的buf非常总要,它是InternalInputBuffer的属性,是一个字节数据,用户暂存从socket中读取的数据,比如:请求行,请求头、请求体 17 */ 18 if (lastValid == buf.length) { 19 throw new IllegalArgumentException 20 (sm.getString("iib.requestheadertoolarge.error")); 21 } 22 23 // 将socket中的数据读到缓冲区buf中,注意!这里就是BIO之所以难懂的关键所在,它会阻塞! 24 // 这个方法会阻塞,如果没有数据可读,则会一直阻塞,有数据,则移动lastValid位置 25 nRead = inputStream.read(buf, pos, buf.length - lastValid); 26 if (nRead > 0) { 27 lastValid = pos + nRead; 28 } 29 30 } else { 31 /** 32 * parsingHeader==false,请求行和请求头已经读取完毕,开始读取请求体 33 */ 34 35 if (buf.length - end < 4500) { 36 // In this case, the request header was really large, so we allocate a 37 // brand new one; the old one will get GCed when subsequent requests 38 // clear all references 39 /** 40 * 如果Tomcat缓存区buf读取完请求行和请求头后,剩余长度不足4500(可配置),新创建一个字节数组buf用于读取请求体 41 * 为什么要这么做,应该是考虑到如果剩余的数据长度较小,每次从操作系统缓存区读取的字节就比较少,读取次数就比较多? 42 * 注意,buf原先指向的字节数据会白GC么?应该不会,因为请求行和请求头有许多字节块(ByteChunk)指向了旧字节数据。 43 * 什么时候才会被GC?应该是一起request处理完毕后。 44 */ 45 buf = new byte[buf.length]; 46 end = 0; 47 } 48 /** 49 * 这里的end是请求头数据的后一位,从这里开始读取请求体数据。 50 * 从操作系统读取数据到buf中,下标pos开始,lastValid结束 51 * 注意:这里每次读取请求体数据的时候都会把pos重置为end(请求头数据的后一位)!!!!! 52 * 表示什么? 53 * 请求体数据每一次从操作系统缓存中读取到buf,然后读取到程序员自己的数组后,在下次再次从操作系统读取数据到buf时,就会把之前读取的请求体数据覆盖掉 54 * 也就是从end位置开始,后面的数据都只能读取一次,这个很重要!!! 55 * 为什么这么做?我的理解是因为请求体数据可以很大,为了单个请求不占用太大内存,所以设计成了覆盖的模式,真是秒啊! 56 */ 57 pos = end; 58 lastValid = pos; 59 60 /** 61 * 原则上这个方法要么阻塞着,要么nRead>0 62 */ 63 nRead = inputStream.read(buf, pos, buf.length - lastValid); 64 if (nRead > 0) { 65 lastValid = pos + nRead; 66 } 67 68 } 69 70 /** 71 * 注意,这里不出意外,只能返回true 72 */ 73 return (nRead > 0); 74 75 }
这个方法由两部分逻辑组成:parsingHeader=true或者false,这个变量表示读取的请求行和请求头,还是读取的请求体。变量名有点歧义,并不是只包含请求头,而是请求行和请求头。
11-30行:读取请求行和请求头数据,逻辑很简单:从操作系统读取数据到字节数组buf中,后移lastValid下标到buf数组最后一个字节的位置。在Tomcat解析完这部分数据后,会把parsingHeader置为false,且用end下标指向请求头后一个字节,以便后续可以读取请求体数据。
35-66行:读取请求体数据,逻辑比请求行和请求头读取稍微复杂点:判断buf数组剩余字节长度是否大于4500,反之重新创建数组。每次读取pos和lastValid都置为end,然后读取数据到buf数组中,lastValid后移。由于请求体数据可能比较大,且理论上没有上限限制,为了减少读取次数,buf剩余空间不能过小。每次读取数据到buf中,都是存放在end位置开始,每次都是覆盖上一次读取的数据,所以我们可以大胆猜测,请求体数据只能读取一次,程序员自己如果需要多次使用,必须自行保存。我想这是为了减少内存使用吧,你们看呢?
还有一个关键点:25行和63行代码:nRead = inputStream.read(buf, pos, buf.length - lastValid),这行代码是从操作系统读取字节,接触过socket编程的都知道read方法这里可能会阻塞的,当操作系统缓存中当前没有数据可读,等待网络传输的时候,read方法阻塞,直到有数据返回后再继续。
回到读取请求行的代码。确定好了start位置后,开始读取请求方法。
67-95行:又是一个while循环,当遇到SP或者HT时,表示请求方法已经读取完毕。
87行:将start到pos前一位用字节块进行标记,只是标记,并不会转换成字符串。具体代码:
1 /** 2 * Sets the buffer to the specified subarray of bytes. 3 * 4 * @param b the ascii bytes 5 * @param off the start offset of the bytes 6 * @param len the length of the bytes 7 */ 8 public void setBytes(byte[] b, int off, int len) { 9 buff = b; 10 start = off; 11 end = start + len; 12 isSet = true; 13 hasHashCode = false; 14 }
101-112行:继续过滤掉SP或者HT,重置start,为读取URL做准备。
128-184行:读取所有的URL字节,遇到空格退出,这里并没有标记URL.
188-203行:根据上面得出的位置标记,利用字节块对URI、URL、参数分别进行标记。
206-217行:再次对空格进行过滤,重置start,准备读取协议版本。
230-250行:读取剩余字节,遇到连续的两个字节CRLF,确定请求行结束位置。
256行:使用字节块标记协议版本。
自此,请求行解析完毕,每个部分都已经利用专门的字节块(ByteChunk)进行标记。我们看到每个循环里面都会调用fill()方法从操作系统读取数据到Tomcat缓冲区中,一次请求数据的传输不一定能够传输完毕,所以Tomcat中要始终保持读取数据的状态,这个是关键,一定要理解,否则就无法理解Tomcat对请求数据的解析过程。
三、请求头解析
再次回到处理请求的入口代码中:其中79行开始处理请求头:getInputBuffer().parseHeaders()。
1 /** 2 * Parse the HTTP headers. 3 */ 4 @Override 5 public boolean parseHeaders() 6 throws IOException { 7 /** 8 * 请求行和请求头读取的标志,如果不是请求行和请求头,进入此方法,抛异常 9 */ 10 if (!parsingHeader) { 11 throw new IllegalStateException( 12 sm.getString("iib.parseheaders.ise.error")); 13 } 14 15 /** 16 * 读取请求头,循环执行,每次循环读取请求头的一个key:value对 17 */ 18 while (parseHeader()) { 19 // Loop until we run out of headers 20 } 21 22 /** 23 * 请求头读取完毕,标志变为false,end=pos,标志此处是请求行和请求头读取完毕的位置 24 */ 25 parsingHeader = false; 26 end = pos; 27 return true; 28 }
整个parseHearders方法比较简单,分三部分逻辑:1、判断是否parsingHeader=true,不是的话抛异常。2、while循环。3、处理完毕parsingHeader=false,end=pos,为读取请求体做准备。
重点关注第二部分的循环,18行代码:while (parseHeader())。仅仅是一个循环,没有方法体。这里其实每次循环都是试图读取一个请求头的key:value对。代码如下:
1 /** 2 * 读取请求头信息,注意:每次调用该方法,完成一个键值对读取,也即下面格式中的一行请求头 3 * 请求头格式如下 4 * =================================== 5 * key:空格(SP)value回车(CR)换行(LF) 6 * ... 7 * key:空格(SP)value回车(CR)换行(LF) 8 * 回车(CR)换行(LF) 9 * =================================== 10 * 11 * Parse an HTTP header. 12 * 13 * @return false after reading a blank line (which indicates that the 14 * HTTP header parsing is done 15 */ 16 @SuppressWarnings("null") // headerValue cannot be null 17 private boolean parseHeader() throws IOException { 18 19 /** 20 * 此循环主要是在每行请求头信息开始前,确定首字节的位置 21 */ 22 while (true) { 23 24 // Read new bytes if needed 25 /** 26 * Tomcat缓存buf中没有带读取数据,重新从操作系统读取一批 27 */ 28 if (pos >= lastValid) { 29 if (!fill()) 30 throw new EOFException(sm.getString("iib.eof.error")); 31 } 32 33 /** 34 * 这里的chr最开始是在读取请求行时赋值,赋予它请求行第一个非空格字节 35 */ 36 prevChr = chr; 37 chr = buf[pos]; 38 39 /** 40 * 首位置是回车符(CR),有2种情况: 41 * 1、CR+(~LF) 首次先往后移动一个位置,试探第二个位置是否是LF,如果是则进入情况2;如果不是,则回退pos。key首字节可以是CR,但第2个字节不能是LF,因为行CRLF是请求头结束标志 42 * 2、CR+LF 请求头结束标志,直接结束请求头读取 43 * 首位置不是CR,直接结束循环,开始读取key 44 */ 45 if (chr == Constants.CR && prevChr != Constants.CR) { 46 /** 47 * 每次while循环首次进入这个if分支preChr都不是CR,如果当前位置pos是CR,则往后移动一位,根据后一位情况决定后续操作 48 * 如果后一位是LF,直接直接请求头读取 49 * 如果后一位不是LF,pos回退一位,用作key。 50 */ 51 // Possible start of CRLF - process the next byte. 52 } else if (prevChr == Constants.CR && chr == Constants.LF) { 53 /** 54 * 请求头结束,注意是请求头结束,不是当前键值对结束,请求头结束标志:没有任何其他数据,直接CRLF 55 */ 56 pos++; 57 return false; 58 } else { 59 /** 60 * 如果当前行的首字节不是CR,直接break,开始读取key 61 * 如果当前行首字节是CR,但是第二字节不是LF,pos回退1位,开始读取key 62 */ 63 if (prevChr == Constants.CR) { 64 // Must have read two bytes (first was CR, second was not LF) 65 pos--; 66 } 67 break; 68 } 69 70 pos++; 71 } 72 73 // Mark the current buffer position 74 /** 75 * 标记当前键值对行开始位置 76 */ 77 int start = pos; 78 int lineStart = start; 79 80 // 81 // Reading the header name 82 // Header name is always US-ASCII 83 // 84 85 /** 86 * colon标记冒号的位置 87 */ 88 boolean colon = false; 89 MessageBytes headerValue = null; 90 91 /** 92 * 读取key,直到当前字节是冒号(:)跳出循环,pos指向冒号后一个字节 93 */ 94 while (!colon) { 95 96 // Read new bytes if needed 97 /** 98 * 获取缓冲区数据 99 */ 100 if (pos >= lastValid) { 101 if (!fill()) 102 throw new EOFException(sm.getString("iib.eof.error")); 103 } 104 105 106 if (buf[pos] == Constants.COLON) { 107 /** 108 * 当前字节是冒号,colon=true,当前循环执行完后,结束循环 109 * 在Tomcat缓冲区buf字节数组中标记出头信息的名称key: 110 * 每个key:value对中有2个MessageBytes对象,每个MessageBytes对象中都有字节块ByteChunk,用来标记buf中的字节段 111 */ 112 colon = true; 113 headerValue = headers.addValue(buf, start, pos - start); 114 } else if (!HttpParser.isToken(buf[pos])) { 115 // Non-token characters are illegal in header names 116 // Parsing continues so the error can be reported in context 117 // skipLine() will handle the error 118 /** 119 * 非普通字符,比如:(,?,:等,跳过这行 120 */ 121 skipLine(lineStart, start); 122 return true; 123 } 124 125 /** 126 * 大写字符转换成小写字符,chr记录key中最后一个有效字节 127 */ 128 chr = buf[pos]; 129 if ((chr >= Constants.A) && (chr <= Constants.Z)) { 130 buf[pos] = (byte) (chr - Constants.LC_OFFSET); 131 } 132 133 /** 134 * 下标自增,继续下次循环 135 */ 136 pos++; 137 138 } 139 140 // Mark the current buffer positio 141 /** 142 * 重置start,开始读取请求头值value 143 */ 144 start = pos; 145 int realPos = pos; 146 147 // 148 // Reading the header value (which can be spanned over multiple lines) 149 // 150 151 boolean eol = false; 152 boolean validLine = true; 153 154 while (validLine) { 155 156 boolean space = true; 157 158 // Skipping spaces 159 /** 160 * 跳过空格(SP)和制表符(HT) 161 */ 162 while (space) { 163 164 // Read new bytes if needed 165 if (pos >= lastValid) { 166 if (!fill()) 167 throw new EOFException(sm.getString("iib.eof.error")); 168 } 169 170 if ((buf[pos] == Constants.SP) || (buf[pos] == Constants.HT)) { 171 pos++; 172 } else { 173 space = false; 174 } 175 176 } 177 178 int lastSignificantChar = realPos; 179 180 // Reading bytes until the end of the line 181 /** 182 * 183 */ 184 while (!eol) { 185 186 // Read new bytes if needed 187 if (pos >= lastValid) { 188 if (!fill()) 189 throw new EOFException(sm.getString("iib.eof.error")); 190 } 191 192 /** 193 * prevChr首次为chr=:,之后为上一次循环的chr 194 * chr为当前pos位置的字节 195 */ 196 prevChr = chr; 197 chr = buf[pos]; 198 if (chr == Constants.CR) { 199 /** 200 * 当前字节是回车符,直接下次循环,看下个字节是否是LF 201 */ 202 // Possible start of CRLF - process the next byte. 203 } else if (prevChr == Constants.CR && chr == Constants.LF) { 204 /** 205 * 当前字节是LF,前一个字节是CR,请求头当前key:value行读取结束 206 */ 207 eol = true; 208 } else if (prevChr == Constants.CR) { 209 /** 210 * 如果前一字节是CR,当前位置字节不是LF,则本key:value对无效,删除! 211 * 直接返回true,读取下一个key:value对 212 */ 213 // Invalid value 214 // Delete the header (it will be the most recent one) 215 headers.removeHeader(headers.size() - 1); 216 skipLine(lineStart, start); 217 return true; 218 } else if (chr != Constants.HT && HttpParser.isControl(chr)) { 219 // Invalid value 220 // Delete the header (it will be the most recent one) 221 headers.removeHeader(headers.size() - 1); 222 skipLine(lineStart, start); 223 return true; 224 } else if (chr == Constants.SP) { 225 /** 226 * 当前位置空格,位置后移一位 227 */ 228 buf[realPos] = chr; 229 realPos++; 230 } else { 231 /** 232 * 当前位置常规字符,位置后移一位,标记最后字符 233 */ 234 buf[realPos] = chr; 235 realPos++; 236 lastSignificantChar = realPos; 237 } 238 239 pos++; 240 241 } 242 243 realPos = lastSignificantChar; 244 245 // Checking the first character of the new line. If the character 246 // is a LWS, then it's a multiline header 247 248 // Read new bytes if needed 249 if (pos >= lastValid) { 250 if (!fill()) 251 throw new EOFException(sm.getString("iib.eof.error")); 252 } 253 254 /** 255 * 特殊逻辑: 256 * 当前key:value对读取完后, 257 * 如果紧接着的是SP(空格)或则HT(制表符),表示当前value读取并未结束,是多行的,将eol=false,继续读取,直到CRLF. 258 * 如果紧接着不是SP和HT,那vaLine=false,跳出循环,value读取完毕 259 */ 260 byte peek = buf[pos]; 261 if (peek != Constants.SP && peek != Constants.HT) { 262 validLine = false; 263 } else { 264 eol = false; 265 // Copying one extra space in the buffer (since there must 266 // be at least one space inserted between the lines) 267 buf[realPos] = peek; 268 realPos++; 269 } 270 271 } 272 273 // Set the header value 274 /** 275 * 使用新的字节块BytChunk标记当前key:value对的value 276 */ 277 headerValue.setBytes(buf, start, realPos - start); 278 279 return true; 280 281 }
22-71行:每行请求头开始读取前,确定首字节的位置。详细逻辑比较复杂,请看代码注释
94-138行:读取请求头key的数据,直到遇到冒号为止,这里同样使用了字节块来标记。
144-结尾:首先重置start,然后再次过滤空格,直到遇到联系的CRLF表示当前key:value结束。请求头value数据同样也使用了字节块来做标记。
读取value有个特殊逻辑:
260-271行:当前请求头value读取完毕后,如果紧接着是空格,表示当前请求头的值有多个,将eol=false,继续循环读取,直到CRLF。这种情况一般很很少使用,了解就好。
至此,已经完整分析了一次HTTP请求中请求行和请求头的详细读取过程。重点要理解Tomcat中的缓存缓冲区,以及IO读取数据的方式。最后按照HTTP规范解析,这个过程比较底层,也比较绕,需要有耐心,下篇文章我们继续开始请求体的处理,敬请关注!
再次强调:以上源码都是基于Tomcat7,且是BIO模型。