什么是多路复用器
聊多路复用器之前呢,先回归昨天的NIO,NIO的出现解决了BIO阻塞线程、一连接一线线程问题。但是它有缺点吗,答案是肯定的。
NIO的缺点
我们把问题放大,如果有一万个连接但是只有一个连接是有数据的,但是对于我们的NIO来说,他每次都会遍历所有连接并且去调用内核,我们都是到用户态切换内核态的消耗还是很大的,这里就会涉及到操作系统的知识,当用户态切换到内核态时会有一个80中断,需要经历的过程大致为,保存用户线程的现场信息,根据中断描述符从中断向量表中查找指令,调用指令,恢复现场,如果我们只有一个连接有数据但是会进行9999次无效的80中断是不是会造成不必要的资源消耗。
如何解决这一个问题
第一种多路复用器selector横空出世,他从根本问题出发,NIO不是80中断消耗大吗,那直接去内核去遍历所有的连接不就好了,这也是selector的做法,大致流程是这样的
还有一个和selector 逻辑想用的多路复用器poll,他和selector不同之处在于selector是有连接限制的。
下面是重头戏epoll
在追踪内核之前我们先把epoll的大致流程讲一下,然后我们去追踪内核去证明。
图片优点笼统,笔者下面一一解释
首先要说一点,目前我说认知的所有IO模型都需要socket、bind、listen这几个指令。
首先还是需要生成一个socket然后通过bind命令将socket与目标端口绑定然后去监听。
接下来就是epoll特有的指令了
第一个epoll_create(size)这个指令会在内存开辟两个空间,一个空间用来存放需要监听的文件描述符以及需要监听对应文件描述符的事件,第二个空间是用来当有事件到来时,将对应的文件描述符以及数据放入这个缓冲区中,当程序调用accept、read时就回去缓冲区中找有没有对应的事件。
第二个指令epoll_ctl(epfds,op,fds,event),这个指令是将对应的文件描述符以及需要监听的事件交由epoll去管理,下面解释一下参数,第一个参数epoll的文件描述符,第二个参数表示需要啥操作,第三个文件描述符表示需要监听的对象,第四个参数为需要监听的事件。
第三个指令epoll_wait(epfds,events,maxevent,timeout),这个指令是来监听epoll中所有需要监听的事件有没有返回,如果有返回就将对应的fds与数据放入缓冲区中。参数对应的意思为,epoll的文件描述符、需要监听的事件、最大返回事件个数、超时时间。
笔者认为有了这些基础我们可以更好的去理解epoll的原理。参数的意思可以使用man int 对应的命令,这个命令来查看。
重点注意
epoll有一个非常了不起的地方就是他对于收到的事件数据放入缓冲区的操作与我们程序accept/read能达到异步,这点是epoll非常厉害的地方。
下面我们来一一证明
笔者写了一个简陋的java代码如下
server端
public class NIOServer {
private static ByteBuffer bb = ByteBuffer.allocate(1024);
private static ServerSocketChannel ssc = null;
private static Selector selector = null;
private static int port = 8888;
public static void main(String[] args) throws IOException {
ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(port));
selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动。。");
while (true){
while (selector.select(500)>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()){
SelectionKey selectionKey = it.next();
it.remove();
if(selectionKey.isAcceptable()){
handlekey(selectionKey);
}else if(selectionKey.isReadable()){
handleRead(selectionKey);
}
}
}
}
}
public static void handlekey(SelectionKey selectionKey) throws IOException {
ServerSocketChannel cssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = cssc.accept();
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
System.out.println("新的客户端加入"+client.getRemoteAddress());
}
public static void handleRead(SelectionKey selectionKey) throws IOException {
SocketChannel cssc = (SocketChannel) selectionKey.channel();
int read = cssc.read(bb);
if(read>0){
System.out.println(new String(bb.array(),0,read));
}
}
}
client端
public class NIOClient {
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open();){
socketChannel.connect(new InetSocketAddress("localhost", 8888));
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
String str = "你好,服务器我是客户端";
sendBuffer.put(str.getBytes());
sendBuffer.flip();
socketChannel.write(sendBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
JavaNIO默认的是使用epoll,如果发现使用的不是epoll我们可以加虚拟机参数。如果没问题参数可以不加
我们通过strace命令来对追踪我们的内核调用。打开我们的out文件
我们先来关注这两条指令,第一个socket是对应生成socket的,并返回了一个文件描述符5,接下来看第二个指令,注意参数,第一个参数就是对应的我们socket的文件描述符,第三个参数很重要NONBLOCK表示设置我们的socket为非阻塞的。我们继续向下看。
这两个指令应该很熟悉,就是bind端口,然后去监听。就不过多解释了。
下面就是我们要重点关注的epoll的指令了。
这个指令上面已经解释过了就是用来创建我们的epoll的并分配资源。我们可以使用man命令来证实一下,如下图
这个就是epoll_create命令的解释,感兴趣可以自己去看一下。我们继续看下一条指令。
下一个指令时epoll_ctl;这些参数大家应该可以看个大概。第一个就是我们创建epoll的时候返回的文件描述符8,第二个字面意思就是添加吗,就是往epoll中去添加socket的文件描述符5,最后的参数是需要监听的事件,以及对应的文件描述符。
下一个指令就是epoll_wait命令,第一个参数就是我们epoll的文件描述符8。而且有没有注意到超时时间,这个是不是和我们java程序中设置的超时时间是一样的,这点我们需要注意,我们先使用man 命令来看一下,指令的介绍。
我们只关注返回值,有兴趣的读者可以自己翻阅一下,返回值这里说,成功时,epoll_wait()返回为请求的I/O准备的文件描述符的数量,如果在请求的超时毫秒内没有文件描述符准备就绪,则返回零。当发生错误时,epoll_wait()返回-1,我们这里的返回值为0表示当前还没有事件。
下面我们启动客户端jar包
我们的日志文件中出现了变化,图中画红线的指令,就是表示epoll_wait收到了事件,然后发现是一个连接事件,然后就去创建连接并返回文件描述符9,我们继续向下看。
我们看到又调用了epoll_ctl将文件描述符9就是对应的我们的连接交由epoll去管理,然后再调用epoll_wait又获取了事件,这个事件就是我们客户端给服务器端发的数据。我们继续向下看。
发现后面的wait总是能返回事件,而且都一样,这说明我们程序中有很大的bug,我打断点调试后发现每次都会进到可读的逻辑中,我自己在处理读数据的时候最后 selectionKey.cancel();我不知道合理不合理,但是确实解决了问题。