咱们都知道,请求参数能够存在于请求头中,也能够存在于请求体中,浏览器中发过来的请求一般是下面这个样子的java
咱们都知道,请求参数能够存在于请求头中,也能够存在于请求体中,浏览器中发过来的请求一般是下面这个样子的spring
POST http://localhost/examples/servlets/servlet/RequestParamExample?version=1 HTTP/1.1 cache-control: no-cache Postman-Token: 3fdd5c34-9b40-4750-8ea1-c868bc9479a7 Content-Type: application/x-www-form-urlencoded User-Agent: PostmanRuntime/6.3.2 Accept: */* Host: localhost accept-encoding: gzip, deflate content-length: 27 Connection: keep-alive firstname=fds&lastname=fdas
在这个请求中,传递了三个参数,version,firstname和lastname,可是参数的地方是不一样的,version参数在请求的url的后面,用“?”和url分开,而后多个参数用&拼接,这种参数就叫作在请求头中。apache
而firstname和lastname这一行,和上面的用了一个空行进行了分割开,这段内容是存在方法体中 。json
如今以http请求为例,分析一下针对于存在于方法头中的参数,tomcat是怎么解析的。首先tomcat接收一个socket请求,这个socket会被封装成一个SocketWrapper对象,而后会NioEndpoint类的一个内部类SocketProcessore的dorun方法会被调用,这个方法中会由AbstractProtocol根据不一样的协议,交由不一样的processor进行处理,针对http,则由AbstractProcessorLight类的process进行处理,这个类是一个轻量级的抽象解析类,提供了upgrade接口,以供协议升级。若是未升级协议,则会调用其子类的service方法,进行处理请求。http请求中,会调用Http11processor的service方法,其接受一个封装了socket的SocketWrapperBase参数。到此,对socket的请求正式开始。以前的流程能够用以下时序图表示。浏览器
http11Processor继承自AbstractProcessor,它的建立实在Abstractprotocolde process方法时,判断有没有相应的processor实例,若是没有的话,则调用createProcess方法进行建立出来的。而createProcessor方法是一个抽象方法,由AbstractHttp1Protocol类实现,建立完成之后会进行缓存,下次使用的时候不用再次建立。AbstractProcess在实例化的时候,会new出来一个Request和Response对象,而后做为它的一个属性,这个地方也就是org.apache.coyote.Request的诞生地。做为承载用户请求数据的一个重要角色,此时的coyote.Request的Request开始了它的首次登场。缓存
public AbstractProcessor(AbstractEndpoint<?> endpoint) { this(endpoint, new Request(), new Response()); } protected AbstractProcessor(AbstractEndpoint<?> endpoint, Request coyoteRequest, Response coyoteResponse) { this.endpoint = endpoint; asyncStateMachine = new AsyncStateMachine(this); request = coyoteRequest; response = coyoteResponse; response.setHook(this); request.setResponse(response); request.setHook(this); }
经过从http11processor的初始化中,能够看出对于请求结果和返回结果以前的过滤器的增长,也就是在初始化的时候增长到这个解析器里面的,而且设置inputBuffer。tomcat
接下来来看真正接收到请求并处理的Http11Procossor的service方法。那么对于对象头参数的解析,也就是在这个方法调用中完成的。mvc
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException { RequestInfo rp = request.getRequestProcessor(); rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); // Setting up the I/O setSocketWrapper(socketWrapper); //初始化inputBuffer和outputBuffer inputBuffer.init(socketWrapper); outputBuffer.init(socketWrapper); // 设置一些标示 keepAlive = true; openSocket = false; readComplete = true; boolean keptAlive = false; SendfileState sendfileState = SendfileState.DONE; //在如下状况下一直自旋 //状态正常,链接存活,方法同步,未升级协议,sendfileState == SendfileState.DONE 而且endpoint非暂停状态 while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null && sendfileState == SendfileState.DONE && !endpoint.isPaused()) { // 解析请求头 ,主要是parseRequestLine方法 try { //按行解析请求行 if (!inputBuffer.parseRequestLine(keptAlive)) { if (inputBuffer.getParsingRequestLinePhase() == -1) { return SocketState.UPGRADING; } else if (handleIncompleteRequestLineRead()) { break; } } ... }
service方法主要是经过http1InputBuffer的parseRequestLine方法解析请求行首部,经过parseHeaders方法解析请求头,而后封装request对象,发送给tomcat的容器(servlet),通过容器处理之后,返回给客户端相应的结果。其中本节关注就是如何解析请求头的,tomcat在这方面作的仍是挺好玩的,我以前想的就是读取一行,而后对一行进行字符串分割,而后tomcat中并无这么读,而是一个字符一个字符的读。能够看一下parseRequestLine这个方法app
parseRequestLine这个方法主要是为了处理这样一行数据框架
POST http://localhost/examples/servlets/servlet/RequestParamExample HTTP/1.1
方法比较长,我给剪切一下
boolean parseRequestLine(boolean keptAlive) throws IOException { // check state if (!parsingRequestLine) { return true; } // // Skipping blank lines 跳过空行 // if (parsingRequestLinePhase < 2) { byte chr = 0; do { } while ((chr == Constants.CR) || (chr == Constants.LF)); byteBuffer.position(byteBuffer.position() - 1); parsingRequestLineStart = byteBuffer.position(); parsingRequestLinePhase = 2; //状态变动 } if (parsingRequestLinePhase == 2) { // // 读取请求方法名称,这里面读取出来是POST // Method name is a token // boolean space = false; while (!space) { } request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, pos - parsingRequestLineStart); //把解析出来的方法,设置为request的method parsingRequestLinePhase = 3; //状态变动 } if (parsingRequestLinePhase == 3) { //读取空白字符,就是请求方法和url中间的空白字符 parsingRequestLinePhase = 4; //状态变动 } if (parsingRequestLinePhase == 4) { // 读取URI boolean space = false; while (!space) { // Read new bytes if needed if (byteBuffer.position() >= byteBuffer.limit()) { if (!fill(false)) // request line parsing return false; } int pos = byteBuffer.position(); byte chr = byteBuffer.get(); if (chr == Constants.SP || chr == Constants.HT) { space = true; end = pos; } else if (chr == Constants.CR || chr == Constants.LF) { // HTTP/0.9 style request parsingRequestLineEol = true; space = true; end = pos; } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) { parsingRequestLineQPos = pos; } else if (HttpParser.isNotRequestTarget(chr)) { throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget")); } } request.queryString().setBytes(byteBuffer.array(), parsingRequestLineQPos + 1, end - parsingRequestLineQPos - 1); request.requestURI().setBytes(byteBuffer.array(), parsingRequestLineStart, parsingRequestLineQPos - parsingRequestLineStart); //设置request的URI的属性值和queryString的属性值 parsingRequestLinePhase = 5; //状态变动 } if (parsingRequestLinePhase == 5) { // 继续读取空白字符,URI和http协议之间的空白字符 parsingRequestLinePhase = 6; //装填变动 } if (parsingRequestLinePhase == 6) { // // 解析协议 // Protocol is always "HTTP/" DIGIT "." DIGIT // request.protocol().setBytes(byteBuffer.array(), parsingRequestLineStart, end - parsingRequestLineStart); //设置request的请求协议属性值 return true; //全部都解析完成之后进行返回 } }
(吐槽一下编辑器,一直没有找到好的编辑器,在代码里面我也能把相关的字体加粗显示,以表示哪些是重点,而且这种黑底白字的我也不喜欢,看来找机会自建博客吧)
parseRequestLine 中使用parsingRequestLinePhase变量来记录如今已经读取到第几种类型的元素了、初始化为0,当parsingRequestLinePhase为2时,表明读取到了请求方法,parsingRequestLinePhase的值含义以下
一、前置空白字符
二、方法POST
3 中间空白字符
4 URI,http://localhost/examples/servlets/servlet/RequestParamExample
五、空白字符
6 协议HTTP/1.1
而后会读取到回车字符,第一行内容的读取就此结束。
这样的话请求行中第一行的解析也就结束了。上面的数字对应的是parsingRequestLinePhase值,以及它具体解析出来的东西。
请求行解析完成之后,接下来就要解析请求头了。请求头的格式以下:
cache-control: no-cache
Postman-Token: 3fdd5c34-9b40-4750-8ea1-c868bc9479a7
Content-Type: application/x-www-form-urlencoded
User-Agent: PostmanRuntime/6.3.2
Accept: */*
Host: localhost
accept-encoding: gzip, deflate
content-length: 27
Connection: keep-alive
在上面已经贴出,为了避免让你再往上翻页,或者你忘了,在这再贴出来一下。
解析请求头使用的是Http11InputBuffer类的parseHeaders方法,这是一个批量解析的方法
boolean parseHeaders() throws IOException {
if (!parsingHeader) {
throw new IllegalStateException(sm.getString("iib.parseheaders.ise.error"));
}
HeaderParseStatus status = HeaderParseStatus.HAVE_MORE_HEADERS;
do {
status = parseHeader();
} while (status == HeaderParseStatus.HAVE_MORE_HEADERS);
if (status == HeaderParseStatus.DONE) {
parsingHeader = false;
end = byteBuffer.position();
return true;
} else {
return false;
}
}
能够看到,这个里面用一个HeaderPraseStatus来标示解析的进度,看是否已经解析完成,还有没有未解析的头,若是有的话,则调用parseHeader进行解析,没有的话,HeaderParseStatus状态变动,则循环结束。看一下如何对单行的请求头进行解析的。
private HeaderParseStatus parseHeader() throws IOException { byte chr = 0; while (headerParsePos == HeaderParsePosition.HEADER_START) { // Read new bytes if needed if (byteBuffer.position() >= byteBuffer.limit()) { if (!fill(false)) {// parse header headerParsePos = HeaderParsePosition.HEADER_START; return HeaderParseStatus.NEED_MORE_DATA; } } chr = byteBuffer.get(); //当取到的字符是\r 则忽略,继续读取下一个字符,当\n的下一个字符是\n 是,则更改解析头的状态为DONE,解析头的工做结束 if (chr == Constants.CR) { // Skip } else if (chr == Constants.LF) { return HeaderParseStatus.DONE; } else { byteBuffer.position(byteBuffer.position() - 1); break; } } if (headerParsePos == HeaderParsePosition.HEADER_START) { // Mark the current buffer position headerData.start = byteBuffer.position(); //移动读取的位置,而后设置当前读取的标示为HEADER_NAME headerParsePos = HeaderParsePosition.HEADER_NAME; } // // 开始读取HEADER_NAME,就是header 冒号 :以前的部分 // Header name is 老是 US-ASCII while (headerParsePos == HeaderParsePosition.HEADER_NAME) { int pos = byteBuffer.position(); chr = byteBuffer.get(); if (chr == Constants.COLON) { //一直一个字符一个字符的读取,当读取到的字符是冒号 ":"时,header name结束,更改状态为HEADER_VALUE_START (开始读取头的value) headerParsePos = HeaderParsePosition.HEADER_VALUE_START; //设置header的name headerData.headerValue = headers.addValue(byteBuffer.array(), headerData.start, pos - headerData.start); } } // Skip the line and ignore the header if (headerParsePos == HeaderParsePosition.HEADER_SKIPLINE) { return skipLine(); } // 读取请求头的Value (可能会跨越多行) while (headerParsePos == HeaderParsePosition.HEADER_VALUE_START || headerParsePos == HeaderParsePosition.HEADER_VALUE || headerParsePos == HeaderParsePosition.HEADER_MULTI_LINE) { //处理value } //设置 header value headerData.headerValue.setBytes(byteBuffer.array(), headerData.start, headerData.lastSignificantChar - headerData.start); headerData.recycle(); return HeaderParseStatus.HAVE_MORE_HEADERS; //默认还有更多的头部信息 }
经过parseHeaders()和parseHeader()方法,解析请求的头部,而后分别把请求头部的name值和value值存储到HeaderParseData中,解析的过程当中,遇到 \r\n 则对头部解析结束
关于请求行的解析,在service方法中就这么多了,剩下的就是关于协议升级和错误处理的相关逻辑,在这本篇中暂时就不去深究。
读到此时,作了哪些事儿呢,就是解析请求行和请求头,尚未看到请求参数解析的影子,那咱们就接着往下走,而后数据被流转到了CyoteAdapter类的service方法中。这是一个适配器,是一个分界岭,tomcat的两大核心的东西 Connector和Container,就是在此时用这个类链接起来的。在这个方法中涉及到以下内容
request/request 转换:tomcat中的cyote.request转换为servlert.Request
Connectainer设置:设置valve以及须要被处理的容器相关。设置完以后就开始调用容器的valve的invoke,从container,而后engineer,context一直到wrapper,经过一系列的管道和过滤器,终于带着request和response对象,走到了httpServlet的service方法中。Request对象中有一个属性叫作parametersParsed,这个参数的意思是请求参数是否已经解析,到了这时候这个参数的值依然是false,也就是说tomcat对请求参数尚未解析。实际上在tomcat1.4以及以前的版本中,参数的解析是在Connector中的,为了提升效率,有时候不须要关注请求的参数,因此在以后的tomcat版本中,参数的解析就放在了真正使用的时候。也就是咱们调用request.getParameter的时候。以上啰啰嗦嗦的说了这么多,那就看一下参数是具体怎么解析的吧。
咱们在本身的servlet中的request,其实是个门面request
这个requestFacade持有Connector.request对象,也就是真正的request,调用getParameter方法时,首先会判断参数是否已经解析,若已经解析,则直接从缓存中拿取数据,若未解析,则调用parseParameter()方法进行解析参数。对于要解析请求行中的参数,解析是在Parameters的handlerQueryParameters函数。
因此对参数的解析,在当前的tomcat版本中均已经放到了Parameter类中。(Request类中parseParameters()处理contentType)
对参数解析的核心方法是
private void processParameters(byte bytes[], int start, int len, Charset charset)
方法虽长,可是思路很简单,就是经过标记start和end逐字节的解析取字符,参数形式为name=value&name2=value2,碰到 = 号,就把前面的记做name,而后后面的记做value,根据&分割参数。而后对一些非标准的参数列表进行处理,形如 && 和&=value&,而后针对解析出来的name和value,调用addParameter方法,把它增长到参数列表中,参数列表实际上也就是个ArrayList。tomcat对请求头的解析采用逐字节读取的方式,对请求参数的解析也采用此种方式,这种写的是如此底层,不知道为何不直接采用拿到字符串,而后采用字符串分割的方式进行解析呢,难道是由于这种方式执行效率更高是么?也许是吧,毕竟我也没有通过度量,之后有机会试试。
在Request中解析参数,有如下的须要注意的逻辑:
protected void parseParameters() { parametersParsed = true; Parameters parameters = coyoteRequest.getParameters(); boolean success = false; boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); parameters.setCharset(charset); if (useBodyEncodingForURI) { parameters.setQueryStringCharset(charset); } parameters.handleQueryParameters(); if( !getConnector().isParseBodyMethod(getMethod()) ) { success = true; return; } 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 ("multipart/form-data".equals(contentType)) { parseParts(false); success = true; return; } if (!("application/x-www-form-urlencoded".equals(contentType))) { success = true; return; } }
从上能够得出如下结论
一、 无论什么类型的请求,均会调用parameters.handleQueryParameters();解析请求行中带的参数 path?p1=value1&p2=value2
二、 根据请求的方法,若是请求不是POST方法,则再也不往下解析,直接返回true
三、 判断请求content-type类型,从从这段代码中能够看出,实际上tomcat只会主动处理两种content-type的请求,
multipart/form-data和application/x-www-form-urlencoded
multipart/form-data处理文件上传的操做,tomcat为咱们进行了封装,使咱们没必要要直接和inputStream打交道,只须要获取文件的part便可,这也就是为何咱们在上传文件时必需要求form的enctype=”multipart/form-data”,而且请求方法是post了。
application/x-www-form-urlencoded处理表单提交的数据,解析表单提交的参数。有时候咱们用POSTMAN模拟发送请求,后台参数接收不到,这时候就须要关注一下content-type使用的是什么了。虽然咱们常常能够看到content-type=application/json或者其余的,可是实际上tomcat对这种传递过来的参数是不作处理的,咱们只能用request.getInputStream本身处理。使用springmvc之类的框架可能对这个有封装。可是必定要谨记的是tomcat只会处理表单的content-type为application/x-www.-form-urlencoded和multipart/form-data的参数。