引言
在前面两篇学习笔记中说过, Tomcat在启动之后, 会启动线程, 对应类名为org.apache.tomcat.util.net.JIoEndpoint.Acceptor, 在这个线程中会不断的在while(running)这个循环中去调用serverSocket.accept()方法去接受socket, 然后包装成socketWrapper, 进而交给线程池中去处理socket, 本篇将详细阐述这个socket是如何处理的.
要想看这个socket如何处理的, 就需要从tomcat如何启动开始看了, 但是启动这块将会留在后面的博客分析, 所以这次我们就从Connector如何初始化的开始看起, 详细看看每个组件的每个属性是如何初始化进去的.
Connector
Connector并不陌生, 在server.xml中, 我们就配置了一个Connector标签
文件名: F:/code/tomcat/conf/server.xml
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
我们配置了一个8080端口的Connector, 就代表我们要暴露一个8080端口去让socket接受数据, 然后指定了protocol为HTTP/1.1, 我们看看Connector的一个构造方法
类名: org.apache.catalina.connector.Connector
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
try {
Class<?> clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
}
在构造方法中, 首先调用setProtocol(protocol)方法设置protocol. 在前面的博客中我们就说过, 这个属性值取HTTP/1.1的时候, 最终会被设置为org.apache.coyote.http11.Http11Protocol, 就是将protocolHandlerClassName这个属性设置为这个类名.
类名: org.apache.catalina.connector.Connector
public void setProtocol(String protocol) {
if (AprLifecycleListener.isAprAvailable()) {
// 省略亿行代码
} else {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11Protocol"); // BIO
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol); // org.apache.coyote.http11NIOProxot
}
}
}
将protocolHandlerClassName属性设置之后, 再使用Class.forName方法获取到这个类的类对象, 也就是Http11Protocol类的类对象, 再使用默认构造方法创建了Http11Protocol的对象, 赋值给Connector对象的protocolHandler属性.
至于Http11Protocol的构造方法, 第二篇学习笔记已经提过了, 它将设置2个属性: endPoint(JIoEndpoint, cHandler(Http11ConnectionHandler), 将Http11Protocol这个对象自身传给cHandler的proto属性, 然后再将这个cHandler对象赋值给endPoint的handler属性.
ServerSocket是如何创建出来的
由于Connector是继承了org.apache.catalina.util.LifecycleMBeanBase这个抽象类的, 因此tomcat在启动的时候, 会调用Connector的startInternal()方法, 在这个startInternal中调用了protocolHandler的start方法, 我们看看在Http11Protocol中的start方法. 发现没有, 所以应该在父类中.
类名: org.apache.coyote.AbstractProtocol
@Override
public void start() throws Exception {
if (getLog().isInfoEnabled())
getLog().info(sm.getString("abstractProtocolHandler.start",
getName()));
try {
// 启动endpoint
endpoint.start();
} catch (Exception ex) {
getLog().error(sm.getString("abstractProtocolHandler.startError",
getName()), ex);
throw ex;
}
}
在启动EndPoint中, 有一个bind方法, 在bind方法中就会根据我们配置的端口号去创建ServerSocket
类名: org.apache.tomcat.util.net.DefaultServerSocketFactory
public ServerSocket createSocket (int port, int backlog)
throws IOException {
return new ServerSocket (port, backlog);
}
这个方法是在JioEndpoint中调用的, 返回的serverSocket会赋值给JIoEndPoint的serverSocket属性. 接下来tomcat就会启动Acceptor线程, 不停地从这个serverSocket中去accept新的Socket.
处理Socket中的数据
多余的就不用说了, tomcat几经周折, 终于来到了Http11Processor的process方法中, 但是实际上并没有这个方法, 这个方法是个通用方法, 在AbstractHttp11Processor中. 我们详细来看看这个方法
绑定输入流
类名: org.apache.coyote.http11.AbstractHttp11Processor
方法名: public SocketState process
// 将socket的InputStream与InternalInputBuffer进行绑定
getInputBuffer().init(socketWrapper, endpoint);
// 将socket的OutputStream与InternalOutputBuffer进行绑定
getOutputBuffer().init(socketWrapper, endpoint);
process方法一开始就将socket的输入流和输出流绑定到processor对象的inputBuffer和outputBuffer上, 这是什么意思呢? 在第二篇笔记中说过, socket本身是有一个sendBuffer和recvBuffer的, 对于tomcat取数据的过程来说, 就是从socket的revcBuffer中去取数据, 但是tomcat自身也还有一个缓冲区, 这个缓冲区就是AbstractInputBuffer, 在BIO中就是它的子类InternalInputBuffer. 我们可以看看AbstractInputBuffer中有哪些核心的属性.
类名: org.apache.coyote.http11.AbstractInputBuffer
// Pointer to the current read buffer.
protected byte[] buf;
// Last valid byte.
protected int lastValid;
// Position in the buffer.
protected int pos;
在BIO的AbstractInputBuffer实现类InternalInputBuffer中, 有这么一个属性
类名: org.apache.coyote.http11.InternalInputBuffer
private InputStream inputStream;
前文提到, socket的输入输出流都会和一个InternalXxxPutBuffer绑定, 而且tomcat从socket的recvBuffer中取到的数据, 也会存到tomcat自己的缓冲区中, 这个缓冲区就是buf属性. 这是一个字节数组, 这个字节数组是在InternalInputBuffer的构造函数中初始化的. 大小是8192
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public InternalInputBuffer
public InternalInputBuffer(Request request, int headerBufferSize,
boolean rejectIllegalHeaderName, HttpParser httpParser) {
this.request = request;
headers = request.getMimeHeaders();
// 请求头的缓冲区域大小,一个请求的请求头数据不能超过这个区域,默认为8192,也就是8*1024个字节=8kb
buf = new byte[headerBufferSize];
this.rejectIllegalHeaderName = rejectIllegalHeaderName;
this.httpParser = httpParser;
inputStreamInputBuffer = new InputStreamInputBuffer();
filterLibrary = new InputFilter[0];
activeFilters = new InputFilter[0];
lastActiveFilter = -1;
parsingHeader = true;
swallowInput = true;
}
这个8192从哪里来的呢? 实际上这是在第二篇笔记中的点, buf是在InternalInputBuffer的构造函数中初始化的, size是构造方法传进来的参数, 而InternalInputBuffer是在Http11Processor的构造函数中初始化的, size也是传进来的参数, 而Http11Processor是在Http11Protocol的createProcessor方法中初始化的, size是protocol的一个属性, 这个属性就是8192:
类名: org.apache.coyote.http11.AbstractHttp11Protocol
// Maximum size of the HTTP message header.
private int maxHttpHeaderSize = 8 * 1024;
buf的来历清楚了, buf是怎么使用的呢? 实际上tomcat就是直接从inputStream中调用read方法, 将输入流读入这个字节数组中的:
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: protected boolean fill(boolean block) throws IOException
nRead = inputStream.read(buf, pos, buf.length - lastValid);
if (nRead > 0) {
lastValid = pos + nRead; // 移动lastValid
}
这段代码会出现在fill方法中, fill方法就是真正的去将socket中的输入流读取到tomcat自己的缓冲区中的核心方法. 从上面那段代码可以看到, 使用了pos, lastValid两个属性. pos就代表从哪里开始读取的socket中的数据, lastValid代表这次读取的数据在buf中哪个坐标结束的. 画个图理解一下.
如图所示, 每次fill的时候, lastValid都是等于pos的, 实际上tomcat的代码中的判断是pos>=lastValid. 下标从pos开始, 读取inputStream的数据, 读完之后让lastValid标记到读完的位置. 每次读取的数据量都是不确定的, 因为现实中会存在很多原因导致服务端的socket的输入流中并不一定是整个请求的数据, 比如网速的原因, 比如客户端自己限制的原因, 因此这个fill方法会调用多次.
案例
举个例子, 比如我现在有这么一个客户端
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8080);
String request1 = "POST /HelloServlet/servletDemo HTTP/1.1\n" +
"Content-Type: application/json\n" +
"User-Agent: PostmanRuntime/7.28.4\n" +
"Accept: */*\n" +
"Host: 127.0";
String request2 =
".0.1\nPostman-Token: ce27f37f-d2ef-4a72-ab51-1b77bfd92aa1\n" +
"Accept-Encoding: gzip, deflate, br\n" +
"Connection: keep-alive\n" +
"Content-Length: 38\n" +
"\n" +
"{\"name\":\"法外狂徒张三\",\"age\":35}";
socket.getOutputStream().write(request1.getBytes());
TimeUnit.SECONDS.sleep(5);
socket.getOutputStream().write(request2.getBytes());
Thread.sleep(300000);
}
这个客户端就自己限制了发送给tomcat的数据是分两段发送, 而且是毫无规则的拆分: 特意将请求头的Host拆分成127.0和.0.1, 我们就拿这个例子来分析tomcat是如何解决这种奇葩客户端(或者是因为网速导致请求在一个socket中被分段了)的输入的.
第一次调用fill
首先要明确一点, 除非是 "连体"并且上一次请求是Connection=keep-alive 的请求, 否则入口都会是processor的process方法. 进入process方法之后, 会在while(keepAlive)这个循环中去处理这个socket. 在这个循环中, 首先调用的是 setRequestLineReadTimeout() 方法. 这个方法首先是设置了keep-alive的超时时间, 我们可以看看是如何设置的.
1. keep-alive超时时间
类名: org.apache.coyote.http11.Http11Processor
方法名: protected void setRequestLineReadTimeout() throws IOException
// 如果长连接没有超时时间,那么从socket中读数据也没有超时时间
if (keepAliveTimeout == -1) {
firstReadTimeout = 0;
} else {
// 一个socket在被处理之前会调用一下access方法,所以queueTime表示的是socket创建好了到真正被处理这段过程的排队时间
long queueTime =
System.currentTimeMillis() - socketWrapper.getLastAccess();
// 如果排队时间大于keepAliveTimeout,表示该socket已经超时了不需要被处理了,设置一个最小的超时时间,当从这个socket上读取数据时会立刻超时
if (queueTime >= keepAliveTimeout) {
// Queued for longer than timeout but there might be
// data so use shortest possible timeout
firstReadTimeout = 1;
} else {
// Cast is safe since queueTime must be less than
// keepAliveTimeout which is an int
// 如果排队时间还没有超过keepAliveTimeout,那么第一次从socket中读取数据的超时时间就是所剩下的时间了
firstReadTimeout = keepAliveTimeout - (int) queueTime;
}
}
// 设置socket的超时时间,然后开始读数据,该时间就是每次读取数据的超时时间
socketWrapper.getSocket().setSoTimeout(firstReadTimeout);
首先lastAccess这个属性在SocketWrapper创建的时候就会初始化为当前时间戳. 这里是第一次对socketWrapper真正的进行处理的方法, 所以这个long queueTime = System.currentTimeMillis() - socketWrapper.getLastAccess();其实就是使用当前时间戳, 减去线程池调度的时间获得的排队时间.
1.1 keepAliveTimeout赋值
关于这个keepAliveTimeout, 我们可以看看在哪里初始化的. 实际上如果我们不专门给keepAliveTimeout设置值, 它取值为soTimeout, 那这个soTimeout是什么呢? 实际上就是server.xml中Connection标签中的connectionTimeout.
类名: org.apache.catalina.connector.Connector
static {
replacements.put("acceptCount", "backlog");
replacements.put("connectionLinger", "soLinger");
replacements.put("connectionTimeout", "soTimeout");
replacements.put("rootFile", "rootfile");
}
在Connection类中, 给标签取了别名, 这个别名应对给protocol实现类的对象设置属性值.
类名: org.apache.catalina.connector.Connector
public boolean setProperty(String name, String value) {
String repl = name;
if (replacements.get(name) != null) {
repl = replacements.get(name);
}
return IntrospectionUtils.setProperty(protocolHandler, repl, value);
}
我们要找的这个keepAliveTimeout属性, 实际上是Http11Processor的一个属性值, 这个属性值是在创建processor的时候初始化的, 所以我们找到创建processor的代码中, 之前已经看过了, 就是在Http11Protocol中的createProcessor方法中创建的, 将keepAliveTimeout赋值为protocol中getKeepAliveTimeout方法的返回值. 该方法最终会判断endpoint中的keepAliveTimeout的属性值是否为空, 为空就返回soTimeout. 至此, 我们搞清楚了keepAliveTimeout是怎么来的, 它的默认值tomcat在默认server.xml中配置的是20000, 也就是20秒.
接着前面queueTime开始讲, 前文提到queueTime就是BIO当前socket在线程池中经历了被调度到现在的时间, 这个时间一般来说是比较短的, 正常情况下肯定不会超过1秒吧. 拿到这个queueTime, 就判断queueTime>=keepAliveTimeout, 如果已经超过了keepAliveTimeout, 就直接设置firstReadTimeout为1, 再设置给socket的soTimeout属性. 这样socket读取就会立即超时(这应该是C语言的写法), queueTime没超过keepAliveTimeout, 就设置firstReadTimeout为keepAliveTimeout-queueTime, 设置给socket的soTime属性, 表示socket还剩下这么多的时间去处理了.
设置了socket的读取超时时间, 那么接下来就可以安心的从socket的inputStream中读取数据了. 使用inputBuffer(InternalInputBuffer).fill()方法, 从inputStream中read数据到buf(byte[8192])中, 看看这个方法具体是怎么处理的.
类名: org.apache.coyote.http11.InternalInputBuffer
protected boolean fill(boolean block) throws IOException {
int nRead = 0;
if (parsingHeader) {
// 如果还在解析请求头,lastValid表示当前解析数据的下标位置,如果该位置等于buf的长度了,表示请求头的数据超过buf了。
if (lastValid == buf.length) {
throw new IllegalArgumentException
(sm.getString("iib.requestheadertoolarge.error"));
}
// 从inputStream中读取数据,len表示要读取的数据长度,pos表示把从inputStream读到的数据放在buf的pos位置
// nRead表示真实读取到的数据
nRead = inputStream.read(buf, pos, buf.length - lastValid);
if (nRead > 0) {
lastValid = pos + nRead; // 移动lastValid
}
} else {
// 当读取请求体的数据时
// buf.length - end表示还能存放多少请求体数据,如果小于4500,那么就新生成一个byte数组,这个新的数组专门用来盛放请求体
if (buf.length - end < 4500) {
// In this case, the request header was really large, so we allocate a
// brand new one; the old one will get GCed when subsequent requests
// clear all references
buf = new byte[buf.length];
end = 0;
}
pos = end;
lastValid = pos;
nRead = inputStream.read(buf, pos, buf.length - lastValid);
if (nRead > 0) {
lastValid = pos + nRead;
}
}
return (nRead > 0);
调用fill方法会走到fill(boolean block)方法中, BIO中这里传递的是true. 首先判断是否正在解析请求头parsingHeader==true, 这个参数是AbstractInputBuffer中的属性, 子类InternalInputBuffer继承了这个属性, 并且在子类InternalInputBuffer的构造函数初始化了这个属性, 赋值为true. 所以在第一次读取socket的时候, 这个值是true, 将会进入if(parsingHeader)的上面那个分支.
进入分支后, 首先判断lastValid==buf.length, 实际上是因为这个fill方法本身就是为了读取socket的数据到buf中的, 所以肯定是不止调用一次的, 这里大概意思就是, 如果还在解析请求头, 并且请求头+请求行的大小已经超过了8192, 那么直接就抛异常iib.requestheadertoolarge.error. 如果一切正常, 则直接调用inputStream的read方法, 从pos(第一次进入自然是0), 读取buf.length-lastValid个字节到buf中去. 这时候lastValid其实也是0, 因为是第一次读取inputStream, 所以这里就是从buf字节数组的0下标开始, 从inputStream中读取8192个字节到buf字节数组中, 当然这就要看inputStream中有没有那么多了, 如果inputStream中的数据小于8192个字节, 那么将会直接全部读入到buf这个字节数组中, 返回读取的字节数组, 再加上pos, 赋值给lastValid, 标记当时的buf中是从pos->lastValid这一段是本次读取的数据.
fill方法读取成功(读出的数据量大于0字节)之后, 会返回true, 之后在setRequestLineReadTimeout方法的最后重新设置socket的读取超时时间
类名: org.apache.coyote.http11.Http11Processor
方法名: protected void setRequestLineReadTimeout() throws IOException
// 当第一次读取数据完成后,设置socket的超时时间为原本的超时时间
if (endpoint.getSoTimeout()> 0) {
setSocketTimeout(endpoint.getSoTimeout());
} else {
setSocketTimeout(0);
}
上面的方法结束以后, 就开始真正的去从buf(byte[8192])中正式的解析请求行和请求头到Request对象中了.
2. Request对象的来历
解析请求行和请求头, 最终都会设置到request对象中去, 这个request对象从哪里来的呢? 实际上就是在创建Http11Processor的时候, 会调用抽象父类的AbstractProcessor的构造方法, 在抽象父类的构造方法中, new出了request对象为org.apache.coyote.Request和response对象为org.apache.coyote.Response, 然后在Http11Processor具体的processor实现类的构造方法中, new出了InternalInputBuffer和InternalOutputBuffer, 再分别将request和response对象通过InternalXxxputBuffer的构造函数传进去.
类名: org.apache.coyote.http11.Http11Processor
public Http11Processor(int headerBufferSize, boolean rejectIllegalHeaderName,
JIoEndpoint endpoint, int maxTrailerSize, Set<String> allowedTrailerHeaders,
int maxExtensionSize, int maxSwallowSize, String relaxedPathChars,
String relaxedQueryChars) {
super(endpoint);
httpParser = new HttpParser(relaxedPathChars, relaxedQueryChars);
inputBuffer = new InternalInputBuffer(request, headerBufferSize, rejectIllegalHeaderName,
httpParser);
request.setInputBuffer(inputBuffer);
outputBuffer = new InternalOutputBuffer(response, headerBufferSize);
response.setOutputBuffer(outputBuffer);
// 初始化过滤器,这里不是Servlet规范中的Filter,而是Tomcat中的Filter
// 包括InputFilter和OutputFilter
// InputFilter是用来处理请求体的
// OutputFilter是用来处理响应体的
initializeFilters(maxTrailerSize, allowedTrailerHeaders, maxExtensionSize, maxSwallowSize);
}
解析请求行(第2~n次调用fill)
类名: org.apache.coyote.http11.AbstractHttp11Processor
方法名: public SocketState process
// 第一次从socket中读取数据,并设置socket的读取数据的超时时间
// 对于BIO,一个socket连接建立好后,不一定马上就被Tomcat处理了,其中需要线程池的调度,所以这段等待的时间要算在socket读取数据的时间内
// 而对于NIO而言,没有阻塞
setRequestLineReadTimeout();
// 解析请求行
if (!getInputBuffer().parseRequestLine(keptAlive)) {
if (handleIncompleteRequestLineRead()) {
break;
}
}
在NIO中, 上面的parseRequestLine方法不可能返回false, 要么直接抛异常, 要么就是返回true, 因此不会进入if分支里面. 在parseRequestLine方法中, 会解析请求行, 我们一步步看看是怎么解析的
第一步: 跳过请求行前的空格
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
int start = 0;
//
// Skipping blank lines
//
byte chr = 0;
do {
// 把buf里面的字符一个个取出来进行判断,遇到非回车换行符则会退出
// Read new bytes if needed
// 如果一直读到的回车换行符则再次调用fill,从inputStream里面读取数据填充到buf中
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Set the start time once we start reading data (even if it is
// just skipping blank lines)
if (request.getStartTime() < 0) {
request.setStartTime(System.currentTimeMillis());
}
chr = buf[pos++];
} while ((chr == Constants.CR) || (chr == Constants.LF));
pos--;
// Mark the current buffer position
start = pos;
上述代码是第一步: 跳过请求前的空格. 在dowhile循环中, 使用局部变量byte类型的chr来接受buf在pos下标的字节值, 一个一个的判断这个字节值是不是CR(\r)或者是LF(\n), 如果是, 就忽略, pos+1, 继续循环, 如果当pos>=lastValid的时候, 就说明buf中的数据已经全部都判断过了, 都是空格, 那么就会调用fill()方法继续从socket的inputStream中读取数据到buf[8192]中, 这时候pos一般都是=lastValid的, 读取数据之后, 又会将buf使用pos->lastValid标记本次读取到的数据在buf中的下标范围. 一直到某个字节值不是CR也不是LF, 就退出循环, 因为退出循环之前的那一次pos被+1了, 所以退出循环后, 让pos-1, 并且设置局部变量int类型的start=pos, 标记下次解析数据的起始位置. 这时候pos一定是<=lastValid的
第二步: 解析请求方法
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
boolean space = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says method name is a token followed by a single SP but
// also be tolerant of multiple SP and/or HT.
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
request.method().setBytes(buf, start, pos - start);
} else if (!HttpParser.isToken(buf[pos])) {
throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
}
pos++;
}
前面已经判断完空格了, 到了第二部, 这时候start=pos, 而且start和pos的位置也是本次请求buf中第一次不是空格的地方. 也就是请求方法的地方, 首先定义局部变量space=false, 代表不是空格, 在while(!space)循环中, 也就是说只要不是空格就不会跳出这个循环, 老样子, 先判断pos>=lastValid, 看看是不是要继续从inputStream中读取数据到buf[8192]中, 然后继续不断的判断buf[pos]是不是等于' '或者\t, 如果不是就继续循环, pos+1, 如果空格或者\t, 就代表buf[8192]中, 在这个空格(\t)的位置(pos)之前, 一直到start的位置, 就是请求方法了. 调用request.method().setBytes(buf, start, pos - start)将buf中从start开始写入pos-start个字节到request.method()中(实际上tomcat并没有这样做, tomcat是做一个标记, 后文将会详细说明, 暂时可以直接理解为将这段字节设置为request的method属性).
这里可能会有个疑问: tomcat这么解析就只能解析出GET, 如果请求行传过来, 请求方法是GE T, 那岂不是将请求方法解析成了GE, 然后后面的URI就会被解析成T了吗? 对, 确实是这样没错, 因为如果你请求方法传过来是个GE T, 就说明你这个请求都没有遵守HTTP1.1协议规范, tomcat为什么要管你呢?
第三步: 解析完请求方法后继续跳过空格
根据HTTP1.1协议规范, 请求方法后面可以加空格符制表符(不限个数), 过来是请求URI, 大概这样: GET\t\t\t\t/HelloServlet/servletDemo\t\t\tHTTP/1.1. 因此tomcat在解析完请求方法之后要跳过这些空格, 代码就直接粘贴了
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
// Spec says single SP but also be tolerant of multiple SP and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
代码和之前很像, 先判断是不是需要fill, 然后再在while(space)循环中判断buf[8192]在pos下标的位置的字节值是不是' '或者\t, 如果是, 就pos+1, 如果不是, 就标记space=false, 之后会跳出循环. 跳出循环之后, pos的位置就是请求方法之后的第一个不是' '或者\t的位置, 然后start=pos, 标记下面解析请求URI的起始位置.
第四步: 解析请求URI
解析请求URI会比较复杂, 因为需要兼容HTTP/0.9协议, 在HTTP/0.9协议中, 请求行大概长这样 GET\t\t\t/HelloServlet/servletDemo, 然后就直接换行, 是没有协议版本的. 因此这里增加了一个boolean类型的局部变量eol, 也就是end of line, 初始值为false, 标记解析完请求URI之后, 是否已经到了请求行的末尾了. 而questionPos则是另一个标记, 比如http://localhost:8080/HelloServlet/servletDemo?name=1&age=2, ?后面就是queryString, 因此增加一个questionPos做一个标记, 来帮助解析queryString. 粘贴代码, 下面分析这坨代码的作用.
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
int end = 0;
int questionPos = -1;
// Reading the URI
boolean eol = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says single SP but it also says be tolerant of HT
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
end = pos;
} else if ((buf[pos] == Constants.CR) || (buf[pos] == Constants.LF)) {
// HTTP/0.9 style request
eol = true;
space = true;
end = pos;
} else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
questionPos = pos;
} else if (questionPos != -1 && !httpParser.isQueryRelaxed(buf[pos])) {
// %nn decoding will be checked at the point of decoding
throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
} else if (httpParser.isNotRequestTargetRelaxed(buf[pos])) {
throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
}
pos++;
}
request.unparsedURI().setBytes(buf, start, end - start);
if (questionPos >= 0) {
request.queryString().setBytes(buf, questionPos + 1, end - questionPos - 1);
request.requestURI().setBytes(buf, start, questionPos - start);
} else {
request.requestURI().setBytes(buf, start, end - start);
}
1. 全量URI: unparsedURI
老样子, 先判断是否需要fill, 如果需要, 就fill. 因为根据HTTP协议规范, 请求方法/请求URI/请求协议版本字符串中途是不允许有' '和\t的, 所以这个循环依然是在while(!space)中进行的. 循环中判断buf[8192]在pos下标的字节值是不是' '或者\t, 如果是就跳出循环, 标记end=pos, space=true(方便后面循环跳过空格), 代表从start到end这个范围就是请求URI的范围, 将其设置进request对象的unparsedURI属性中. 这个unparsedURI, 顾名思义, 就是没有被解析的URI, 也就是全量URI, 大概长这样: /HelloServlet/servletDemo?name=1&age=2, 是包含了queryString的.
2. HTTP/0.9的处理
如果读取过程中, 碰到的并不是' '和\t, 而是\r或者\n, 就代表回车或者换行了, 也就是说, 在解析请求URI的过程中竟然出现了回车换行操作, 说明这是HTTO/0.9协议. 则标记eol=true, end=pos, space=true(方便后面循环跳过空格), 跳出循环, 进而也是将这个全量URI设置到unparsedURI属性中, 与HTTP/1.X不同的是, eol=true, 代表已经是请求行的末尾, 后面将会根据这个eol的值来决定是否还需要解析协议版本.
3. queryString的处理
在第三和第四个if分支中
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
questionPos = pos;
} else if (questionPos != -1 && !httpParser.isQueryRelaxed(buf[pos])) {
// %nn decoding will be checked at the point of decoding
throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
}
上面的分支判断buf[8192]在pos的位置是不是?, 如果是, 就设置questionPos=pos, 标记?的位置是questionPos, 在下面的分支中, 判断questionPos是否是-1, 如果不是, 说明就有queryString, 就会调用httpParser.isQueryRelaxed(buf[pos])方法, 这个方法返回的是个boolean类型, 根据名字来看, 这个方法好像是判断这个查询是否是个轻松的查询, 听起来很奇葩, 实际上这个可以理解为查询的string是否符合要求, 里面有点小复杂, 所以我也没看, 大致意思是没错的, 就是看这个queryString是否是个轻松的查询, 也就是判断这个queryString是否符合规范. 拿到标记?位置的questionPos, 又有了全量URI的end位置, 那么queryString的起点自然是questionPos+1, 长度自然就是end-questionPos-1, 那么requestURI的起点自然就是start, 长度是questionPos-start. 因此:
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
if (questionPos >= 0) {
request.queryString().setBytes(buf, questionPos + 1,
end - questionPos - 1);
request.requestURI().setBytes(buf, start, questionPos - start);
} else {
request.requestURI().setBytes(buf, start, end - start);
}
判断questionPos>=0, 如果是, 就说明有queryString, 然后就设置requestURI和queryString, 如果false, 就说明没有queryString, 那么requestURI就是全量URI了.
第五步: 解析完请求URI后继续跳过空格
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
// Spec says single SP but also says be tolerant of multiple SP and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
老规矩, 判断是否需要fill, 如果需要, 就fill. 继续判断是否是' '或者\t, 遇到不是的, 就说明pos已经到了协议版本的下标了, 跳出循环, 开始解析协议版本
第六步: 解析协议版本
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
end = 0;
// Reading the protocol
// Protocol is always "HTTP/" DIGIT "." DIGIT
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.CR) {
end = pos;
} else if (buf[pos] == Constants.LF) {
if (end == 0)
end = pos;
eol = true;
} else if (!HttpParser.isHttpProtocol(buf[pos])) {
throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
}
pos++;
}
if ((end - start) > 0) {
request.protocol().setBytes(buf, start, end - start);
} else {
request.protocol().setString("");
}
return true;
1. \r和\n的不同
在计算机中, \r代表的是回车符, 也就是回到行首, 但是不会换行, 而\n就是换行, 但是不会回到行首. 所以\r\n连起来, 就是回车加换行.
但是在HTTP协议(或者说tomcat本身的判断??)中, 并没有这么严格的要求, 只要是\n, 就算是一行, 因为综合代码来看, 凡是判断\r的tomcat都是选择跳过(或者说并没有执行什么关键性的逻辑), 每次都是靠\n来判断行结尾的. 即使没有\r只有\n也会被判定为eol.
前文中说过, eol就是end of line的意思, 当还没有到达请求行的末尾的时候, 就不停的while(!eol)中去读取数据, 这时候读取的数据一定就是请求协议版本了. 当遇到\r的时候, 就把pos的值赋值给end, 也就是说, end是在读取请求协议版本时, 第一次遇到\r的位置, 但是此时还不会跳出循环, 因为eol还是false, 所以会继续循环, 直到遇到\n换行符时, 会判断此前是否遇到过\r, 如果没有遇到过, end会是=0的, 然后将pos赋值给end, 让end标记为第一次遇到\n的位置. 遇到\n无论如何都会设置eol=true, 就代表到了请求行的末尾了.
2. 判断字节值是否是协议版本
由于协议版本一定是HTTP/X.X, X是数字, 所以tomcat直接写了一套判断当前buf[8192]中在pos下标的字节值是不是协议版本中的字符. 也就是!HttpParser.isHttpProtocol(buf[pos])
类名: org.apache.tomcat.util.http.parser.HttpParser
public static boolean isHttpProtocol(int c) {
// Fast for valid HTTP protocol characters, slower for some incorrect
// ones
try {
return IS_HTTP_PROTOCOL[c];
} catch (ArrayIndexOutOfBoundsException ex) {
return false;
}
}
伪代码
类名: org.apache.tomcat.util.http.parser.HttpParser
private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE];
// Not valid for HTTP protocol
// "HTTP/" DIGIT "." DIGIT
static {
if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
IS_HTTP_PROTOCOL[i] = true;
}
}
所以只要不是H, T, P, /, ., 0~9的字节值, 都会返回false, 抛异常. 解析完协议版本后, start就是协议版本在buf[8192]中的开始的位置, end要么就是0(HTTP/0.9), 要么就是第一次遇到\r或者\n的位置. 因此直接设置request对象的protocol的值
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: public boolean parseRequestLine(boolean useAvailableDataOnly)
if ((end - start) > 0) {
request.protocol().setBytes(buf, start, end - start);
} else {
request.protocol().setString("");
}
第七步: 结束解析请求行
结束直接返回true. 因此parseRequestLine方法只可能返回true, 否则就会抛异常, 并不存在返回false的情况.
解析请求头(第n~m次调用fill)
解析完请求行之后, 会有一些小判断, 比如如果endpoint被暂停了, 就返回503: Service unavailable. 并设置keptAlive=true, 设置请求头和cookie的最大限制. 然后才开始调用getInputBuffer().parseHeaders(). 我们看看parseHeaders是怎么解析的.
类名: org.apache.coyote.http11.InternalInputBuffer
public boolean parseHeaders() throws IOException {
if (!parsingHeader) {
throw new IllegalStateException(
sm.getString("iib.parseheaders.ise.error"));
}
while (parseHeader()) {
// Loop until we run out of headers
}
parsingHeader = false;
end = pos;
return true;
}
首先判断是不是正在解析header, 因为这时候刚进来, 也是刚刚解析完请求行, 所以这里进来一定是true. 则不会进入抛异常的分支. 然后while循环调用parseHeader(), 只有当parseHeader返回false才会终止循环. 当循环解析完header之后, 设置parsingHeader为false, 并且标记end=pos, 需要注意的是, 这个end并不是局部变量, 这个end是InternalInputBuffer的成员变量, 专门用来标记buf[8192]中请求头结束的位置, 也正是请求体开始的位置. 标记完之后, 返回true. 下面我们来看看parseHeader方法的执行流程.
可以先说一个结论, 这个方法每次解析成功一次请求头, 都会返回true, 并在外部的while循环中继续调用本方法解析下一个请求头, 直到刚开始解析请求就只遇到多个\r一个\n或者只遇到\n返回false, 然后外部调用的地方跳出while循环.
第一步: 判断是否还有请求头要解析
进入parseHeader()方法后, 先定义一个局部变量byte chr = 0, 接着就是一个while(true)循环
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: private boolean parseHeader() throws IOException
byte chr = 0;
while (true) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
chr = buf[pos];
if (chr == Constants.CR) { // 回车
// Skip
} else if (chr == Constants.LF) { // 换行
pos++;
return false;
// 在解析某一行时遇到一个回车换行了,则表示请求头的数据结束了
} else {
break;
}
pos++;
}
// Mark the current buffer position
int start = pos;
老规矩, 如果pos>=lastValid, 就需要fill.
这段代码就是开始解析某个请求头的时候, 判断看看还有没有要解析的请求头. 先判断是否有\r, 如果有就跳过, pos+1, 继续判断下一个, 如果一直出现\r就一直跳过一直pos+1, 直到遇到\n, 就说明没有请求头要解析了, 就返回false, 如果不是\r也不是\n, 就说明这是个请求头, 将跳出这个循环, 开始正式的解析请求头.
跳出循环之后, 设置局部变量start=pos, 标记为这个请求头的开始在buf[8192]中的下标位
第二步: 解析请求头Key
1 解析请求头key
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: private boolean parseHeader() throws IOException
// colon是冒号的意思
boolean colon = false;
MessageBytes headerValue = null;
while (!colon) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.COLON) {
colon = true;
headerValue = headers.addValue(buf, start, pos - start);
} else if (!HttpParser.isToken(buf[pos])) {
// Non-token characters are illegal in header names
// Parsing continues so the error can be reported in context
// skipLine() will handle the error
skipLine(start);
return true;
}
chr = buf[pos];
if ((chr >= Constants.A) && (chr <= Constants.Z)) {
buf[pos] = (byte) (chr - Constants.LC_OFFSET);
}
pos++;
}
// Mark the current buffer position
start = pos;
HTTP协议中请求头的写法是KEY:VALUE, 因此解析请求头, 首先拿到的是请求头Key, 所以就定义一个boolean类型的局部变量colon=false, 表示当前pos下标的buf中的字节值不是冒号, 使用while(!colon)循环一直检查buf[pos]的字节值, 如果不是冒号, 而是token, 跳过这一行, 并做一系列操作让pos移动到正确的下一个请求头的开始位置, 并且返回true. 如果既不是冒号, 也不是token, 就说明是个合理的请求头key的字符, 就将大写字符转成小写字符, 并且pos+1, 继续判断下一个字节值. 当遇到:的时候, 标记colon=true, 此时start是本次请求头的起始位置, pos就是:的前一个位置(也就是请求头Key的最后一个字符的位置), 于是调用headerValue = headers.addValue(buf, start, pos - start), 添加一个请求头, 返回一个MessageBytes赋值给while外层定义的一个局部变量headValue.
结束解析请求头key之后, 赋值当前的pos给start, 标记本次请求头value的起始位置.
2. isToken是什么意思
1中说如果当前pos下标处的字节值是token, 就会跳过这一行. 其实就是判断这个字符是不是一个特殊字符, 比如control字符, 也就是ASCII码中0-31的字符, 以及是不是符号类字符, 也就是,.{}()这种字符.
3. addValue方法怎么理解
我们可以看到, addValue方法是使用headers调用的, headers是一个InternalInputBuffer的成员变量, 它是一个MimeHeaders类型的变量, 在InternalInputBuffer的构造函数中赋值给headers的, 来源是org.apache.coyote.Request中定义的成员变量.
类名: org.apache.coyote.Request
private MimeHeaders headers = new MimeHeaders();
headers是用来保存从inputStream中解析出来的header的, 底层保存数据封装的类型为MimeHeaderField
类名: org.apache.tomcat.util.http.MimeHeaders
private MimeHeaderField[] headers = new MimeHeaderField[DEFAULT_HEADER_SIZE];
因此一个header被解析的headerKey和headerValue都会被封装到一个叫做MimeHeaderField的对象中, 保存在headers的内部数组内. 在1中的代码中, 调用addValue, 就会创建一个MimeHeaderField的对象, 然后将headerKey设置到该对象中, 并且将该对象的value属性返回出去, 用于后续读取值之后再设置进去.
前面讲过, 这个headers来源就是Request对象request, java中对象的引用都是指针引用, 所以上面的addValue方法已经将该请求头的MimeHeaderField对象放入了request对象中.
第四步: 解析请求头value
解析请求头value比较特别, 因为请求头value可以跨越多行, 跨越多行的时候, 行首必须以' '或者\t开头. 比如这样
// 在\n后面还有\t, 会被认为是同一个请求头的value, 并且值是保留\t并不保留\n
Postman-Token: ce27f37f-d2ef-4a72\n\t-ab51-1b77bfd92aa1\n
但是如果只有\n没有' '或者\t的话, 就会忽略该请求头value的\n后面的部分.
// 会忽略\n后面的-ab51-1b77bfd92aa1
Postman-Token: ce27f37f-d2ef-4a72\n-ab51-1b77bfd92aa1\n
由于请求头的value可以跨越多行, 所以必须使用双层循环去做 首先定义一个boolean类型的局部变量validLine=true, 标记这个请求头value是否是一个跨越多行的value, 使用while(validLine)来循环, 如果发现不是一个跨越多行的value, 就退出while循环. 在while循环外层定义boolean类型的局部变量eol=false, 表示是否解析到了行末尾. 当buf[pos]=\n的时候, eol=true. 在while(!eol)中去循环检查buf[pos++]. 具体代码就不用细看了, 过程跟上面也差不多. 都是先跳过空格, 然后再在while循环中解析数据, 当遇到\n时退出循环.
类名: org.apache.coyote.http11.InternalInputBuffer
方法名: private boolean parseHeader() throws IOException
// Set the header value
headerValue.setBytes(buf, start, realPos - start);
最后确定请求头value的起始和末尾后, 就设置到之前返回的headerValue中. 也就自然被设置进request对象的headers属性中了.
MessageBytes
在前面我们看到, 不论是解析请求行还是解析请求头, 都是用一个setBytes方法去设置:
request.method().setBytes(buf, start, pos - start);
request.requestURI().setBytes(buf, start, questionPos - start);
request.queryString().setBytes(buf, questionPos + 1, end - questionPos - 1);
headerValue.setBytes(buf, start, realPos - start);
这到底是什么玩意儿呢? 实际上, setBytes就是MessageBytes中的方法, 入参有3个: 一个是字节数组byte[] buf, 一个int类型的起始标记start, 然后一个int类型的长度length.
ByteChunk字节块
类名: org.apache.tomcat.util.buf.MessageBytes
private final ByteChunk byteC=new ByteChunk();
在MessageBytes中有一个私有final属性的ByteChunk类型的成员变量byteC, 这个类型叫做字节块. setBytes方法正是对这个属性进行操作. 这个属性在创建对象之后就会初始化new一个, 然后我们看看MessageBytes的setBytes方法.
类名: org.apache.tomcat.util.buf.MessageBytes
public void setBytes(byte[] b, int off, int len) {
byteC.setBytes( b, off, len );
type=T_BYTES;
hasStrValue=false;
hasHashCode=false;
hasIntValue=false;
hasLongValue=false;
}
发现是调用了这个私有属性byteC的setBytes方法, 我们看看ByteChunk的setBytes方法
类名: org.apache.tomcat.util.buf.ByteChunk
public void setBytes(byte[] b, int off, int len) {
buff = b;
start = off;
end = start + len;
isSet = true;
hasHashCode = false;
}
发现实际上这个setBytes方法, 其实就是将MessageBytes的内部属性的ByteChunk中的buf指向这个字节数组, 然后使用start=off, end=start+len, 来标记这个MessageBytes的内容是在字节数组中的哪一段. 然后在MessageBytes的setBytes方法中, 内部属性设置type=T_BYTES代表当前的内容值是以byteChunk字节快的方式存储的, hasStrValue, hasHashCode, hasIntValue, hasLongValue此时都是false, 将会在未来真正用到这个数据的时候, 才会从字节数组中转换过去, 然后再更改对应的属性状态值(hasXxxValue=true).
所以org.apache.coyote.Request中的各个属性, 比如请求方法method, 请求URI, 协议版本, 以及内部headers的请求头列表的value(key不太一样但是也差不多)都是MessageBytes, 都只是标记了一下对应属性在buf[8192]中是在哪一段, 一直到真正用到的时候才会去真正转换为具体的类型. 因此可以归结为: 效率原因.
总结
以上就是tomcat如何从socket的inputStream中解析数据到request中的过程了. tomcat自身带一个buf缓冲区, 是一个字节数组, 长度是8192, 调用fill方法会把socket的inputStream的数据read到buf中, pos下标代表本次fill的开始位置, lastValid代表本次fill的结束位置, 调用parseRequestLine方法和parseHeaders方法会解析数据到request对象中, 对象中的属性都是MessageBytes字节块, 仅仅做一个标记作用, 只标记当前属性在buf中是在哪一段, 并不真正解析数据. 在每个while循环中都会在开头判断pos是否>=lastValid, 如果是, 就会再次fill从inputStream中read数据到buf中去.