一、什么是BIO
传统的BIO模型使用是java.net包中API实现Socket通信,而传统的BIO通信中阻塞共发现在两个 地方。
服务端ServerSocker::accept
客户端Socket数据读写
这种IO的弊端即是无法处理并发的客户端请求,因此可以通过为每个客户端单独分配一个线程,则客户的端的Socket数据读写不再阻塞新的连接请求,可以满足并发的客户端请求。
同样的在高并发,大量客户端连接造成大量线程,容易产生线程OOM,同时也有大量的线程上下文切换影响性能。
二、什么是NIO
- 在JAVA中NIO是指new IO,是JDK为实现非阻塞IO实现的一套新API
- 在linux中NIO是指非阻塞的IO,主要与poll和epoll内核调用有关
JAVA的NIO一种重要的方法为configureBlocking,示例代码如下:
package com.zte.sunquan.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class SocketServerNIO {
public static void main(String[] args) throws Exception {
List<SocketChannel> channels = new ArrayList<>();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//非阻塞
while (true) {
Thread.sleep(1000);
//不阻塞,但需要每次都问一下内核
SocketChannel clientSocket = ssc.accept();
if (clientSocket == null) {
System.out.println("No client....");
} else {
//将clientSocket保留
clientSocket.configureBlocking(false);
System.out.println("client connected in:" + clientSocket.socket().getPort());
channels.add(clientSocket);
}
ByteBuffer buffer = ByteBuffer.allocate(4096);
//不阻塞,但这里每次也需要问一下内核,即使有些客户端没有事件
for (SocketChannel channel : channels) {
int num = channel.read(buffer);
if (num > 0) {
buffer.flip();
byte[] content=new byte[buffer.limit()];
buffer.get(content);
String s = new String(content);
System.out.println(channel.socket().getPort()+" Read client msg:"+s);
buffer.clear();
}
}
}
}
}
如上代码中使用单个线程负责了IO读写和请求连接响应建立,当然可以优雅一点的方法,即将连接建立与IO读写分线程处理,让IO的读写不影响高并发下连接请求与建立。但上述代码仍有明显的弊端。考虑:
for (SocketChannel channel : channels) {
int num = channel.read(buffer);
在上述代码中,每次循环都要与所有建立的客户端进行一次read操作,涉及用户究竟与内核空间的切换,考虑在连接数特别多背景下,一次可能只会有几个连接有IO事件,如上的实现会造成大量的性能浪费。
那有没有一种可能,让内核主动告知我们哪些连接有IO事件,这样应用精确地去指定的连接上进行IO事件的处理,而不是傻傻地每个连接read一遍?
三、多路复用器
多路复用器使用,可以解决第二节最后的问题,通过selector.select,内核只会将有事件的socket返回,避免了应用循环遍历尝试。
下面示例代码描述了使用JAVA中NIO的API实现的服务端代码
package com.zte.sdn.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* 多路复用器示例代码
**/
public class NioServer {
private Selector startServer() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//打开一个多路复用器
//poll系统调用:
//epoll系统调用:
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Start server at port:9090");
return selector;
}
public static void main(String[] args) throws IOException {
NioServer nioServer = new NioServer();
Selector selector = nioServer.startServer();
while (true) {
System.out.println("ask");
//使用select向内核询问是否有事件(由于一开始只注册了ServerSocket的OP_ACCEPT)
//所以第一次只判断是否有连接事件
//后面由于注册客户端OP_READ,从面判断是否有可读事件
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
nioServer.handler(selectionKey, selector);
}
}
}
}
private void handler(SelectionKey selectionKey, Selector selector) throws IOException {
if (selectionKey.isAcceptable()) {
//一个连接事件
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
//再注册进去
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
System.out.println("receive connect:" + client.getRemoteAddress());
} else if (selectionKey.isReadable()) {
//可读事件
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//讲到内容写回客户端
client.write(buffer);
}
System.out.println("receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}
}
}
在上述示例中,一个线程完成了服务端接口客户端连接以及IO读写动作。思考上面编程思路的弊端是什么?
考虑到一个IO的读写如何非常耗时,必然会影响客户端建立连接并发性能,以及大量IO读写的性能。
自然地针对客户端连接与IO读写可以分不同selector单独处理,各司其职,所以改进的实现如下:
else if (selectionKey.isReadable()) {
executorService.submit(()->{
//可读事件
try {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//讲到内容写回客户端
client.write(buffer);
}
System.out.println("receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
});
}
如上所示,实现了IO的处理异步化。但上述实现虽然缓解了大量长时间IO带来的性能问题,但不能从根本上解决,那有没有办法,将客户端连接事件与IO事件完全分离开?当然如果IO读取的数据业务处理比较耗时,则可以另起线程再进行异步处理。
四、多Selector版本
代码:
package com.zte.sdn.nio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class IOHandler extends Thread {
private Selector selector;
public IOHandler(Selector selector, String name) {
this.selector = selector;
this.setName(name);
}
@Override
public void run() {
while (true) {
try {
handler();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handler() throws IOException {
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
try {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//讲到内容写回客户端
client.write(buffer);
}
System.out.println(Thread.currentThread().getName() + " receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
} catch (Exception e) {
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
break;
}
}
}
}
}
}
}
MultiNioServer
package com.zte.sdn.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多路复用器示例代码
**/
public class MultiNioServer {
private ExecutorService executorService = Executors.newFixedThreadPool(5);
private Selector startServer() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//打开一个多路复用器
//poll系统调用:
//epoll系统调用:
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Start server at port:9090");
return selector;
}
public static void main(String[] args) throws IOException {
MultiNioServer nioServer = new MultiNioServer();
Selector selector = nioServer.startServer();
Selector selector1 = Selector.open();
Selector selector2 = Selector.open();
Selector[] selectors = new Selector[]{selector1, selector2};
new IOHandler(selector1, "A").start();
new IOHandler(selector2, "B").start();
int i = 0;
while (true) {
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
//一个连接事件
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
//循环注册至另外的Selector
client.register(selectors[i++ % 2], SelectionKey.OP_READ, ByteBuffer.allocate(8192));
System.out.println("receive connect:" + client.getRemoteAddress());
}
}
}
}
}
}
五、IO总结
传统的NIO(不使用多路利用器),虽然解决了IO阻塞问题,但需要用户程序遍历地向内核询问所有客户端;
当使用了多路复用器(使用select/poll),每次循环需要将客户端列表传递给内核,由内核遍历将有事件的fd返回,避免用户态与内核态的频繁切换。
但使用select/poll仍然存在弊端,即每次循环都要进行fd列表的传递,如何能够避免每次循环向内核传递fd列表?
思考:如内核能提前申请一块空间存储(红黑树)事件句柄,在有客户端连接上,则记录在该空间,如此客户端程序询问是否事件时,则只需简单问一句,而不需要每次都传递fd列表。
这说的其实epoll内核调用能解决的。
我们老师去教室收作业进行批改的场景为例类比上述实现:
IO类型 | 说明 | 弊端 |
---|---|---|
BIO | 老师来到教室,依次询问每个学生写了作业没有,写了则批改,没写则需等他写好再批改,期间有同学入学毕业则需排队等待 | 两处阻塞 |
NIO | 老师来到教室,依次询问每个学生写了作业没有,写了则批改,没写则直接下一个,期间有同学入学毕业则需排队等待,相对较快 | 依次询问,没写作业的也要问 |
NIO-select/poll | 老师每次来到教室,带着提前准备的名单贴到教室黑板,并告知在名单上同学且完成作业,报给我,后序老师直接拿到报名同学作业,批改即可 | 不同于每个同学依次询问,但还要每次准备名单 |
NIO-epoll | 开班时,提前在教室黑板贴上人员名单,有新同学加入或毕业则相应的增加或删除,老师每次来到教室,再不用准备名单,同学自动举手通告老师作用写好或上厕所相应事件,老师获知后直接处理 | 解决上述所有弊端 |
ps.select的fd有1024的数量约束,poll无此限制
六、Strace分析
七、Netty的线程模型
Netty拥有两个NIO线程池,分别是bossGroup和workerGroup,前者处理新建连接请求,然后将新建立的连接轮询交给workerGroup中的其中一个NioEventLoop来处理,后续该连接上的读写操作都是由同一个NioEventLoop来处理。注意,虽然bossGroup也能指定多个NioEventLoop(一个NioEventLoop对应一个线程),但是默认情况下只会有一个线程,因为一般情况下应用程序只会使用一个对外监听端口。
为何不能使用多线程来监听同一个对外端口么,即多线程epoll_wait到同一个epoll实例上?
这里会引来惊群的问题和epoll设置的是LT模式
现代linux中,多个socker同时监听同一个端口也是可行的,nginx 1.9.1也支持这一行为。linux 3.9以上内核支持SO_REUSEPORT选项,允许多个socker bind/listen在同一端口上。这样,多个进程可以各自申请socker监听同一端口,当连接事件来临时,内核做负载均衡,唤醒监听的其中一个进程来处理,reuseport机制有效的解决了epoll惊群问题
单线程模型
Reactor 单线程模型,是指所有的 I/O 操作都在同一个 NIO 线程上面完成的,此时NIO线程职责包括:接收新建连接请求、读写操作等。
多线程模型
Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理连接读写操作,一个NIO线程处理Accept。一个NIO线程可以处理多个连接事件,一个连接的事件只能属于一个NIO线程
主从模型
主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel注 册 到 I/O 线 程 池(sub reactor 线 程 池)的某个I/O线程上, 由它负责SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。