Tomcat中BIO模式读取数据的原理之长连接

浅析socket

我们在使用Tomcat的时候, 默认都会用8080端口去接收数据. 比如我们启动tomcat, 就可以访问localhost:8080, 就能访问到tomcat默认部署的一个项目

image.png

我在<Tomcat基本使用和整体模块的梳理>文中也说过, Tomcat只是一个用于接收数据的, 将操作系统给来的数据, 读取, 封装到Request中, 进而经过各种阀门, 过滤器的层层处理, 最终走到servlet中去. 读取数据的核心就在于, 是如何将操作系统中给的数据, 解析成java代码中的Request对象.

在研究tomcat解析数据的时候, 可以看到, 数据实际上是从socket中拿出来的. 回忆起BIO中socket的demo, 我们在创建一个Server的时候, 是new一个ServerSocket, 指定一个端口号, 然后再写一个客户端, new一个Socket, 指定IP地址和端口号, 然后拿到OutPutStream, 就可以往服务端发数据了. 通过这个demo, 我们可以知道, socket发送信息, 只需要知道对方的IP地址和端口号就可以了. 而我们知道, 传输层的协议是TCP协议, 我们在使用Socket发送数据的时候, 我们根本没有去按照TCP协议的格式去发送数据, 所以TCP协议肯定是有个"人"已经实现了的, 这个"人"是Socket吗? 我们可以看看new ServerSocket的时候, 方法中做了什么.

image.png

类名: java.net.DualStackPlainSocketImpl
static native int socket0(boolean stream, boolean v6Only) throws IOException;
复制代码

过程中并没有找到任何跟TCP有关的玩意儿, 但是看到了一个有用的信息: 最终通过一个本地方法去拿到一个int类型的文件描述符, socketCreate() -> socket0()[不同操作系统不一样]. 说明Socket的创建最终使用的JNI的方式调用的jvm的方法. 既然只能走到jvm层面, 说明这是一个系统级别的玩意儿. 因此可以推断, TCP协议实际上是操作系统实现的. 而socket就是操作系统为了让其它应用程序使用TCP协议而暴露出的一个类似于接口的东西.

两台计算机通信, 本质上来说就是两方操作系统都实现了TCP协议, 两方操作系统分别暴露自己的端口, 让对方知道自己的端口, 然后自己再通过对方的IP和端口发送数据, 这些数据通过网络传输到对方计算机的网卡中, 然后再通过一系列的操作从网卡进入内存, 而socket正是操作系统给应用程序操作这些数据(读取, 发送)的桥梁.

因此, socket会缓存操作系统接受到的这些数据的. 因为应用程序是我写的, 我可以选择在客户端发送数据之后, 服务端并不读取这些数据, 服务端不读取这些数据, 这些数据就没有了吗? 当然不是, 因此socket中一定会缓存这些数据, 我们不妨试一下.

public class ServerMain {
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8888);
        TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
    }
}
复制代码
public class SocketClientMain {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8888);
        String str = "abc";
        int i = 1;
        while (true) {
            System.out.println(i++);
            socket.getOutputStream().write(str.getBytes());
        }

    }
}
输出结果:
... 省略916693916694
916695
916696
916697
916698
916699 // 前面916698行都是非常快的打印, 但是从这里开始就非常慢了(大约半分钟一次)
复制代码

从输出的情况可见, socket中确实是有个缓存的, 并且这个缓存是有限制的. 实际上Socket自己本身是有2个缓冲区的, 一个叫RecvBuff, 一个叫SendBuff. 对于tomcat接收数据进而转化成Request对象这个过程来说, 实际上也就是从RecvBuff中去取数据.

BIO中一个HTTP请求的socket是如何传入到具体HTTP协议的处理器中的

我们知道, 在BIO的线程模型中, 是一个线程处理一个socket的, 一般我们写代码, 会在accept到一个socket之后, 另起一个线程(或者使用线程池)去处理这个socket, 这样主线程就能回头去accept下一个socket了. tomcat的BIO模型中也是用的这种方法. 我们在上文中提到过, tomcat启动后, 会启动一个线程去不停的接受socket:

类名: org.apache.tomcat.util.net.AbstractEndpoint
public final void start() throws Exception {
    if (bindState == BindState.UNBOUND) {
        bind();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}
复制代码

在AbstractEndpoint中的start()方法中, 调用startInternal()方法, 选择看BIO的代码

image.png

看到调用了startAcceptorThreads()方法, 内部调用了createAcceptor()方法

image.png

类名: org.apache.tomcat.util.net.JIoEndpoint
protected AbstractEndpoint.Acceptor createAcceptor() {
    return new Acceptor();
}
复制代码

进入BIO模式的方法中, 看到new了一个Acceptor, 这个Acceptor就是一个实现了Runnable的受保护的静态内部类. 下面是该类的run方法的片段

类名: org.apache.tomcat.util.net.AbstractEndpoint.Acceptor
方法名: public void run()
try {
    // Accept the next incoming connection from the server
    // bio socket
    socket = serverSocketFactory.acceptSocket(serverSocket);//
    socket.getLocalSocketAddress();
    System.out.println("接收到了一个socket连接");
} catch (IOException ioe) {
    countDownConnection();
    errorDelay = handleExceptionWithDelay(errorDelay);
    throw ioe;
}
复制代码

可以看到, 正是Acceptor线程去不停地调用serverSocket的accept方法去接收数据的. 一切源头也正是这里. 接收到数据后:

类名: org.apache.tomcat.util.net.AbstractEndpoint.Acceptor
方法名: public void run()
// 如果Endpoint正在运行并且没有被暂停,那么就处理该socket
if (running && !paused && setSocketOptions(socket)) {
    // Hand this socket off to an appropriate processor
    // socket被正常的交给了线程池,processSocket就会返回true
    // 如果没有被交给线程池或者中途Endpoint被停止了,则返回false
    // 返回false则关闭该socket
    if (!processSocket(socket)) {
        countDownConnection();
        // Close socket right away
        closeSocket(socket);
    }
} else {
    countDownConnection();
    // Close socket right away
    closeSocket(socket);
}
复制代码

在processSocket方法中, 将socket包装成一个SockeetWrapper对象, 传递给另一个实现了Runnable的类: SocketProcessor, 进而交给线程池去执行该类的run方法.

类名: org.apache.tomcat.util.net.JIoEndpoint
方法名: protected boolean processSocket(Socket socket)
// bio, 一个socket连接对应一个线程
// 一个http请求对应一个线程?
getExecutor().execute(new SocketProcessor(wrapper));
复制代码

最终是在SocketProcessor的run方法中去读取数据的. 在该run方法中, 找到了一个handler.process的调用:

类名: org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor
方法名: public void run()
// 当前socket没有关闭则处理socket
if ((state != SocketState.CLOSED)) {
    // SocketState是Tomcat定义的一个状态,这个状态需要处理一下socket才能确定,因为跟客户端,跟具体的请求信息有关系
    if (status == null) {
        state = handler.process(socket, SocketStatus.OPEN_READ);
    } else {
        // status表示应该读数据还是应该写数据
        // state表示处理完socket后socket的状态
        state = handler.process(socket,status);
    }
}
复制代码

通过查看这个handler对象的类的继承关系可以发现, 这个handler是一个Handler接口类型, 下面有2个实现类

image.png

由此可以推断在我们使用HTTP协议访问tomcat的时候, 这个handler肯定就是Http11ConnectionHandler了. 它肯定是tomcat启动的时候, 根据Connector这个标签去初始化的, 我们可以看看原委

类名: org.apache.catalina.connector.Connector
public void setProtocol(String protocol) {
    if (AprLifecycleListener.isAprAvailable()) {
        if ("HTTP/1.1".equals(protocol)) {
            setProtocolHandlerClassName
            ("org.apache.coyote.http11.Http11AprProtocol");
        } else if ("AJP/1.3".equals(protocol)) {
            setProtocolHandlerClassName
            ("org.apache.coyote.ajp.AjpAprProtocol");
        } else if (protocol != null) {
            setProtocolHandlerClassName(protocol);
        } else {
            setProtocolHandlerClassName
            ("org.apache.coyote.http11.Http11AprProtocol");
        }
    } 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
        }
    }
}
复制代码

在Connector的setProtocol方法中, 根据server.xml中配置的Connector标签的protocol属性值, 设置具体的协议类名, 其中HTTP/1.1会被认为是org.apache.coyote.http11.Http11Protocol这个协议类, 后续就会生成这个协议类的实例, 看看这个协议类的构造方法

类名: org.apache.coyote.http11.Http11Protocol
public Http11Protocol() {
    // JIoEndpoint继承自 public abstract class AbstractEndpoint<S>
    // AbstractEndpoint有3个实现类: 1. NioEndpoint; 2. JioEndpoint(BIO); 3. AprEndpoint
    // 所以endpoint其实就是提供了不同的IO模型, 从socket中读取数据(流), 之后再根据协议去解析数据
    // endpoint中有个Acceptor是一个runnable的实现类, 从serverSocket(在Endpoint.bind()方法中初始化)接受socket
    // 再调用processSocket(socket)处理socket
    endpoint = new JIoEndpoint();
    cHandler = new Http11ConnectionHandler(this);
    ((JIoEndpoint) endpoint).setHandler(cHandler);
    setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
    setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
    setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
}
复制代码

在Http11Protocol协议类中:

  1. new出了Endpoint为JIoEndpoint
  2. new出了Http11ConnectionHandler为cHandler, 在Http11ConnectionHandler类的构造方法中将自己(this, Http11Protocol对象)传给了Http11ConnectionHandler对象(cHandler)的proto属性
  3. 将这个cHandler赋值给了JIoEndpont对象的handler属性.
类名: org.apache.tomcat.util.net.JIoEndpoint.Acceptor
方法名: public void run()
// 在while(running)循环中不断去acceptSocket
socket = serverSocketFactory.acceptSocket(serverSocket);
复制代码
类名: org.apache.tomcat.util.net.DefaultServerSocketFactory
@Override
public Socket acceptSocket(ServerSocket socket) throws IOException {
    return socket.accept();
}
复制代码

这样就串起来了: 在JioEndpoint的非静态内部类Acceptor的对象中启动的线程, 不停的去调用socket.accept()去接受socket, 每接受到一个socket, 就去调用processSocket方法, processSocket方法又new了SocketProcessor丢给线程池, 那SocketProcessor的run方法当然可以用其外部类的cHandler属性, 这个cHandler当然就是Http11ConnectionHandler对象咯. 在run方法中去调用cHandler的process方法.

在cHandler(Http11ConnectionHandler)的process方法中, 会获取一个Processor, 如果processor为空, 就初始化一个Processor, 如下是process方法中的部分代码, 方法太长, 只贴了核心代码:

类名: org.apache.coyote.AbstractProtocol.AbstractConnectionHandler
public SocketState process(SocketWrapper<S> wrapper,
        SocketStatus status) {
    S socket = wrapper.getSocket();
    Processor<S> processor = connections.get(socket);
    if (processor == null) {
        // 从被回收的processor中获取processor
        processor = recycledProcessors.poll();
    }
    if (processor == null) {
        processor = createProcessor(); // 创建具体的processor, BIO中: Http11Processor
    }
    state = processor.process(wrapper);
    return state;
}
复制代码

第一次从connections拿到的都是空, 会走创建Http11Porcessor的方法中去, 得到一个Http11Porcessor(BIO的协议处理器)的对象, 之后再会调用这个processor的process方法, 传进包装的SocketWrapper对象, 真正的去从socket中读取数据.

我们自己写代码的时候, 从socket中拿到数据, 都是从socket.getInputStream()中去read的, tomcat也是需要遵循这个逻辑的, socket中的数据都是输入流, 是0101的玩意儿, 因此在tomcat的不同协议的处理器中, 会根据不同的应用层协议解析这些输入流, 而针对http协议, tomcat就是自己写了一套解析HTTP协议的逻辑. 下面我们就看看tomcat是怎么根据HTTP协议去解析socket中的数据的.

HTTP1.1协议请求报文浅析

我使用postman发送一个HTTP的POST请求给tomcat, 将数据打印出来, 如下所示

image.png

POST /HelloServlet/servletDemo HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: ce27f37f-d2ef-4a72-ab51-1b77bfd92aa1
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 38

{"name":"法外狂徒张三","age":35}
复制代码

可见, HTTP协议是有格式要求的, 下面将分析一下HTTP1.1协议的大概的格式要求

image.png

请求行中的3项内容会以空格隔开, 请求头中放着一些对请求的描述信息, 请求头结束后, 是一个空行, 然后最后是请求体正文.

请求行没什么好说的, 下面列举HTTP1.1规范定义的首部字段(包含请求头和响应头)的介绍

HTTP1.1首部字段

HTTP/1.1规范定义了如下47种首部字段

通用首部字段

字段名 字段解释
Cache-Control 控制缓存的行为
Connection 控制不再转发给代理的首部字段/管理是否持久(长)连接
Date 创建报文的日期时间
Pragma 报文指令
Trailer 报文末端的首部一览
Transfer-Encoding 指定报文主体的编码传输方式
Upgrade 升级为其它协议
Via 代理服务器的相关信息
Warning 错误通知

请求首部字段

字段名 字段解释
Accept 用户代理可处理的媒体类型
Accept-Charset 优先的字符集
Accept-Encoding 优先的内容编码
Accept-Language 优先的语言(自然语言)
Authorization Web认证信息
Expect 期待服务器的特定行为
From 用户的电子邮箱地址
Host 请求资源所在的服务器
If-Match 比较实体标记(ETag)
If-Modified-Since 比较资源的更新时间
If-None-Match 比较实体标记(与If-Match相反)
If-Range 资源未更新时发送实体Byte的范围请求
If-Unmodified-Since 比较资源的更新时间(与If-Modified-Since相反)
Max-Forwards 最大传输逐跳数
Proxy-Authorization 代理服务器要求客户端的认证信息
Range 实体的字节范围请求
Referer 对请求中URI的原始获取方
TE 传输编码的优先级
User-Agent HTTP 客户端程序的信息

响应首部字段

字段名 字段解释
Accept-Ranges 是否接受字节范围请求
Age 推算资源创建经过时间
ETag 资源的匹配信息
Location 令客户端重定向至指定URI
Proxy-Authenticate 代理服务器对客户端的认证信息
Retry-After 对再次发起请求的时机要求
Server-HTTP 服务器的安装信息
Vary 代理服务器缓存的管理信息
WWW-Authenticate 服务器对客户端的认证信息

实体首部字段

字段名 字段解释
Allow 资源可支持的HTTP方法
Content-Encoding 实体主体适用的编码方式
Content-Language 实体主体的自然语言
Content-Length 实体主体的大小(单位:字节)
Content-Location 替代对应资源的URI
Content-MD5 实体主体的报文摘要
Content-Range 实体主体的位置范围
Content-Type 实体主体的媒体类型
Expires 实体主体过期的日期时间
Last-Modified 资源的最后修改日期时间

tomcat中创建http协议处理器

既然HTTP协议是固有格式的, 那么tomcat在解析它的时候, 就需要按照它的要求来, 下面看看tomcat的创建HTTP协议处理器的方法

类名: org.apache.coyote.http11.Http11Protocol
@Override
protected Http11Processor createProcessor() {
    Http11Processor processor = new Http11Processor(
            proto.getMaxHttpHeaderSize(), proto.getRejectIllegalHeaderName(),
            (JIoEndpoint)proto.endpoint, proto.getMaxTrailerSize(),
            proto.getAllowedTrailerHeadersAsSet(), proto.getMaxExtensionSize(),
            proto.getMaxSwallowSize(), proto.getRelaxedPathChars(),
            proto.getRelaxedQueryChars());
    processor.setAdapter(proto.adapter);
    processor.setMaxKeepAliveRequests(proto.getMaxKeepAliveRequests());
    processor.setKeepAliveTimeout(proto.getKeepAliveTimeout());
    processor.setConnectionUploadTimeout(
            proto.getConnectionUploadTimeout());
    processor.setDisableUploadTimeout(proto.getDisableUploadTimeout());
    processor.setCompressionMinSize(proto.getCompressionMinSize());
    processor.setCompression(proto.getCompression());
    processor.setNoCompressionUserAgents(proto.getNoCompressionUserAgents());
    processor.setCompressableMimeTypes(proto.getCompressableMimeTypes());
    processor.setRestrictedUserAgents(proto.getRestrictedUserAgents());
    processor.setSocketBuffer(proto.getSocketBuffer());
    processor.setMaxSavePostSize(proto.getMaxSavePostSize());
    processor.setServer(proto.getServer());
    processor.setDisableKeepAlivePercentage(
            proto.getDisableKeepAlivePercentage());
    processor.setMaxCookieCount(proto.getMaxCookieCount());
    register(processor);
    return processor;
}
复制代码
类名: 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);
}
复制代码

可以看到, tomcat在创建HTTP协议处理器的时候, 将一开始存在于Http11Protocol的信息又都封装给了处理器, 比如Endpoint, 最大支持长链接的请求数, 长链接最大超时时间等等, 下面我们可以看看关于长链接的这两个参数是怎么使用的.

tomcat对于长链接的实现

HTTP协议中有一个首部字段Connection, 在判断是否为长连接的情况下, 可以取值为keep-alive和close, 具体是什么意思呢? 先看看现象

HTTP中Connection首部字段为keep-alive时的现象示例

在webapps中的HelloServlet文件夹中创建index.html

文件名: /webapps/HelloServlet/index.html
<html>
    <body>
    <h2>hello world</h2>
        <img src="http://localhost:8080/HelloServler/servletDemo?time=1">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=2">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=3">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=4">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=5">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=6">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=7">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=8">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=9">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=10">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=11">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=12">
    </body>
</html>
复制代码
System.out.println(inputStream + ": " + request.unparsedURI().toString());
复制代码

在InternalInputBuffer的java代码中随便找个解析socket的地方, 打印一下它的inputStream属性 在浏览器中访问http://localhost:8080/HelloServlet/index.html 得到如下输出:

输出
java.net.SocketInputStream@5e11f428: /HelloServlet/index.html
java.net.SocketInputStream@5e11f428: /HelloServler/servletDemo?time=1
java.net.SocketInputStream@6482263a: /HelloServler/servletDemo?time=2
java.net.SocketInputStream@5c487d1: /HelloServler/servletDemo?time=3
java.net.SocketInputStream@550a91db: /HelloServler/servletDemo?time=4
java.net.SocketInputStream@22c22bd9: /HelloServler/servletDemo?time=5
java.net.SocketInputStream@2c4dc77f: /HelloServler/servletDemo?time=6
java.net.SocketInputStream@6482263a: /HelloServler/servletDemo?time=7
java.net.SocketInputStream@5e11f428: /HelloServler/servletDemo?time=8
java.net.SocketInputStream@5c487d1: /HelloServler/servletDemo?time=9
java.net.SocketInputStream@22c22bd9: /HelloServler/servletDemo?time=10
java.net.SocketInputStream@6482263a: /HelloServler/servletDemo?time=11
java.net.SocketInputStream@5e11f428: /HelloServler/servletDemo?time=12
java.net.SocketInputStream@5e11f428: /favicon.ico
复制代码

这样看好像并没有什么效果, 我们可以改一下一个属性, 那就是Connector的maxKeepAliveRequests属性, 我们将它设置为2

文件名: /conf/server.xml
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" maxKeepAliveRequests="2"/>
复制代码

重启tomcat, 再访问该html看看输出:

输出
java.net.SocketInputStream@25684cdc: /HelloServlet/index.html
java.net.SocketInputStream@62180732: /HelloServler/servletDemo?time=2
java.net.SocketInputStream@25684cdc: /HelloServler/servletDemo?time=1
java.net.SocketInputStream@2c84029e: /HelloServler/servletDemo?time=3
java.net.SocketInputStream@18d5c9cb: /HelloServler/servletDemo?time=5
java.net.SocketInputStream@3ee39c93: /HelloServler/servletDemo?time=4
java.net.SocketInputStream@300e6080: /HelloServler/servletDemo?time=6
java.net.SocketInputStream@300e6080: /HelloServler/servletDemo?time=9
java.net.SocketInputStream@18d5c9cb: /HelloServler/servletDemo?time=10
java.net.SocketInputStream@62180732: /HelloServler/servletDemo?time=12
java.net.SocketInputStream@3ee39c93: /HelloServler/servletDemo?time=11
java.net.SocketInputStream@2c84029e: /HelloServler/servletDemo?time=7
java.net.SocketInputStream@12884c66: /HelloServler/servletDemo?time=8
java.net.SocketInputStream@12884c66: /favicon.ico
复制代码

首先, 对于浏览器的请求, 浏览器并不会逐个发送同时解析到的请求的, 比如我上述的操作, 我在html中有12个标签页, 那么浏览器会首先单独的发送我访问的index.html, 然后解析html中有多少个img标签, 然后6个标签的请求一起发送.

如果仔细观察, 就可以发现, 如果我没设置Connector的maxKeepAliveRequests属性, 那么请求的第一次, 也就是html, 它的输入流是@5e11f428, 然后由于time1和time6一起发送, 原先index的socket已经空闲了, 因此交给了time1去使用, 然后time2一直到time6都是新建的socket, 而time7到time12就会发现, 全部都是公用的time1到time6和index的socket, 包括最后的请求网址图标也是用的@5e11f428

如果只这么分析, 我们最多只能得出一个结论: socket在建立后, 好像并没有关闭, 好像后续的请求发送时, 如果前面请求的socket空闲之后, 一直都可以复用前面请求的socket一样. 所以我们需要分析第二次的输出情况

第二次我将Connector的maxKeepAliveRequests属性设置为2, 然后index老样子, 第一次请求嘛, 肯定会创建一个socket, 我们得到它的输入流是@25684cdc, 然后发送time1到time6, 老样子, index的socket空闲后, 交给了time1去使用, time2到time6都是用的新建的socket. 然后等前面6次请求响应之后, 后面6次请求就发送了, 我们发现, @25684cdc并没有在time7到time12中出现了, 其中time8使用的@12884c66并没有出现在前面7次请求中, 说明time8是新建的socket, 而time7, time9到time12都是复用的前面的socket, 然后最后的请求图标请求, 复用的是time8的socket.

这次我们可以得到结论: maxKeepAliveRequests属性设置为多少, socket就可以复用多少次.

我们不妨看看第二次响应结果, 浏览器中的响应

image.png

image.png

image.png

所以我们可以看到, 所有的请求的请求头中, Connection字段都为keep-alive, 说明这是浏览器设置的, 默认是长连接, 但是tomcat中对socket做了限制, 可以通过配置, 让客户端无法一直通过同一个socket去发送数据. 当tomcat的响应中的响应头中带有Connection=close时, tomcat就会关闭这个socket.

但是这样仅仅只是证明浏览器可以复用socket, 但是这个维度太大了, 我认为这样不太可能, 因为如果同一个浏览器访问不同的域名的地址, 那肯定不可能复用socket的嘛, 但是如果是localhost和127.0.0.1呢? 这样的话它还会不会复用socket呢? 我猜测不会, 但是光猜测没意义, 可以试验一下. 所以我把index.html中, time7到time12的域名换一下, 换成127.0.0.1:8080, 看看time7到time12是否还能复用

文件名: /webapps/HelloServlet/index.html
<html>
    <body>
    <h2>hello world</h2>
        <img src="http://localhost:8080/HelloServler/servletDemo?time=1">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=2">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=3">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=4">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=5">
        <img src="http://localhost:8080/HelloServler/servletDemo?time=6">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=7">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=8">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=9">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=10">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=11">
        <img src="http://127.0.0.1:8080/HelloServler/servletDemo?time=12">
    </body>
</html>
复制代码

使用localhost访问html, 并重启一下tomcat(仅仅为了初始化socket的复用情况), 这时候我的maxKeepAliveRequests属性还是2

输出
java.net.SocketInputStream@28ef627: /HelloServlet/index.html
java.net.SocketInputStream@28ef627: /HelloServler/servletDemo?time=1
java.net.SocketInputStream@558d912c: /HelloServler/servletDemo?time=2
java.net.SocketInputStream@58f82f4e: /HelloServler/servletDemo?time=3
java.net.SocketInputStream@4691c63d: /HelloServler/servletDemo?time=4
java.net.SocketInputStream@22aca64: /HelloServler/servletDemo?time=7
java.net.SocketInputStream@1ee53b84: /HelloServler/servletDemo?time=5
java.net.SocketInputStream@6c78945e: /HelloServler/servletDemo?time=9
java.net.SocketInputStream@75001be8: /HelloServler/servletDemo?time=6
java.net.SocketInputStream@407599b8: /HelloServler/servletDemo?time=10
java.net.SocketInputStream@2edca1c7: /HelloServler/servletDemo?time=8
java.net.SocketInputStream@263364d9: /HelloServler/servletDemo?time=11
java.net.SocketInputStream@22aca64: /HelloServler/servletDemo?time=12
java.net.SocketInputStream@58f82f4e: /favicon.ico
复制代码

可以看到, 仅仅只有time1复用了index.html请求的socket, 而time7到time12全部是自己新建的socket, 而图标请求由于是使用localhost访问, 因此复用了time3的socket(随机一个), 这时候我们看看图标请求的响应头, Connection首部字段值为close

image.png

以上演示说明:

  1. Connection=keep-alive是针对域名的
  2. 长连接并不是真正的就不断开连接了, 而是可以由服务器去控制什么时候断开连接, 断开连接前的最后一个响应, 服务器会给客户端一个Connection=close的响应头, 示意客户端也可以将这个socket关闭了.
  3. 所谓长连接, 就是客户端在发送请求的时候, 看看浏览器中是否具有该域名曾经创建过的空闲可用的socket, 如果有, 就使用已有的socket, 不会再去新建一个socket了.
  4. 我们通过响应头close的推测, 可以推测出, 实际上客户端如果发送请求的时候, 请求头中的Connection首部字段如果为close, 服务器端也不会去继续使用这个socket了(将在源码验证).

tomcat中对长连接的实现

首先复述一下tomcat接受socket的过程

  1. Acceptor线程不断的在while(running)中调用socket.accept()去接受socket
  2. 接受到一个socket, 就创建任务丢进线程池
  3. 任务肯定是实现了Runnable的, 在任务的run方法中创建/获取(如果之前回收了就会从回收站中拿, 而不会创建)了Http11Processor对象processor, 调用该对象的process方法(在父类AbstractHttp11Processor中)
  4. 在processor.process(socketWrapper)中去读取/解析socket中的输入流

在AbstractHttp11Processor中, 跟keep-alive业务逻辑相关的有3个受保护的属性

类名: org.apache.coyote.http11.AbstractHttp11Processor
// 表示当前请求是不是一个keep-alive的
protected boolean keepAlive = true;
// 表示当前请求是一个被keep-alive的
// 表示当前请求的上一个请求是keep-alive, 所以当前请求是可以复用上次请求的socket的
protected boolean keptAlive;
// 用于指示套接字应保持打开状态的标志
protected boolean openSocket = false;
复制代码

在process方法中, 我们就必须要来处理这个keep-alive的情况了, 根据我们的分析, 如果tomcat在解析请求头的时候, 发现Connection字段是keep-alive, 那么它处理完这个请求之后, 就不会关闭这个socket, 我们大概可以总结一下这个代码的写法, 至于真实的源码, 实在是太长了, 可以自己去看看, 在org.apache.coyote.http11.AbstractHttp11Processor类的public SocketState process方法中. 下面是我自己总结提炼出的核心代码(与源码出入比较大, 由我自己整理):

伪代码, 本代码会在getExecutor().execute(new SocketProcessor(socketWrapper))线程池中执行
SocketState process(Socket socket) {
    keepAlive = true;
    while (keepAlive) {
        InputStream in = socket.getInputStream();
        // 将输入流解析并封装成一个Request对象
        Request request = in.read();
        // 拿到请求头中的Connection
        String conn = request.getHeaders().get("Connection");
        keepAlive = "keep-alive".equals(conn);
        // 调用Servlet
        adapter.service(request, response);
        endRequest();
        nextRequest();
        // 如果keepAlive = true, 就代表socket不应该关闭
        opensocket = keepAlive;
        // 如果处理完当前请求后, 发现socket里没有下一个请求的数据了, 那么就退出当前循环
        // 如果是keepAlive就不会关闭socket, 如果是close就关闭socket
        // 对于BIO来说, 由于一个线程处理一个socket, 当退出这个loop后, 当前线程就会结束掉
        // 但是对于keepAlive来说, 即使当前线程结束掉了, socket也不应该被关闭
        if(inputBuffer.lastValid == 0) {
            // 一般正常的浏览器在某个socket中, 一次只会发一个完整的请求
            // 但是不乏一些程序故意去在一个socket中发送连体的好几个请求, 这里就是为了防止这种情况
            // 如果一个程序在一个socket中发送连体的请求, 并且keepAlive = true, 那么上个请求结束后, tomcat还会继续在本次循环中处理下次的请求
            break;
        }
    }
    // 如果process结束尾声的时候, openSocket还是true, 那process方法就返回socket的状态是open
    if (openSocket) {
        return SocketState.OPEN;
    }
}
复制代码

上面的代码是在SocketProcessor这个线程的run方法中执行的, 在该run方法中, 会判断process方法最终返回的socket状态, 如果是OPEN的话, 就会再次getExecutor().execute(new SocketProcessor(socketWrapper))去执行process方法.

类名: org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor
方法名: public void run()
// 如果Socket的状态是被关闭,那么就减掉连接数并关闭socket
if (state == SocketState.CLOSED) {
    // Close socket
    if (log.isTraceEnabled()) {
        log.trace("Closing socket:"+socket);
    }
    countDownConnection();
    try {
        socket.getSocket().close();
    } catch (IOException e) {
        // Ignore
    }
} else if (state == SocketState.OPEN ||
        state == SocketState.UPGRADING ||
        state == SocketState.UPGRADING_TOMCAT  ||
        state == SocketState.UPGRADED){
    // 如果调用process方法返回SocketState.OPEN, 就设置该socket是一个被keepAlive(keptAlive = true)的
    socket.setKeptAlive(true);
    // 调用一下socket的access方法, 标注本次访问的时间, 也就是下次请求的上次请求的最后时间
    socket.access();
    // 设置了launch是true, 这个就决定是否要将该socket再次丢进线程池去执行
    launch = true;
} else if (state == SocketState.LONG) {
    // socket不会关闭,但是当前线程会执行结束
    socket.access();
    waitingRequests.add(socket);
}
复制代码

在run方法的finally块中, 判断了launch的值

类名: org.apache.tomcat.util.net.JIoEndpoint.SocketProcessor
方法名: public void run()
finally {
    if (launch) {
        try {
            // 如果在上面决定了launch = true, 将在finally块中将本次请求的socket继续丢进线程池执行
            getExecutor().execute(new SocketProcessor(socket, SocketStatus.OPEN_READ));
        } catch (RejectedExecutionException x) {
            log.warn("Socket reprocessing request was rejected for:"+socket,x);
            try {
                //unable to handle connection at this time
                handler.process(socket, SocketStatus.DISCONNECT);
            } finally {
                countDownConnection();
            }
        } catch (NullPointerException npe) {
            if (running) {
                log.error(sm.getString("endpoint.launch.fail"),
                        npe);
            }
        }
    }
}
复制代码

这是大概的流程, 接着前面的4步, 从socket中解析输入流开始

  1. SocketProcessor(Runnable)的run方法中去调用AbstractHttp11Processor的process方法

在process方法中

1.1. 解析请求行
1.2. 解析请求头
1.3. 拿到请求头中的Connection首部字段, 判断是否是keep-alive的
1.4. 如果是keep-alive的
    1.4.1. 如果客户端在该socket中一次性连体的发送多个请求, 不跳出while循环, 继续在while中处理下次请求
    1.4.2. 如果客户端正常, 没有发送连体请求, 就设置处理结果返回socket状态为open
复制代码
  1. SocketProcessor(Runnable)的run方法中, 拿到process方法的返回socket状态
  2. 如果socket状态为open, 就继续将该socket设置进一个新的SocketProcessor(Runnable)中
  3. 将这个新的SocketProcessor丢进线程池执行

tomcat对长连接设置的超时处理

由于这个涉及到tomcat从socket输入流中读取数据的方式, 将会留在后面的博客进行解释

猜你喜欢

转载自juejin.im/post/7042317100995575839