1.概述
我们在开发中大多数使用Tomcat作为web服务器,今天我们来讨论一下Tomcat的BIO和NIO数据处理模式,作为丰富我们的Tomcat理论知识大家有必要掌握它们。
下面我们看一下Tomcat支持的四种线程模式
线程模式 | 描述 |
---|---|
BIO | 阻塞式IO,即Tomcat使用传统的java.io进行操作。该模式下每个请求都会创建一个线程,对性能开销大,不适合高并发场景。优点是稳定,适合连接数目小且固定架构 |
NIO | 非阻塞式IO,jdk1.4 之后实现的新IO。该模式基于多路复用选择器监测连接状态在通知线程处理,从而达到非阻塞的目的。比传统BIO能更好的支持并发性能。Tomcat 8.0之后默认采用该模式 |
APR | 全称是 Apache Portable Runtime/Apache可移植运行库),是Apache HTTP服务器的支持库。可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作。使用需要编译安装APR 库 |
AIO | 异步非阻塞式IO,jdk1.7后之支持 。与nio不同在于不需要多路复用选择器,而是请求处理 |
其实也提供异步非阻塞模式(AIO),但今天我们只研究同步工作原理,后续再给大家讲解异步模式。
2.BIO模式
2.1 定义
BIO:同步阻塞IO(一个连接一个线程),数据的读写必须阻塞在一个线程内等待其完成。例如有一排水壶在烧开水,BIO的工作模式就是叫一个线程停留在一个水壶那儿,直到这个水壶的水烧开才去处理下一个水壶,但实际上线程在等待水壶烧开的时间段什么都没有做。
BIO是Tomcat8以前的默认IO模式,为了方便大家理解阻塞的特点,我们来写一个IO模型,默认为阻塞模式,其中需要的接口ServerSocketChannel、SocketChannel 、Buffer
2.2 BIO运行流程
-
服务器启动一个serverSocket;
-
客户端启动Socket对服务器进行通信,默认情况下服务器需要对每个客户建立一个线程与之通讯;
-
客户端发出请求后,先咨询服务器,是否有线程响应,如果没有则会等待,或者被拒绝;
-
如果有响应,客户端线程会等待请求结束后,再继续执行。
2.3 BIO简易模型
public class Server {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口进来的 TCP 链接
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 这里会阻塞,直到有一个请求的连接进来
SocketChannel socketChannel = serverSocketChannel.accept();
// 开启一个新的线程来处理这个请求,然后在 while 循环中继续监听 8080 端口
SocketHandler handler = new SocketHandler(socketChannel);
new Thread(handler).start();
}
}
}
这里看一下新的线程需要做什么,SocketHandler:
public class SocketHandler implements Runnable {
private SocketChannel socketChannel;
public SocketHandler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 将请求数据读入 Buffer 中
int num;
while ((num = socketChannel.read(buffer)) > 0) {
// 读取 Buffer 内容之前先 flip 一下
buffer.flip();
// 提取 Buffer 中的数据
byte[] bytes = new byte[num];
buffer.get(bytes);
String re = new String(bytes, "UTF-8");
System.out.println("收到请求:" + re);
// 回应客户端
ByteBuffer writeBuffer = ByteBuffer.wrap(("我已经收到你的请求,你的请求内容是:" + re).getBytes());
socketChannel.write(writeBuffer);
buffer.clear();
}
} catch (IOException e) {
IOUtils.closeQuietly(socketChannel);
}
}
}
最后,客户端 SocketChannel 的使用,客户端比较简单
public class SocketChannelTest {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 发送请求
ByteBuffer buffer = ByteBuffer.wrap("1234567890".getBytes());
socketChannel.write(buffer);
// 读取响应
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int num;
if ((num = socketChannel.read(readBuffer)) > 0) {
readBuffer.flip();
byte[] re = new byte[num];
readBuffer.get(re);
String result = new String(re, "UTF-8");
System.out.println("返回值: " + result);
}
}
}
以上代码,意味着来一个新的连接,我们就新开一个线程来处理这个连接,之后的操作全部由那个线程来完成。
那么,这个模式下性能瓶颈在哪里呢?
1、首先每次来一个连接就开一个线程,对于少量并发请求还可以勉强完成任务,如果大量请求成百上千次就会出现请求阻塞,内存过渡消耗,线程切换的开销非常大。
2、其次,阻塞操作在这里也是一个问题,accept()默认就是阻塞操作,当请求过来时马上进行新建线程使用SocketChannel,但是这里不代表对方的数据已全部传输过来,所以SocketChannel#read方法将阻塞,等待数据,明显这个等待是不值得的,同理write方法的等待也是不值得的。
2.4 Tomcat中BIO的工作原理
由于BIO是Tomcat中的默认运行模式,这里不需要任何设置环境,下面我们看一下它的工作原理:
1.Tomcat通过Acceptor接受到一个socket链接请求后
2.Tomcat将该请求封装成一个SocketProcessor连接线程;并放入Executor连接池中
3.SocketProcessor负责从socket中阻塞读取数据,并且向socket中阻塞写入数据;
4.最后每一个SocketProcessor对应了一个Http11Processor,并负责解析自己的请求数据。
2-1634291875649.pngTomcat通过Acceptor接收到一个socket链接请求后,会将该请求封装成一个SocketProcessor连接线程,然后将它放入到连接池中。SocketProcessor负责从socket中阻塞读取数据,并且向socket中阻塞写入数据。每个SocketProcessor对应了一个Http11Processor负责解析请求数据。
说完了阻塞IO模式的缺点,我们再介绍非阻塞IO。
3.NIO模式
3.1 定义
NIO/NIO2 :同步非阻塞(一个线程处理多个请求,多路复用;比如在redis的处理连接的实现),同时支持阻塞和非阻塞,但主要是使用同步非阻塞IO,例如同样有一排水壶在烧水,一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生改变(水烧开),从而进行下一步操作。
NIO是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。
java nio 是一个基于缓冲区、并能提供非阻塞I/O踩着的java api,因此nio 也被看成是non-blocking I/O的缩写。 它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio 模式来运行只需要在Tomcat安装目录conf/server.xml 中将对应的protocol的属性值改为 org.apache.coyote.http11.Http11NioProtocol即可。非阻塞IO的核心是使用一个Selector来管理多个通道,可以是SocketChannel,也可以是ServerSocketChannel。
3.1 Tomcat设置NIO模式:
1.添加manager/status用户
<role rolename="manager-gui"/>
<user username="tomcat" password="15715746746" roles="manager-gui"/>
2.修改server.xml配置,这里protocol设置 Http11NioProtocol类, 不设置为BIO 。
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443" />
3.重启Tocmat
3-1634292901351.png3.2 Tomcat中NIO的工作原理
NIO作为非阻塞线程IO操作,我们先看一下它的基本思想:
1.由一个专门的线程处理所有的I/O事件,并负责分发;
2.事件驱动机制,而不再同步的去监视事件;
3.线程间通过wait、notify等方式通讯,保证每次上下文切换都是有意义的,减少无谓的线程切换;
1.png通过上图我们解读NIO工作原理
1.Tomcat利用Acceptor来阻塞获取socket连接,NIO中叫socketChannel;
2.Acceptor接收到socketChannel后,需要将socketChannel绑定到一个Selector中,并注册读事件;
3.此时开启一个线程来轮询Selector中是否存在就绪事件,如果存在就将就绪事件查出来,并处理事件,那么负责处理就绪事件的线程对象为“Poller”,每一个Poller中都包含一个Selector,这样每一个Poller线程就负责轮询自己的Selector事件;
4.然后将处理事件SockectChannel和当前要做的事情(读或写)封装为SocketProcesson对象,并将它放入连接池中,后续步骤则与BIO类似了;
注意:NIO采用双向通道(channel)进行数据传输,而不是单向的流(stream)。在通道上我们可以注册指定的事件,一共有如下四种事件:
1、服务器端接收客户端连接事件OP_ACCEPT
2、客户端连接服务器端事件OP_CONNECT
3、读事件OP_READ
4、写事件OP_WRITE
服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个通道或多个通道上的事情。以服务端为例,如果服务端的selector上注册了读事件时刻客户端给服务端发送了一些数据,BIO这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达则处理这些事件;如果没有感兴趣的事件到达则处理线程会一直阻塞,直到感兴趣的事件到达为止。
利用java的异步请求I/O处理,可以通过少量的线程处理大量的请求
注意:Tomcat 8 以上版本在linux系统中,默认使用的就是NIO模块,不需要额外的修改,Tomcat7 必须修改Connector配置来启动
网友评论