本文共 5543 字,大约阅读时间需要 18 分钟。
Java NIO包中提供了channel、buffer、selector等多个组件,通过他们提供的API可以实现多路复用的I/O模型,本文通过实现一个简单的使用多路复用的服务端程序,来演示JavaAPI中提供的方法与底层epoll函数实现的具体关系。
服务端代码,标准的demo案例,程序运行后服务端启动并绑定9090端口,等待客户端连接,读取到客户端消息后再直接把消息后回复给客户端。
import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.util.Iterator;import java.util.Set;public class SocketMultiplexIO { private static Selector selector; public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(9090)); serverSocketChannel.configureBlocking(false); selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务端启动了。。。"); while (true) { Setkeys = selector.keys(); System.out.println("当前epoll注册的事件:" + keys.size()); while (selector.select() > 0) { Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); iterator.remove(); if (selectionKey.isAcceptable()) { System.out.println("有一个客户端连接了。。。"); acceptHandler(selectionKey); } else if (selectionKey.isReadable()) { selectionKey.cancel(); System.out.println("cancel函数,取消了accept事件"); readHandler(selectionKey); } } } } } private static void readHandler(SelectionKey key) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); buffer.clear(); int read; try { while (true) { read = client.read(buffer); if (read > 0) { buffer.flip(); while (buffer.hasRemaining()) { client.write(buffer); } buffer.clear(); } else if (read == 0) { break; } else { client.close(); break; } } } catch (IOException e) { e.printStackTrace(); } } private static void acceptHandler(SelectionKey selectionKey) throws IOException { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel(); SocketChannel accept = serverSocketChannel.accept(); accept.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); accept.register(selector, SelectionKey.OP_READ, byteBuffer); }}
1、启动服务端,当前注册了一个事件:accept
[root@node05 test_io]# strace -ff -o log java SocketMultiplexIO服务端启动了。。。当前epoll注册的事件:1
很明显代码目前阻塞在select这个方法调用上,等待accpet事件,也就是客户端的连接。
while (selector.select() > 0)
追踪系统调用分析。
首先完成socket创建,返回一个fd=4,然后 bind(4, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INET6, “::”, &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0
,绑定fd4到9090端口,并开启监听。继续往下看。
调用epoll_create,返回一个fd=7,这个实际上就是创建了一个evetpoll对象,通过epoll_ctl,把直接创建的fd=4(LISTEN),添加(EPOLL_CTL_ADD)到evetpoll对象中(红黑树结构维护),接着调用epoll_wait阻塞了,等待事件到达。
查看进程中的FD,进行验证,fd4:listen事件,fd7:evetpoll对象。
2、接下来,启动一个客户端
直接通过nc连接。
服务端打印
[root@node05 test_io]# strace -ff -o log java SocketMultiplexIO服务端启动了。。。当前epoll注册的事件:1有一个客户端连接了。。。
继续看调用分析
当有客户端连接后,直接系统阻塞在2787行的epoll_wait调用,返回了结果1,服务端accept客户端的连接请求(客户端ip,192.168.70.113),并返回了一个fd=8,并把fd8再通过epoll_ctl函数添加到了evetpoll对象中,和前一步添加accpet事件一样,然后又继续阻塞此时等待的就是两个事件了(accept事件和read事件)。
查看进程中的fd,多了一个fd8,服务端与客户端连接建立成功。
分析一下到目前为止的操作
服务端Java进程中,现在有fd4(LISTEN监听),fd7(eventpoll对象),fd8(客户端与服务端已经建立的连接),其中在eventpoll中(fd7)通过epoll_ctl绑定了fd4的accept事件和fd8的read事件,这两步对应的就是代码中的如下两行:
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);accept.register(selector, SelectionKey.OP_READ, byteBuffer);
3、客户端向服务端发送数据
这一步注意,我在代码中写了,如果服务端收到了可读事件,首先会调用cancel这个方法,然后把读取出来的数据再通过buffer写回给客户端。
selectionKey.cancel();//看看这个方法底层最终做了什么操作???
客户端发送1234567890,并收到了服务端写回的1234567890。
[root@node04 ~]# nc 192.168.70.114 909012345678901234567890
服务端输出
[root@node05 test_io]# strace -ff -o log java SocketMultiplexIO服务端启动了。。。当前epoll注册的事件:1有一个客户端连接了。。。cancel函数,取消了accept事件
查看系统调用
接着前一步2850行,客户端发送数据后epoll_wait返回,2851行调用了系统打印,(中文字符集编码不一致)。
就是打印了这一行
System.out.println("cancel函数,取消了accept事件");
2863读到了数据,2868写出了数据,接着2870看到了一个epoll_ctl调用,EPOLL_CTL_DEL,是一个删除事件,并发删除的fd为8,这就说明了cancel调用会解除对应事件的绑定,使用时要注意。
所以按道理此时客户端如果再给服务端发送数据,服务端是不会响应的。
事实证明确实如此
[root@node04 ~]# nc 192.168.70.114 9090123456789012345678900987654321
但是此时如果有新的连接到来,服务端还是可以接收的,因为accept事件并没有删除。
再启一个客户端连接并发送数据,服务端响应。
[root@node04 ~]# nc 192.168.70.114 909012345678901234567890
[root@node05 test_io]# strace -ff -o log java SocketMultiplexIO服务端启动了。。。当前epoll注册的事件:1有一个客户端连接了。。。cancel函数,取消了accept事件有一个客户端连接了。。。cancel函数,取消了accept事件
连接事件到达,2871行阻塞返回,创建一个新的fd10,并添加可读事件到fd10上,读到数据后,又把fd10移除。
新的fd10。
经过上面演示的三个步骤,并进行系统调用追踪,现在你应该能够了解到Java API中的一些函数对底层的具体调用,多路复用中都逃不开这些方法的使用,如果对epoll中的epoll_create、epoll_ctl、epoll_wait三个函数不太理解,建议先补习一下epoll的相关知识,这样会更加容易理解这套流程。
简单理解epoll中的三个函数
epoll_create
创建一个eventpoll对象,内部通过红黑树结构维护各个fd上注册的事件,并通过链表维护已经到达的事件。
epoll_ctl
对事件的添加、删除等操作。
epoll_wait
阻塞的方法,直到等待注册的事件到达,到达后直接从链表中获取。
转载地址:http://oolrb.baihongyu.com/