前面我们介绍了BIO,NIO还有AIO,这一篇我们来简单看一下Nio的底层是怎么实现的.要是不想看这一篇可以直接看结论:Nio底层实际调用的epoll的三个函数,分别是epoll_create(创建epoll对象),epoll_ctl(把channel关联到epoll对象),epoll_wait(等待事件就绪),下一篇我们将介绍select,poll和epoll的区别.
1.前置条件:看NIO的源码,必须下载openJdk源码,因为windos上面的实现和linux上面的实现不一样,都是调用操作系统内核方法来实现的,你在idea点进去看到的是windos上面的实现,如果看linux的实现需要自己去下载源码.
可以参考:OpenJdk源码下载 - 月下小魔王 - 博客园
小技巧:如何看navite(操作系统底层方法)方法的实现,openJdk,例如我当前的类是EpollArrayWrapper,我想看他的epollCreate方法,直接搜索EpollArrayWrapper_epollCreate,就能找到对应的C语言实现,我们看到epoll_create就是调用了linux系统的epoll_create返回一个int.
2.我们再来复习一下NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)
1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
3、NIO 的 Buffer 和 channel 都是既可以读也可以写
NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,内核每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO(只读取有事件发生的channel)。
3.代码演示
不要在windows或者mac点进去看selector的代码,因为不同平台会有不同的实现,linux下面是epoll
public class NioSelectorServer {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 设置ServerSocketChannel为非阻塞
//必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
serverSocket.configureBlocking(false);
// 打开Selector处理Channel,即创建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功");
while (true) {
// 阻塞等待需要处理的事件发生
selector.select();
// 获取selector中注册的全部事件的 SelectionKey 实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍历SelectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
SocketChannel socketChannel = server.accept();
//设置为true会报错
socketChannel.configureBlocking(false);
// 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,则进行读取和打印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
//NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
int len = socketChannel.read(byteBuffer);
// 如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
socketChannel.close();
}
} else if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件");
// NIO事件触发是水平触发
// 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
// 在有数据往外写的时候再注册写事件
key.interestOps(SelectionKey.OP_READ);
//sc.close();
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
}
我们重点关注下面这几个方法:
Selector.open()
创建多路复用器,底层new了一个数组EpollArrayWrapper,并且调用Liunx的内核方法epoll_create,创建了epoll实例.
socketChannel.register(selector, SelectionKey.OP_READ)
将channel注册到多路复用器上,这里实际做的只是把我们的socketChannel放进去一个集合里面,并没有进行真正的绑定,返回selectKey,根据这个key可以获取对应的channel.
selector.select() //阻塞等待需要处理的事件发生,这里底层的方法会真正调用epoll_ctl绑定我们的socketChannel
下图是看源码时候的主流程,可以根据下图自己尝试去跟一下
4.nio底层调用的这些都是linux系统内部方法,我们可以在Linux上用man 命令去查看这些方法的描述(例如 man epoll_create)
1. accept:
accept a connection on a socket 接收一个socket连接
2. epoll_create:
open an epoll file descriptor. 打开一个epoll文件描述符(linux万物皆文件),说白了了就是创建一个linux的epoll对象
int epoll_create(int size);
创建一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用,类似返回一个索引。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。
3. epoll_ctl:
control interface for an epoll descriptor. 操作epoll文件描述符的接口,说白了就是将epoll和我们的目标fd(channel)关联起来.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
使用文件描述符epfd引用的epoll实例,对目标文件描述符fd执行op操作。
参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。
4.epoll_wait:
wait for an I/O event on an epoll file descriptor 在epoll文件描述符上等待I/O事件发生.
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待文件描述符epfd上的事件。
epfd是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最多等到多少个事件就返回,timeout是超时时间。