Nio多路复用代码
public class NioTest {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9090));
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 创建epoll
Selector selector = Selector.open();
// 绑定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server init");
while (true) {
// 阻塞等待需要处理的事件发生
selector.select();
// 获取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("client started");
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(6);
int len = socketChannel.read(byteBuffer);
if (len > 0) {
System.out.println("Read message:" + new String(byteBuffer.array()));
} else {
System.out.println("client cut connection");
socketChannel.close();
}
}
iterator.remove();
}
}
}
}
源码分析
多路复用是指用一个线程可以处理成千上万的客户端的连接,非阻塞式的监听所有连接,当连接有事件时,将有事件的连接单独放入一个集合中,线程去处理这些有事件的连接
NIO多路复用底层基于epoll事件轮询机制实现
// 创建epoll事件轮询器
Selector selector = Selector.open();
该代码就是实现事件轮询的关键,跟进源码可以发现调用两个方法返回了selector
持续跟进
在Linux版本的jdk中,发现返回的是一个EpollSelectorProvider的实现类,在这个类中,也看到了一开始的openSelector()方法
跟进EpollSelectorImpl()构造函数
可以看到这里创建了一个集合,后续的如
// 绑定监听事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
register函数,就是把连接放入这个集合中
在这个集合构造函数中,终于可以看到创建epoll的函数了,这个epollCreat()方法是一个native方法,代表其为C语言实现的本地方法,跟进这个本地方法
这个epoll_create(256)函数无法跟进了,因为这是Linux操作系统的内核函数,所以,selector底层就是用该函数创建的epoll轮询器
再贴上一张NIO多路复用实现的逻辑图
// 阻塞等待需要处理的事件发生 selector.select();
该代码的底层也是Linux内核函数,一个为epoll_ctl,该函数就是监听集合内连接的事件,有事件的连接就会挪到就绪队列rdlist中。
还有一个函数为epoll_wait,当socket收到数据后,操作系统的中断程序调用回调函数会给epoll实例的事件就绪列表rdlist里添加该socket引用(这块是操作系统实现的),当程序执行到epoll_wait时,如果rdlist已经引用socket,那么epoll_waiti直接返回,如果rdlist为空,阻塞进程
Epoll函数详解
int epoll_create(int size);
创建一个epol实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用。参数size代表可能会容纳size个描述符,但siz不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
使用文件描述符epfd引用的epoll3实例,对目标文件描述符fd执行op操作。
参数epfd表示epolly对应的文件描述符,参数fd表示socket对应的文件描述符。
参数op有以下几个值:
EPOLL CTL ADD:注册新的fd到epfd中,并关联事件event;
EPOLL CTL MOD:修改已经注册的fd的监听事件,
EPOLL CTL DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null;
参数event是一个结构体,events有很多可选值,这里只举例最常见的几个:
EPOLLIN:表示对应的文件描述符是可读的;
EPOLLOUT:表示对应的文件描述符是可写的;
EPOLLERR:表示对应的文件描述符发生了错误;
成功则返回0,失败返回-1
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
等待文件描述符epfd上的事件。
epfd是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最多等到多少个事件就返回,timeout;是超时时间。
总结
IO多路复用底层主要用的Linux内核函数(select,pol,epoll)来实现,windows不支持epoll3实现,windows底层是基于winsock2的select函数实现的,不开源