tomcat源码分析04:Connector启动流程

490 阅读7分钟

注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….

本文是tomcat源码分析的第四篇,本文来分析Connector的启动流程。

1. Connector的创建

在我们的示例demo中,我们是这样创建Connector的:

public class Demo01 {

    @Test
    public void test() throws Exception {
        Tomcat tomcat = new Tomcat();

        // 创建连接器
        Connector connector = new Connector();
        connector.setPort(8080);
        connector.setURIEncoding("UTF-8");
        tomcat.getService().addConnector(connector);

        ...
    }
}

进入Connector的构造方法:

public Connector() {
    this("HTTP/1.1");
}

tomcat默认提供的参数为HTTP/1.1,这个参数在ProtocolHandler#create方法中会用到:

也就是说,tomcat默认创建的ProtocolHandlerorg.apache.coyote.http11.Http11NioProtocol

我们再看看Http11NioProtocol的构造方法:

public Http11NioProtocol() {
    super(new NioEndpoint());
}

这里我们又看到一个关键组件:NioEndpointHttp11NioProtocol中使用的EndpointNioEndpoint

到此,简单的一个Connector的创建为我们准备了三个组件:

  1. Connector:连接器
  2. Http11NioProtocol:http协议的nio实现
  3. NioEndpoint:NIO量身定制的线程池

关于NioEndpoint,其注释如下:

NIO tailored thread pool, providing the following services:

  • Socket acceptor thread
  • Socket poller thread
  • Worker threads pool

NIO量身定制的线程池,提供以下服务

  • socket 连接线程(处理socket连接事件)
  • socket 轮询线程(处理socket读写事件)
  • 工作线程

这三个组件的具体功能我们后面再分析。

2. Connector 初始化

接关我们来看看Connector的初始化,进入Connector#initInternal

protected void initInternal() throws LifecycleException {
    // 省略了一些代码
    ...

    try {
        protocolHandler.init();
    } catch (Exception e) {
        ...
    }
}

Connector#initInternal方法中,做的主要工作就是调用protocolHandler.init()方法了,我们进入AbstractProtocol#init一探究竟:

public void init() throws Exception {
    // 设置 endpoint 的一些属性
    String endpointName = getName();
    endpoint.setName(endpointName.substring(1, endpointName.length()-1));
    endpoint.setDomain(domain);
    // 绑定端口、主机,设置 bindState,处理了oname属性值
    endpoint.init();
}

这块设置了endpoint的一些属性,然后调用endpoint.init()方法,根据前面的分析,这里的endpoint类型是NioEndpoint,我们进入AbstractEndpoint#init

public final void init() throws Exception {
    if (bindOnInit) {
        // 绑定端口、主机
        bindWithCleanup();
        bindState = BindState.BOUND_ON_INIT;
    }
    ...
}

AbstractEndpoint#init中,主要调用了bindWithCleanup(),该方法就是用来处理ip、端口绑定的,跟着这个方法执行下去,最终执行到NioEndpoint#bind方法:

public void bind() throws Exception {
    // 初始化serverSocket
    initServerSocket();

    setStopLatch(new CountDownLatch(1));
    initialiseSsl();
    // 开启 selector
    selectorPool.open(getName());
}

这个方法先是初始化了serverSocket,然后开启了一个selector,我们且看下去:

2.1 NioEndpoint#initServerSocket

NioEndpoint#initServerSocket代码如下:

protected void initServerSocket() throws Exception {
    if (...) {
        ...
    } else {
        // 得到一个 ServerSocketChannel
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        // 获取地址与端口
        InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
        // 进行绑定操作
        serverSock.bind(addr, getAcceptCount());
    }
    // 设置true,表示还是阻塞的
    serverSock.configureBlocking(true);
}

先获取了一个ServerSocketChannel,然后为其绑定ip与端口,这些都是很标准的nio操作了,没啥好说的,不过令人疑惑的是,在最后调用的serverSock.configureBlocking(...)方法:

serverSock.configureBlocking(true);

传入的值为true,这表示ServerSocketChannel是阻塞的!这表示,tomcat在处理连接请求时,使用的阻塞IO

2.2 selectorPool.open(...)

我们再来看看selectorPool.open(...)的执行,进入NioSelectorPool#open方法:

public void open(String name) throws IOException {
    enabled = true;
    // 获取 selector
    getSharedSelector();
    if (shared) {
        blockingSelector = new NioBlockingSelector();
        // 开启 selector
        blockingSelector.open(name, getSharedSelector());
    }
}

我们先来看看 NioSelectorPool#getSharedSelector 方法:

protected Selector getSharedSelector() throws IOException {
    if (shared && sharedSelector == null) {
        synchronized (NioSelectorPool.class) {
            if (sharedSelector == null) {
                // 得到一个 selector 对象,nio 提供的方法
                sharedSelector = Selector.open();
            }
        }
    }
    return  sharedSelector;
}

这个方法就是用来产生一个selector对象的,没干什么重要的事。

我们继续,进入NioBlockingSelector#open方法:

public void open(String name, Selector selector) {
    sharedSelector = selector;
    poller = new BlockPoller();
    poller.selector = sharedSelector;
    poller.setDaemon(true);
    poller.setName(name + "-BlockPoller");
    poller.start();
}

这个方法创建了一个BlockPoller实例,处理了一些属性后,调用了BlockPoller#start方法,我们进入BlockPoller

protected static class BlockPoller extends Thread {
    /**
     * Thread#run 方法
     */
    @Override
    public void run() {
        while (run) {
            try {
                events();
                int keyCount = 0;
                try {
                    int i = wakeupCounter.get();
                    // 进行 selector 操作
                    if (i > 0) {
                        keyCount = selector.selectNow();
                    } else {
                        wakeupCounter.set(-1);
                        keyCount = selector.select(1000);
                    }
                }
                // 省略一些代码
                ...
            }
            // 省略了一些配置
            ...
        }
    }
}

BlockPoller 继承了Threade,在它的run()方法中,进行了selector.select(...)操作,这些都是nio的标准操作了。

如果有小伙伴对nio编程比较熟悉,就会发现到目前为止,这个selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件,因此selector.selectNow()/selector.select(...)方法不会获取到任何channel,关于它的作用,我们后面再分析。

到这里,Connector 的初始化操作就完成了,主要进行了如下操作:

  1. 初始化 ServerSockChannel (只是创建了对象,绑定ip、端口,并未对外监听连接)
  2. 开启 BlockPoller 线程(selector没有绑定ServerSocketChannel,也没注册要轮询的事件)

3. Connector 启动

梳理完初始化流程后,接着我们来看看启动流程:Connector#startInternal

protected void startInternal() throws LifecycleException {
    // 省略了一些代码
    ...

    try {
        protocolHandler.start();
    } catch (Exception e) {
        ...
    }
}

进入 AbstractEndpoint#start 方法:

public final void start() throws Exception {
    if (bindState == BindState.UNBOUND) {
        // 绑定端口、主机
        bindWithCleanup();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

初始操作已经执行过了,代码会直接运行startInternal(),也就是NioEndpoint#startInternal方法:

@Override
public void startInternal() throws Exception {
    if (!running) {
        running = true;
        paused = false;
        ...
        // 创建处理业务逻辑的线程池
        if (getExecutor() == null) {
            createExecutor();
        }
        // 初始化连接数计算器,就是用来计算服务端连接数的
        initializeConnectionLatch();
        //  设置pollerThread相关信息
        poller = new Poller();
        Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
        pollerThread.setPriority(threadPriority);
        pollerThread.setDaemon(true);
        // 启动 Poller 线程,处理读写事件
        pollerThread.start();
        // 启动 Acceptor 线程,处理连接事件
        startAcceptorThread();
    }
}

NioEndpoint#startInternal方法的流程如下:

  1. 创建了一个线程池,该线程池主要是用来处理业务的
  2. 初始化连接数计算器,tomcat可以配置最大连接请求,就是在这里计算当前连接请求的
  3. 启动 Poller 线程,处理读写事件
  4. 启动 Acceptor 线程,处理连接事件

3.1 启动Poller线程

我们先来看看Poller

public class Poller implements Runnable {

    private Selector selector;

    public Poller() throws IOException {
        // 得到一个 selector 对象
        this.selector = Selector.open();
    }

    @Override
    public void run() {
        while (true) {
            boolean hasEvents = false;
            try {
                if (!close) {
                    hasEvents = events();
                    // 1. selector 操作
                    if (wakeupCounter.getAndSet(-1) > 0) {
                        keyCount = selector.selectNow();
                    } else {
                        keyCount = selector.select(selectorTimeout);
                    }
                    wakeupCounter.set(0);
                }
                ...
                if (keyCount == 0) {
                    hasEvents = (hasEvents | events());
                }
            } catch (Throwable x) {
                ...
            }
            
            Iterator<SelectionKey> iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            // selector 上存在key,进入处理流程
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                iterator.remove();
                NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
                if (socketWrapper != null) {
                    // 2. 处理请求
                    processKey(sk, socketWrapper);
                }
            }
        }
        ...
    }

    ...
}

Poller实现了Runnable接口,在构造方法中,会得到一个selector对象,紧接着就在run()方法中使用这个selector对象了,同BlockPoller, 这个selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件.

接着我们再看Poller#run()方法,这个方法的轮询操作BlockPoller相差无几,纵观整个方法,就只干了两件事:

  1. 轮询请求
  2. 调用processKey(...)处理请求

selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件,因此根本不会有请求过来,那这个轮询有何意义,processKey(...)又是处理什么请求呢?这两个疑问先留着,我们在下篇分析tomcat处理请求连接时再揭晓吧!

3.2 启动Acceptor线程

我们再看看Acceptor线程的启动,进入AbstractEndpoint#startAcceptorThread方法:

    protected void startAcceptorThread() {
        acceptor = new Acceptor<>(this);
        String threadName = getName() + "-Acceptor";
        acceptor.setThreadName(threadName);
        Thread t = new Thread(acceptor, threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }

这个方法先是创建了Acceptor实例,传入的参数为this,其实就是NioEndpoint,接着就是设置了一些属性,然后启动了线程,我们进入Acceptor

public class Acceptor<U> implements Runnable {

    /** 类型是 NioEndpoint */
    private final AbstractEndpoint<?,U> endpoint;

    ...

    public Acceptor(AbstractEndpoint<?,U> endpoint) {
        this.endpoint = endpoint;
    }

     @Override
    public void run() {

        int errorDelay = 0;

        try {
            while (!stopCalled) {
                ...

                try {
                    ...

                    U socket = null;
                    try {
                        // 接收服务端连接
                        socket = endpoint.serverSocketAccept();
                    } catch (Exception ioe) {
                        endpoint.countDownConnection();
                        if (endpoint.isRunning()) {
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    errorDelay = 0;
                    if (!stopCalled && !endpoint.isPaused()) {
                        // 手动注册到 selector
                        if (!endpoint.setSocketOptions(socket)) {
                            endpoint.closeSocket(socket);
                        }
                    } else {
                        endpoint.destroySocket(socket);
                    }
                } catch (Throwable t) {
                    ...
                }
            }
        } finally {
            stopLatch.countDown();
        }
        state = AcceptorState.ENDED;
    }

    ...
}

Acceptor#run()方法中,会调用endpoint.serverSocketAccept()获取服务端的连接,然后调用endpoint.setSocketOptions(...)将连接注册到selector上。

我们来看看它是怎么获取到一个连接的,进入NioEndpoint#serverSocketAccept方法:

protected SocketChannel serverSocketAccept() throws Exception {
    return serverSock.accept();
}

这里调用的是ServerSocketChannel#accept方法来获取服务端连接,需要注意的是,这个方法是阻塞的,也就是说,tomcat会一直在这等着,直到有新的服务端连接进来才会被唤醒,因此Acceptor是阻塞的。

总结下就是,tomcat在获取服务端连接时,使用的是阻塞IO!

到这里,Contector的启动就结果了,endpoint.serverSocketAccept()方法调用后,就能监听连接请求了。

4. 总结

本文分析了Connector的启动流程,分为两个部分来分析:初始化与启动,总结如下:

  • 初始化:创建了ServerSocketChannel,并绑定了IP与端口;启动了BlockPoller线程
  • 启动:启动了Poller线程与Acceptor线程

需要注意的是:

  1. tomcat在获取服务端连接时,使用的是阻塞IO
  2. 到目前为止,Poller线程的selector没有绑定ServerSocketChannel,也没有注册任何要轮询的事件

好了,本文就到这里,我们下一篇再来分析tomcat的连接处理过程。


限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。

本文首发于微信公众号 Java技术探秘,链接:mp.weixin.qq.com/s/XB0nKcbw-…,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!