Java NIO (一)

因工作需要我接触到了netty框架,这让我想起之前为夺高薪而在CSDN购买的Netty课程。如今看来,这套课程买的很值。这套课程中关于NIO的讲解,让我对Tomcat产生了浓厚的兴趣,于是我阅读了Tomcat中关于服务端和客户端之间连接部分的源码。从今天起想对这块内容进行一次全面梳理。下面就让我们一起看看这块看似贫瘠,实则充满生机的域外圣地吧!

1 概述

在正式梳理前,我们先看一下JDK提供的一种新IO——Java NIO,其全称为:java non-blocking IO。它是JDK从1.4版本开始提供的新API。它对原有输入/输出做了一些改进,因此又被称为NIO,即New IO。它是一种同步非阻塞IO。

JDK中与NIO相关的类均被放在java.nio包及其子包下,并且它们对原java.io包中的很多类都进行了改写。NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。下面这幅图就展示了NIO的通信模型:

关于这个通信模型,我想谈一下自己的理解:从图中可以看出无论是NIO模式下,还是BIO模式下,均存在两个角色:服务端和客户端。服务端用于监听指定端口,等待客户端的连接请求,如果有客户端请求连接,则服务端会创建对应的连接对象,并将该请求的核心诉求交由下游的业务组件进行处理。客户端则与指定服务端之间建立连接,并与之进行交互。相比之下客户端的操作非常简单,就是发送连接请求,然后与服务端进行通信。既然如此是不是就可以认为NIO和BIO是一样的呢?答案肯定是不能。NIO和BIO在处理客户端与服务端之间的连接的方式上有很大差别:在BIO模式下,客户端与服务端成功建立连接后,服务端会将这个连接交由一个单独的线程处理,即服务端线程与客户端之间是1:1的关系,具体如下图所示:

 也就是说可以同时接入的客户端的数量是由服务端自己决定的,因此如果同时接入的客户端的数量超过服务端所能承受的最大限度,就会造成服务端崩溃,影响客户端体验。个人认为客户端与服务端之间的同步阻塞式通信是造成这种问题的根本原因。这种通信模式的特点就是客户端与服务端之间的连接只要建立起来,服务端分配的处理当前连接的线程就必须同步等待客户端的所有操作,即即使当前连接对应的客户端被莫名挂起(发送数据、处理数据或其他莫名原因),这段时间内,该服务端线程即使有足够精力处理其他事情,其也不能做,譬如:在客户端数据未发送完成的时间段内服务端线程必须等待,不能干其他事情。这就好比在路口等红绿灯一样,即使这个红绿灯左右两侧的道路空空如也,或者红绿灯前后的道路上排起了万米长龙,在红绿灯提示车辆可以通行之前,所有车辆也必须等待。借助这个案例,我们可以对BIO模式下的通信模型有更加具象化的认识。计算机的这种特征,也从侧面说明了即使其拥有远超人类的计算性能,其也无法逃脱永恒的自然规律。说到这里,我不禁在想,这种废柴通信模型,应该不会有什么项目会用吧?可现实就喜欢打我的脸,又或许是我喜欢被现实打脸吧!老版的Tomcat就用了,而且利用这种通信模型开发的Tomcat还被广泛应用于各大公司的各个项目中。那Tomcat的工程师是如何解决BIO通信模式下服务端的线程数量随客户端数量增加而不断增加的问题呢?解决的方案很简单,线程池,即规定服务端所能承受的最大线程数,超过规定数量的客户端连接直接被废弃或延迟处理。这种方案舍弃了一部分客户的诉求,只服务了一部分客户,所以说现实中真的存在绝对公平吗?不过这种方案充分发挥了计算机高速处理的能力。然而这种方案依旧没有解决计算机资源浪费的问题。好了,我们还是先看一下NIO是如何处理客户端与服务端之间的连接吧!下面这幅图展示NIO模式下客户端与服务端之间连接的处理方式:

从图中可以看出客户端与服务端建立连接后,服务端会将该连接包装成一个Channel对象,然后将其注册到Selector上,接着启动一个单独的线程处理该Selector。注意:Channel注册到Selector上时,会指定自己关心的事件,如果Selector检测到该事件发生,则会获取事件,并对其进行处理。这就好比现在的银行通过在大堂增加客户服务台一样。先由其对到银行办理业务的客户进行过滤,如果客户需求简单,比如咨询等,则直接处理,否则流转该客户到业务处理窗口处理。在这里大堂中新增加的客户服务台就是上图中的选择器——Selector,而客户就是上图中的客户端——Client,银行服务大楼就是我们通常所说的服务器——Server,各个服务窗口就是服务器中的线程——Thread,而服务窗口中坐着的办事员就是本文中提到的业务组件。通过银行这个服务客户的案例,我对NIO的三大组件的作用有了一些具象的认识,那如何利用这些工具写一个可用的服务器呢?

2 NIO在 Tomcat 中的应用

通过上一小节我对服务端编程中的一些基本概念有了大致的了解,但这无法让我停下 探索这片繁茂之地的脚步。在不考虑性能和其他复杂问题的情况下,我们可以利用BIO工具在很短时间内写出一个小型服务端程序,但我实在不知道如何利用NIO工具快速写出一个这样的服务端小程序。为了解决这个问题,我曾疯狂百度,甚至购买各种课程,包括本篇文章开头提到的Netty课程,然而结果令人遗憾。于是我开启了“摆烂”模式,主动寻找那些与这件事情无关,却能够带给我激情的事情做,比如玩游戏。直到一年前,我发现自己所面临的问题依旧如此严峻,于是又开始神经质式的学习模式。这一时期,我主动搜索并购买各种与Tomcat相关的资料,想以此构建全方位的Tomcat知识体系,并提升自己的专业技术能力,进而博得不错的机会。不过,这次和上次追求NIO速成的结果一样。然而有句俗话讲:失之东隅,收之桑榆。在这次追求速成的过程中,我对NIO这个知识体系却有了一些不一样的理解。虽然我不知道如何描述这种理解,但总觉得有必要对其中使用的NIO体系来一次全面梳理。

我们都知道Tomcat是一个Web服务器,通常用于部署Web应用程序。想必使用过Tomcat的你也知道其启动后,通常会监听服务端8080端口,以等待客户端的连接。看到这不知您是否想到了Socket编程中配置监听端口的步骤,所以按照Java Socket编程来理解Tomcat中应该也有这样的步骤。在正式梳理前,先来看一下Tomcat的体系结构,具体如下图所示:

从图中不难看出,Tomcat的体系结构非常复杂的,因此想要快速学好,对于基础薄弱的人来讲是有一定难度的。从图中可以看出,Tomcat的组件是一层套一层的,比如Server组件包含一个Service组件,Service组件包含一个Connector组件和Engine组件,Engine组件包含了一个Host组件,Host组件包含了Context组件,Context组件包含了其他组件和Web应用。这种模式与俄罗斯套娃非常类似。如果没有玩过俄罗斯套娃,相信您应该在抖音短视频中刷到过《大杨哥整蛊小杨哥》的视频,每当“单纯”的小杨哥以为要拿到礼物时,总会被容器内缩小版的容器整到崩溃。看着他愤怒又无奈的样子,着实可笑。继续回到要讨论的Connector组件上。从图中可以知道它位于Service组件中,并且持有一个线程池。另外从图中也可以看出浏览器到Web应用的所有请求都要先进到Connector组件中。那Connector组件是做什么的呢?Connector代表了一个连接,通过它可以实现客户端对Tomcat的接入,比如可以启动一个服务端套接字ServerSocket接收客户端的请求等。那Tomcat的工程师团队开发这个组件的思路是什么呢?

  1. 初始化。这个过程位于ConnectorCreateRule的begin()方法中。这个方法中有这样一段代码:Connector con = new Connector(attributes.getValue("protocol"));
  2. 启动。这个过程位于StandardService的startInternal()方法中。这个方法中有这样一段代码:
for (Connector connector: connectors) {try {// If it has already failed, don't try and start itif (connector.getState() != LifecycleState.FAILED) {connector.start();}} catch (Exception e) {log.error(sm.getString("standardService.connector.startFailed",connector), e);}
}

 这里就不再罗列该组件停止的逻辑了,因为它与启动的逻辑基本一致,有兴趣的可以自己翻看一下Tomcat源码。下面继续翻阅Connector类的start()方法,最终会走到Connector类的startInternal()方法中,这个方法有这样一段代码:protocolHandler.start()。这段代码就是启动ProtocolHandler的实现类。那这个实现类会是谁呢?通过阅读Connector类的源码可以知道这个实现类是org.apache.coyote.http11.Http11NioProtocol。这个类是干什么的?在继续梳理这个类之前,还是先看一下Connector的继承结构,具体如下图所示:

这里的Lifecycle接口定义了Tomcat中各组件的生命周期,其中的方法都与这个生命周期有关,比如:init()、start()、stop()。【这句描述仅是个人理解,如有不对,还请见谅】这块知识并非本系列文章的重点,所以这里不再赘述。下面还是看一下与本系列主题具有密切关系的组件Http11NioProtocol,其继承结构如下所示:

图中的浅色部分就是Http11NioProtocol类的继承结构。从图中可以看出Http11NioProtocol最终实现了ProtocolHandler接口。现在看这些类的继承结构又有什么意义呢?还记得前面提到的Connector对象的初始化吗?在Connector类的构造函数中有这样一段代码,如下所示:

Class<?> clazz = Class.forName(protocolHandlerClassName);
p = (ProtocolHandler) clazz.getConstructor().newInstance();

也就是说,在创建Connector对象的时候,就会为该类中ProtocolHandler类型的属性protocolHandler进行初始化,而且其实际类型就是刚刚看到的Http11NioProtocol【注意:这里是通过java反射方式创建ProtocolHandler对象的】。知道这个又有什么意义呢?我们先来看一下Http11NioProtocol类的构造方法,如下所示:

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

这个构造方法会调用父类构造方法,并传递一个NioEndpoint类型的对象进去。那这个父类构造做了什么呢?具体源码如下所示:

public AbstractHttp11JsseProtocol(AbstractJsseEndpoint<S,?> endpoint) {super(endpoint);
}public AbstractHttp11Protocol(AbstractEndpoint<S,?> endpoint) {super(endpoint);setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);ConnectionHandler<S> cHandler = new ConnectionHandler<>(this);setHandler(cHandler);getEndpoint().setHandler(cHandler);
}public AbstractProtocol(AbstractEndpoint<S,?> endpoint) {this.endpoint = endpoint;setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
}

从源码可以看到,这些构造方法其实就是做一些属性的初始化,比如:connectionTimeout、soLinger、tcpNoDelay、endpoint、handler等。这里尤其要注意的是endpoint和handler这两个属性,主要关注以下几点:

  1. ProtocolHandler的实现类Http11NioProtocol对象持有一个Handler<S>类型的对象
  2. AbstractEndpoint<S,U>的实现类NioEndpoint对象持有一个Handler<S>类型的对象
  3. ProtocolHandler的实现类Http11NioProtocol对象持有一个AbstractEndpoint<S,U>类型的对象

注意:Http11NioProtocol对象和NioEndpoint对象持有的Handler<S>对象是同一个。

说实在话,我有点搞不清楚自己为什么要梳理这个了,但不梳理又总觉得无法构建起一个完整的知识体系。因此在下恳请各位看官稍安勿躁。先让我们一起看一下NioEndpoint的继承体系,具体如下图所示:

从图中可以看出NioEndpoint类最终继承了AbstractEndpoint<S, U>抽象类,同时其指定的泛型类型为NioChannel和SocketChannel。

下面我们再来看一下Handler<S>的继承体系及其实现类,具体的继承体系可以参见下面这幅图:

从图中不难看出Handler接口位于AbstractEndpoint类中,前者是AbstractEndpoint类中的一个静态内部接口。而ConnectionHandler实现了这个接口,不过这个实现类位于AbstractProtocol类中,且前者是后者中的一个静态内部类。看到这里是不是有种无间道的感觉?如果您可以一下子理解,我真的由衷钦佩,因为直到现在我还是有点懵,不知道为什么会这样做。不过再看一下前面梳理出来的三个重要步骤,我觉得自己好像懂了,这三个步骤是:

不过这种认识好像不是特别清晰,所以我想先了解一下NioEndpoint类的作用。了解一个事务的最好的方法就是触碰它。NioEndpoint类究竟做了什么呢?我觉得可以从下面几个方面来了解一下。

2.1 初始化

为了弄清楚NioEndpoint类的作用,本小节我们以Connector类的initInternal()方法为切入点来了解。在这个方法中有这样一句代码:protocolHandler.init()。也就是说Connector在进行初始化的时候,会主动调用ProtocolHandler实现类中的init()方法,根据上面的梳理这个调用先走到AbstractEndpoint<S,U>类中的init()方法中然后这个方法会先判断bindOnInit的值是否为true,如果是则继续调用AbstractEndpoint<S,U>类中的bindWithCleanup()方法,接着这个方法会调用AbstractEndpoint<S, U>中的bind()方法。注意这个方法是个抽象方法,具体的实现逻辑位于子类中。因此这个调用最终走的是NioEndpoint类中的bind()方法,最后这个方法又调用了本类中的initServerSocket()方法。这个方法主要完成了ServerSocketChannel对象的创建及初始化。这个调用过程涉及到的源码如下所示:

// 1) Connector类中的initInternal()方法
protected void initInternal() throws LifecycleException {// .........
try {// 调用 ProtocolHandler 的 init 方法完成相应的初始化// 执行过程为:调用 AbstractHttp11Protocol 的 init 方法(该方法内部调用父类的 init 方法,父类的 init 方法中再调用 endpoint 的 init 方法)// Endpoint 类的 init 方法主要完成protocolHandler.init();} catch (Exception e) {throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);}
}
// 2) AbstractProtocol类中的init()方法
public void init() throws Exception {
// .........// 调用 NioEndpoint 类的 init 方法/*** Endpoint 类结构图为:* AbstractEndpoint - 抽象类* AbstractJsseEndpoint 继承于 AbstractEndpoint;AprEndpoint 继承于 AbstractEndpoint* Nio2Endpoint / NioEndpoint 继承于 AbstractJsseEndpoint*/endpoint.init();
}
// 3) AbstractEndpoint类中的init()方法
public void init() throws Exception {if (bindOnInit) {bindWithCleanup();bindState = BindState.BOUND_ON_INIT;}// .........
}
// 4) AbstractEndpoint类中的bindWithCleanup()方法
private void bindWithCleanup() throws Exception {try {bind(); // 绑定到特定端口} catch (Throwable t) {// Ensure open sockets etc. are cleaned up if something goes// wrong during bindExceptionUtils.handleThrowable(t);unbind(); // 释放绑定throw t;}
}
// 4) NioEndpoint类中的bind()方法
public void bind() throws Exception {// 初始化 ServerSocketinitServerSocket();setStopLatch(new CountDownLatch(1));// Initialize SSL if neededinitialiseSsl();
}
// 5) NioEndpoint类中的initServerSocket()方法
protected void initServerSocket() throws Exception {if (getUseInheritedChannel()) {// Retrieve the channel provided by the OS// System.inheritedChannel() 用于返回从生成当前JVM(Java虚拟机)的实体继承的通道Channel ic = System.inheritedChannel();if (ic instanceof ServerSocketChannel) {serverSock = (ServerSocketChannel) ic;}if (serverSock == null) {throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));}} else {// 调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel// serverSock 为 ServerSocketChannel 类型serverSock = ServerSocketChannel.open();socketProperties.setProperties(serverSock.socket());// TODO 绑定 8080 端口,并对其进行监听// bind()方法将 ServerSocket 类绑定到指定的地址,而 ServerSocketChannel 类也有bind() 方法,该方法 public final ServerSocketChannel bind(SocketAddress local) 的作用是将通道的套接字绑定到本地地址并侦听连接InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());serverSock.socket().bind(addr,getAcceptCount());}serverSock.configureBlocking(true); //mimic APR behavior
}

梳理到这里我们看到了Tomcat中用到的NIO相关的第一个类ServerSocketChannel。通过这个源码不难看出,Tomcat的工程师在开发时也是按照JDK的规范一步步书写的:

  1. 通过ServerSocketChannel类提供的open()方法创建一个ServerSocketChannel对象
  2. 为该ServerSocketChannel对象设置相关属性,譬如:soTimeout、receiveBufferSize等
  3. 将该ServerSocketChannel对象绑定到指定端口上,譬如8080,注意这里的8080是通过配置文件配置的

2.2 启动

在上一小节中,通过梳理initInternal()方法,我们知道了NioEndpoint的第一个作用:初始化ServerSocketChannel对象。这一小节我们继续梳理NioEndpoint的作用,不过这次的切入点是Connector类的startInternal()方法。该方法有这样一句代码:protocolHandler.start()。这说明Connector类在启动的时候会主动调用ProtocolHandler实现类中的start()方法,根据前面的梳理这个调用先走到AbstractEndpoint<S,U>类中的init()方法中然后这个方法会先判断bindState的状态是否是BindState.UNBOUND,如果是则继续走初始化流程,否则调用AbstractEndpoint<S,U>类中的startInternal()方法。注意这个方法是个抽象方法,具体的实现逻辑位于子类中。因此这个调用最终走到了NioEndpoint类中的startInternal()方法中。上面这个调用过程涉及到的源码如下所示:

// 1) Connector类中的startInternal()方法
protected void startInternal() throws LifecycleException {
// .........try {// 执行 protocolHandler 对象的 start 方法protocolHandler.start();} catch (Exception e) {throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);}
}
// 2) AbstractProtocol类中的start()方法
public void start() throws Exception {// .........endpoint.start();// .........
}
// 3) AbstractEndpoint类中的start()方法
public final void start() throws Exception {// 如果没有绑定到相关端口,则执行绑定操作if (bindState == BindState.UNBOUND) {bindWithCleanup();bindState = BindState.BOUND_ON_START;}// 启动监听线程startInternal();
}// 4) NioEndpoint类中的bind()方法
public void bind() throws Exception {// 初始化 ServerSocketinitServerSocket();setStopLatch(new CountDownLatch(1));// Initialize SSL if neededinitialiseSsl();
}
// 5) NioEndpoint类中的startInternal()方法
public void startInternal() throws Exception {if (!running) {running = true;paused = false;if (socketProperties.getProcessorCache() != 0) {processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,socketProperties.getProcessorCache());}if (socketProperties.getEventCache() != 0) {eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,socketProperties.getEventCache());}if (socketProperties.getBufferPool() != 0) {nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,socketProperties.getBufferPool());}// Create worker collectionif (getExecutor() == null) {createExecutor();}// 初始化 LimitLatch ,其用于控制最大线程数initializeConnectionLatch();// Start poller threadpoller = new Poller();// 创建通道管理器 Selector ,并启动在后台运行Thread pollerThread = new Thread(poller, getName() + "-Poller");pollerThread.setPriority(threadPriority);pollerThread.setDaemon(true);pollerThread.start();// 开启接收线程,后台运行,用于接收客户端发来的请求,调用的是父类的方法// 这个方法实际是在它的超类AbstractEndpoint里面startAcceptorThread();}
}

通过NioEndpoint类的源码不难看出startInternal()这个方法主要完成了这样几件事:

  1. 创建工作线程池【这里涉及到了线程池的知识】
  2. 创建Poller工作线程,为其设置相关属性,并启动之
  3. 启动Acceptor线程

这里我们主要看Poller和Acceptor这两个类。它们都实现了Runnable接口,并对其中的run方法进行了实现。这里要注意一下:Acceptor类持有了当前的AbstractEndpoint对象,即NioEndpoint对象,同时Acceptor的泛型为SocketChannel。下面先来看一下Acceptor类的run()方法,其源码如下所示:

public void run() {int errorDelay = 0;long pauseStart = 0;try {// Loop until we receive a shutdown commandwhile (!stopCalled) {// Loop if endpoint is paused.// There are two likely scenarios here.// The first scenario is that Tomcat is shutting down. In this// case - and particularly for the unit tests - we want to exit// this loop as quickly as possible. The second scenario is a// genuine pause of the connector. In this case we want to avoid// excessive CPU usage.// Therefore, we start with a tight loop but if there isn't a// rapid transition to stop then sleeps are introduced.// < 1ms       - tight loop// 1ms to 10ms - 1ms sleep// > 10ms      - 10ms sleepwhile (endpoint.isPaused() && !stopCalled) {if (state != AcceptorState.PAUSED) {pauseStart = System.nanoTime();// Entered pause statestate = AcceptorState.PAUSED;}if ((System.nanoTime() - pauseStart) > 1_000_000) {// Paused for more than 1mstry {if ((System.nanoTime() - pauseStart) > 10_000_000) {Thread.sleep(10);} else {Thread.sleep(1);}} catch (InterruptedException e) {// Ignore}}}if (stopCalled) {break;}state = AcceptorState.RUNNING;try {//if we have reached max connections, wait// 如果达到最大连接数,则等待// 判断连接数是否大于阈值 (默认为 10000), 否则将会休眠// 里面用到了 AQSendpoint.countUpOrAwaitConnection();// Endpoint might have been paused while waiting for latch// If that is the case, don't accept new connectionsif (endpoint.isPaused()) {continue;}// 下面这段相当于 SocketChannel socket = null;U socket = null;try {// Accept the next incoming connection from the server socket// 接收请求,serverSocketAccept 方法由子类实现,本代码中的子类即 NioEndpoint// 阻塞, 直到有连接// bind 方法中已经设置为 阻塞// 监听socket负责接收新连接// Acceptor 在启动后会阻塞在 endpoint.serverSocketAccept(); 方法处,当有新连接到达时,该方法返回一个 SocketChannel// 这里的 endpoint 为 NioEndpointsocket = endpoint.serverSocketAccept();} catch (Exception ioe) {// We didn't get a socketendpoint.countDownConnection();if (endpoint.isRunning()) {// Introduce delay if necessaryerrorDelay = handleExceptionWithDelay(errorDelay);// re-throwthrow ioe;} else {break;}}// Successful accept, reset the error delayerrorDelay = 0;// Configure the socketif (!stopCalled && !endpoint.isPaused()) {// setSocketOptions() will hand the socket off to// an appropriate processor if successful// 将 Socket 交给 Poller 线程处理//处理接受到的 socket 对象并发布事件if (!endpoint.setSocketOptions(socket)) {endpoint.closeSocket(socket);}} else {endpoint.destroySocket(socket);}} catch (Throwable t) {ExceptionUtils.handleThrowable(t);String msg = sm.getString("endpoint.accept.fail");// APR specific.// Could push this down but not sure it is worth the trouble.if (t instanceof Error) {Error e = (Error) t;if (e.getError() == 233) {// Not an error on HP-UX so log as a warning// so it can be filtered out on that platform// See bug 50273log.warn(msg, t);} else {log.error(msg, t);}} else {log.error(msg, t);}}}} finally {stopLatch.countDown();}state = AcceptorState.ENDED;
}

这个方法看着很长,但仔细研究您会发现,其本质上就是一个while(true)循环,循环体内的主要逻辑就是:socket = endpoint.serverSocketAccept()和endpoint.setSocketOptions(socket),其中前者是从被监听端口(8080)上拿到客户端的Socket连接,然后将其交给Acceptor持有的NioEndpoint对象(这个对象是在Connector创建ProtocolHandler时创建的)进行处理(调用NioEndpoint类上的setSocketOptions()方法)。因此我个人觉得Acceptor线程的主要作用就是接收并创建客户端Socket连接。至于前面提到的NioEndpoint中的setSocketOptions()方法究竟做了什么,个人觉得没有必要详细描述,大家翻看源码便可一目了然。这里我只用自己的话总结一下:这个方法将从8080端口获取到的SocketChannel连接包装到NioSocketWrapper对象中,然后将该对象注册到Poller线程上,并由其完成相应的处理。这里提到的Poller是我们要梳理的第二个重要的后台线程。它是一个位于NioEndpoint类中且实现了Runnable接口的线程类。它里面有一个Selector类型的属性selector,这个属性的初始化是在Poller线程的构造方法中完成的,具体代码为:this.selector = Selector.open()。这就是我们要梳理的NIO中的第二个重要类Selector。梳理到这里我不禁好奇Poller类创建这个对象干做什么呢?因为按照Netty课程中的讲解,Selector不是出现在这里的。如果要了解Selector对象的作用还得翻看Poller类的源码,先来看一下Poller类的run()方法,其源码如下所示:

public void run() {// Loop until destroy() is called// 线程启动后,这里会一直循环,直到有相关事件过来,可以处理为止while (true) {boolean hasEvents = false;try {if (!close) {// 当前通道是否有需要执行的事件// 检查 events 事件队列是否存在元素// 然后遍历 events 事件队列的所有元素, 将 Socket 以 OP_READ 事件注册到 Selector 上hasEvents = events();if (wakeupCounter.getAndSet(-1) > 0) {// If we are here, means we have other stuff to do// Do a non blocking selectkeyCount = selector.selectNow();System.out.println("111111111111");} else {keyCount = selector.select(selectorTimeout);System.out.println("2222222222222");}wakeupCounter.set(0);}if (close) {events();timeout(0, false);try {selector.close();} catch (IOException ioe) {log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);}break;}// Either we timed out or we woke up, process events firstif (keyCount == 0) {hasEvents = (hasEvents | events());}} catch (Throwable x) {ExceptionUtils.handleThrowable(x);log.error(sm.getString("endpoint.nio.selectorLoopError"), x);continue;}Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;// Walk through the collection of ready keys and dispatch// any active event.while (iterator != null && iterator.hasNext()) {System.out.println("222222222222212412341234123412341");SelectionKey sk = iterator.next();iterator.remove();NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();// Attachment may be null if another thread has called// cancelledKey()// 处理到达的请求if (socketWrapper != null) {// 处理 socket 请求processKey(sk, socketWrapper);}}// Process timeoutstimeout(keyCount,hasEvents);}getStopLatch().countDown();
}

Netty课程中提到的编码逻辑在源码的后半部分体现出来了。首先调用Selector的selectorKeys()方法获得发生了某种事件SocketChannel,然后判断事件类型,并作进一步处理。原本的SocketChannel被NioSocketWrapper替换了;原本判断事件类型并做进一步处理的逻辑,在这里被交给了processKey()方法(这个方法会判断当前发生的事件类型是读,还是写,又或者是文件处理)。有兴趣的读者可以自己翻阅一下Tomcat源码。到这里我们看到了NIO三大组件中的第二个Selector(当然这里也出现了SocketChannel,由于它与ServerSocketChannel同属Channel,所以这里就不再罗列了)。

2.3 Tomcat中用到的NIO Buffer

在前两小节我们梳理了Tomcat用到的NIO中的两大组件Channel和Selector,那Tomcat是否有用到NIO中的第三大组件Buffer呢?这个答案自然是是的。还记得第二小节提到的包装SocketChannel的方法吗?是的,就是NioEndpoint中的setSocketOptions()方法,这个方法会首先创建一个SocketBufferHandler对象,具体创建代码为:

SocketBufferHandler bufhandler = new SocketBufferHandler(socketProperties.getAppReadBufSize(),socketProperties.getAppWriteBufSize(),socketProperties.getDirectBuffer());
// 下面再看一下这个类的构造方法
public SocketBufferHandler(int readBufferSize, int writeBufferSize, boolean direct) {this.direct = direct;if (direct) {// 创建直接缓冲区 - 用于读readBuffer = ByteBuffer.allocateDirect(readBufferSize);// 创建直接缓冲区 - 用于写writeBuffer = ByteBuffer.allocateDirect(writeBufferSize);} else {// 设置缓冲区初始容量 - 用于读readBuffer = ByteBuffer.allocate(readBufferSize);// 设置缓冲区初始容量 - 用于写writeBuffer = ByteBuffer.allocate(writeBufferSize);}
}

从这段源码中可以看到一个名为ByteBuffer的类,它就是NIO中的第三大组件Buffer。关于它的作用及用法我们将在后续章节中逐步展开,这里就不再啰嗦了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/386052.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

乐尚代驾六订单执行一

加载当前订单 需求 无论是司机端&#xff0c;还是乘客端&#xff0c;遇到页面切换&#xff0c;重新登录小程序等&#xff0c;只要回到首页面&#xff0c;查看当前是否有正在执行订单&#xff0c;如果有跳转到当前订单执行页面 之前这个接口已经开发&#xff0c;为了测试&…

JAVAWeb实战(后端篇)

因为前后端代码内容过多&#xff0c;这篇只写后端的代码&#xff0c;前端的在另一篇写 项目实战一&#xff1a; 1.创建数据库,表等数据 创建数据库 create database schedule_system 创建表&#xff0c;并添加内容 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0;-- ---------…

Node.js版本管理工具之NVM

目录 一、NVM介绍二、NVM的下载安装1、NVM下载2、卸载旧版Node.js3、安装 三、NVM配置及使用1、设置nvm镜像源2、安装Node.js3、卸载Node.js4、使用或切换Node.js版本5、设置全局安装路径和缓存路径 四、常用命令技术交流 博主介绍&#xff1a; 计算机科班人&#xff0c;全栈工…

Win11 操作(四)g502鼠标连接电脑不亮灯无反应

罗技鼠标连接电脑不亮灯无反应 前言 罗技技术&#x1f4a9;中&#x1f4a9;&#xff0c;贴吧技术神中神&#xff01; 最近买了一个g502&#xff0c;结果买回来直接插上电脑连灯都不亮&#xff0c;问了一下客服。客服简单的让我换接口&#xff0c;又是下载ghub之类的&#xf…

Linux 安装 GDB (无Root 权限)

引入 在Linux系统中&#xff0c;如果你需要在集群或者远程操作没有root权限的机子&#xff0c;安装GDB&#xff08;GNU调试器&#xff09;可能会有些限制&#xff0c;因为通常安装新软件或更新系统文件需要管理员权限。下面我们介绍可以在没有root权限的情况下安装GDB&#xf…

ElasticSearch核心之DSL查询语句实战

什么是DSL&#xff1f; Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。 DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。目前常用的框架查询方法什么的底层都是构建DSL语句实现的&#xff0c;所以你必…

openFeign配置okhttp

原来的项目出现了性能问题&#xff0c;老大不知道怎么的&#xff0c;让我改openFeign线程池为okhttp&#xff0c;说原生的不支持线程池性能比较差。 原openFeign配置文章地址 一、pom文件 <dependency><groupId>org.springframework.cloud</groupId><arti…

【短视频矩阵系统源码部署/技术应用开发】

短视频矩阵系统&#xff1a;选择专业服务商指南 该短视频矩阵系统由多个关键模块组成&#xff0c;包括混剪算法、账号管理与发布、消息处理以及数据管理等。为了优化带宽使用&#xff0c;文件导出功能已被独立处理。 此外&#xff0c;系统还集成了后台运营管理功能。 在技术架…

Python设计模式 - 工厂方法模式

定义 工厂方法模式是一种创建型设计模式&#xff0c;它定义一个创建对象的接口&#xff0c;让其子类来处理对象的创建&#xff0c;而不是直接实例化对象。 结构 抽象工厂&#xff08;Factory&#xff09;&#xff1a;声明工厂方法&#xff0c;返回一个产品对象。具体工厂类都…

git等常用工具以及cmake

一、将git中的代码克隆进电脑以及常用工具介绍 1.安装git 首先需要安装git sudo apt install git 注意一定要加--recursive&#xff0c;因为文件中有很多“引用文件“&#xff0c;即第三方文件&#xff08;库&#xff09;&#xff0c;加入该选项会将文件中包含的子模…

区块链技术如何重塑医疗健康行业未来?

区块链在医疗领域的应用日益广泛&#xff0c;主要体现在以下几个方面&#xff1a; 一、医疗数据管理 电子病历管理&#xff1a; 区块链技术可以用于构建去中心化的电子病历系统&#xff0c;确保病历数据的不可篡改性和安全性。患者可以通过区块链平台安全地管理自己的电子病历…

30岁决心转行,AI太香了

今天是一篇老学员的经历分享&#xff0c;此时的王同学在大洋彼岸即将毕业&#xff0c;手握多家北美大厂offer&#xff0c;一片明媚。谁能想到王同学的转码之路竟始于一场裁员&#xff0c;这场访谈拉开了他的回忆。 最近总刷到一些关于转行的话题&#xff0c;很多刚毕业的同学喜…

【OpenCV C++20 学习笔记】图片融合

图片融合 原理实现结果展示完整代码 原理 关于OpenCV的配置和基础用法&#xff0c;请参阅本专栏的其他文章&#xff1a;垚武田的OpenCV合集 这里采用的图片熔合的算法来自Richard Szeliski的书《Computer Vision: Algorithms and Applications》&#xff08;《计算机视觉&#…

极简Springboot+Mybatis-Plus+Vue零基础萌新都看得懂的分页查询(富含前后端项目案例)

目录 springboot配置相关 依赖配置 yaml配置 MySQL创建与使用 &#xff08;可拿软件包项目系统&#xff09; 创建数据库 创建数据表 mybatis-plus相关 Mapper配置 ​编辑 启动类放MapperScan 启动类中配置 添加config配置文件 Springboot编码 实体类 mapperc(Dao…

LINUX -exec函数族

1、功能&#xff1a; *让父子进程来执行不相干的操作 *能够替换进程地址空间的代码.text段 *执行另外的程序&#xff0c;不需要创建额外的的地址空间 *当前程序中调用另外一个应用程序 2、执行目录下的程序&#xff1a; *指定执行目录下的程序 int execl(const char *path,…

工业三防平板,高效能与轻便性的结合

在当今数字化、智能化的工业时代&#xff0c;工业三防平板作为一种创新的设备&#xff0c;正以其独特的优势在各个领域发挥着重要作用。它不仅具备高效能的处理能力&#xff0c;还拥有出色的轻便性&#xff0c;为工业生产和管理带来了前所未有的便利。 一、高效能的核心动力 工…

Python爬虫-中国汽车市场月销量数据

前言 本文是该专栏的第34篇,后面会持续分享python爬虫干货知识,记得关注。 在本文中,笔者将通过某汽车平台,来采集“中国汽车市场”的月销量数据。 具体实现思路和详细逻辑,笔者将在正文结合完整代码进行详细介绍。废话不多说,下面跟着笔者直接往下看正文详细内容。(附…

GroupMamba实战:使用GroupMamba实现图像分类任务(一)

摘要 状态空间模型&#xff08;SSM&#xff09;的最新进展展示了在具有次二次复杂性的长距离依赖建模中的有效性能。GroupMamba解决了将基于SSM的模型扩展到计算机视觉领域的挑战&#xff0c;特别是大型模型尺寸的不稳定性和低效性。GroupMamba在ImageNet-1K的图像分类、MS-CO…

DC-DC 反激式电路的共模噪声分析

本系列文章的第 5 和第 6 部分[1-7]介绍有助于抑制非隔离 DC-DC 稳压器电路传导和辐射电磁干扰 (EMI) 的实用指南和示例。当然&#xff0c;如果不考虑电隔离设计&#xff0c;DC-DC 电源 EMI 的任何处理方式都不全面&#xff0c;因为在这些电路中&#xff0c;电源变压器的 EMI 性…

Python常用内置库介绍

Python作为一门强大且易学的编程语言&#xff0c;内置了许多功能强大的库&#xff0c;让开发者能够更加便捷地完成各种任务。本文中&#xff0c;我将详细介绍Python中常用的内置库。 math&#xff1a;提供数学函数&#xff0c;如三角函数、对数函数等。 示例&#xff1a;计算平…