NIO:客户端发送的连接请求都会注册到多路复用器selector上,多路复用 器轮询到连接有IO请求就进行处理
一、nio非阻塞代码
public static void main(String[] args) throws IOException {
//1、打开ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//2、绑定监听地址InetSocketAddress
ssc.socket().bind(new InetSocketAddress(9000));
//3、创建Selector,并启动线程
Selector selector = Selector.open();
//4、把ServerSocketChannel注册到Selector,监听
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int select = selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
//5 Selector轮询指定的key
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
System.out.println("有客户端连接事件发生了。。");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//6、处理新的客户端接入
SocketChannel sc = ssc.accept();
//7、设置新的socket参数
sc.configureBlocking(false);
//8、将新接入的客户端连接注册到Reactor线程的多路复用器上,读取客户端发送的网络消息
sc.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("有客户端数据可读事件发生了。。");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//9、异步读取客户端请求消息到缓冲区
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
//10 将消息异步发送给客户端
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件");
key.interestOps(SelectionKey.OP_READ);
}
}
打开cmd窗口,输入以下命令,实现客户端和服务端交互:
telnet localhost 9000
ctrl + ]
send 123

二、nio服务端代码时序图
上面服务端代码启动时序图:

三、核心代码底层调用
1 Selector.open() //创建多路复用器 (底层调用的epoll_create)
2 socketChannel.register() //将channel注册到多路复用器上 (底层调用的 epoll_ctl)
3 selector.select() //阻塞等待需要处理的事件发生 (底层调用的epoll_wait)
几个核心函数
int epoll_create(int size);
1、创建一个epoll实例,并返回一个非负数作为文件描述符,size代表表可能会容纳size个描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
2、使用文件描述符epfd引用的epoll实例,对目标文件描述符fd执行op操作。
epfd: epoll对应的文件描述符 fd: socket对应的文件描述符 参数op有以下几个值: EPOLL_CTL_ADD:注册新的fd到epfd中,并关联事件event; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null; events有很多可选值,这里只举例最常见的几个: EPOLLIN :表示对应的文件描述符是可读的; EPOLLOUT:表示对应的文件描述符是可写的; EPOLLERR:表示对应的文件描述符发生了错误;
1 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
3、等待文件描述符epfd上的事件。
epfd: Epoll对应的文件描述符,
events: 调用者所有可用事件的集合,
maxevents: 最多等到多少个事件就返回, timeout: 超时时间。
总结:nio就是通过操作系统内核函数创建socket,获取文件描述符,再创建一个Selector ,获取Epoll文件描述符,将socket文件描述符绑定到Selector的文件描述符上,进行事件的异步通知;就实现了使用一条线程,并且不需要太多的无效的遍历 ,将事件处理交给操作系统内核,事件通知无需遍历,大大提高了效率。