本章本来三部分,
第一部分是对 linux epoll机制的一些简述
第二部分是 java nio 到 hotspot 的一些简述
第三部 是一些简要总结
第一部分:
select/poll机制:
select/poll 是 epoll之前提出的一种多路复用机制,它大概的原理是在一个线程内,将用户层的fd集合拷贝进内核,进行遍历,然后返回 可读,可写的fd集合。
由前一段可以看出select/poll的问题是,他每次遍历都需要将fd从用户态复制到内核态。如果连接数一多,对性能造成很大的影响。
epoll机制:
epoll 是基于事件通知机制 与 多路复用,在内核级别实现的机制。
它的原理是, 当一个客户端向服务发起一个请求是,服务端会创建一个socket 或者说叫 fd,来跟该这个请求做一一对应。
1 首先 epoll 是将 该对应的 fd,放进一颗红黑树里
2 对该fd 注册事件,假设这里是可读事件
3 当这个客户端连接对服务器发送数据时,服务器网卡会发起一个中断,调用该 socket 相应的回调,将其放进 一个 链表里
在linux 上提供了一组 api ,分别是 epoll_create,epoll_ctl,epoll_wait 供我们使用。它们对应的作用分表是
1 epoll_create 用于初始化,对应的结构体
2 epoll_ctl 对fd做相应的操作,可以将fd 加入到红黑树,可以给fd注册相应的时间
3 epoll_wait 如果链表上有相应的fd,返回链表上的fd
为什么要用红黑树:
二叉树实现简单,但是查询性能不稳定,常因为动态更新导致性能退化问题。 AVL树是一种高度平衡二叉树,查找效率非常高,但是因为要维护这种高度平衡性,所以在每次删除和新增都可能需要付昂贵的代价。
而二叉树,是从二三四叉树演变来的,具体可以看看<算法>这本书,所以每次调整时,只需要调整有限的节点,所以在维护平衡的成本上,要比AVL树要低得多。所以,红黑树的插入,删除,查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支持这种工业级应用,我们更倾向于这种性能稳定的平衡二叉树
第二部分:
java级别的NIO:
java 的 nio 的底层使用的是 linux 的 epoll 机制。我们先从一个简单的demo开始。
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 {
public static void main(String[] args) throws Exception {
// new
ServerSocketChannel serverSocketChannel
= ServerSocketChannel.open();
Selector selector = Selector.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
if(selector.select(1000) == 0 ){
System.out.println("-================");
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if(key.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("");
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable()){
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("");
}
keyIterator.remove();
}
}
}
}
java nio 的核心组件有三个,分别是 channel ,buffer,selector。
channel: channel,如果对于网络IO来说,你们可以简单理解为 socket,它封装了socket,对它进行读写的操作 |
buffer: 数据的载体或者容器 |
selector: selector 底层就是封装 epoll_create,epoll_ctl,epoll_wait 这几个 API。 |
其中大致的逻辑是,socket先将自己注册到 selector,然后当可操作的时候,就将该socket返回,然后就可以做相应的操作
现在我们来分析一下 Selector的关键源代码:
其中 native 函数 在 openjdk\jdk\src\solaris\native\sun\nio\ch\EPollArrayWrapper.c 文件中,这里我删掉native 函数部分代码,方便阅读
我们主要分析 Selector.open(),erverSocketChannel.register,selector.select(),这三部分代码,因为分别包含了 epoll 关键的API
1 Selector.open():
Selector.open()-----> openSelector() ----> new EpollArrayWrapper() ---- > this.epollCreate() 其中 epollCreate()是native 函数 |
epollCreate()对应代码
Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)
{
int epfd = epoll_create(256);
return epfd;
}
2 erverSocketChannel.register:
将对应的事件加入到集合,然后在select的时候批量 epoll_ctl |
epoll_ctl()对应代码
Java_sun_nio_ch_EPollArrayWrapper_epollCtl(JNIEnv *env, jobject this, jint epfd,
jint opcode, jint fd, jint events)
{
struct epoll_event event;
int res;
event.events = events;
event.data.fd = fd;
RESTARTABLE(epoll_ctl(epfd, (int)opcode, (int)fd, &event), res);
}
3 selector.select:
最终会调用 EPollSelectorImpl.doSelect() ---- > 再调用 EPollArrayWrapper.poll()--->epollCtl()和epollWait() 其中 epollWait() 是native 函数 |
epollWait()对应代码
Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this,
jlong address, jint numfds,
jlong timeout, jint epfd)
{
struct epoll_event *events = jlong_to_ptr(address);
int res;
if (timeout <= 0) { /* Indefinite or no wait */
RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res);
} else { /* Bounded wait; bounded restarts */
res = iepoll(epfd, events, numfds, timeout);
}
return res;
}
第三部分:
由此,我们可以总结出,上面 java nio server的demo,可以简化为 以下伪代码:
fun(){
int serverFd = new Socket();
int epfd = epoll_create;
// 监控主fd
epoll_ctl(epfd,EPOLL_CTL_ADD,serverFd,是否有新连接);
while(1){
// 获取就绪事件
int count = epoll_wait(epfd)
for(i=0;i<count;i++){
if(是新连接事件){
// 添加客户端fd
epoll_ctl(epfd,EPOLL_CTL_ADD,clientFd,是否可读可写);
}
if(是可读事件){
}
}
}
}
先使用 epoll_create 初始化内核结构,然后使用 epoll_ctl 监听 服务器fd 的 是否有连接事件,
如果有可连接事件,则继续将客户端fd继续监听起来,给对应的客户端fd注册 可读,可写事件
其实,可以看出,java nio,其实是比较底层的,如果要在实际项目中使用,还需要考虑很多东西,比如线程模型,tcp 拆包,粘包等等。所以出现了很多框架来解决这方面的问题,如netty,jetty等等。
参考:
极客时间 算法与数据结构
<算法>
尚硅谷netty视频
还有其他一些博客