Java实现多路复用
- code
- Service
- Client
- run epoll
- 其他复用器
我们前面介绍过了再OS层面如何实现多路复用,现在我们来看下在Java代码中如何实现多路复用。
code
Service
我们首先介绍了service端的小demo SocketMultiplex.java:
private static ServerSocketChannel server = null;
private static Selector selector = null;
private static int port = 9091;
先定义出几个成员变量,ServerSocketChannel ,Selector ,以及端口号9091
下面我们对变量进行初始化:
public static void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("service start up");
} catch (Exception ex) {
}
}
创建出一个ServerSocketChannel,并且绑定端口号9091,完成selector和channel的注册,指定为非阻塞模式
然后我们再定义出两个方法:
public static void acceptSocket(SelectionKey selectionKey) {
try {
ServerSocketChannel clientChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = clientChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("new client accept: " + client.getRemoteAddress());
} catch (Exception ex) {
}
}
public static void readData(SelectionKey selectionKey) {
try {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
if (client.read(buffer) > 0) {
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("client port: " + client.socket().getPort() +
", data: " + new String(data));
buffer.clear();
}
} catch (Exception ex) {
}
}
acceptSocket定义service端接收到客户端连接后的处理逻辑,readData定义如何从channel中读取出数据。
最后便是我们的主线程方法:
public static void main(String[] args) {
try {
initServer();
while (true) {
while (selector.select(500) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
acceptSocket(selectionKey);
} else if (selectionKey.isReadable()) {
readData(selectionKey);
} else if (selectionKey.isWritable()) {
// dowrite
}
}
}
}
} catch (Exception ex) {
} finally {
try {
selector.close();
server.close();
} catch (Exception ex) {
}
}
}
逻辑很简单,首先初始化成员变量,启动selector和channel,然后进入轮询,当select方法由结果返回之后,要么执行acceptSocket,要么执行readData操作。
Client
client端的代码就更是简单了,
做的事情很简单,就是跟服务端建立连接,并且发送数据
run epoll
我们还是一样,先来看下代码的运行情况:
通过命令监控系统调用:
epoll_create,epoll_ctl这些在前面已经讲解过了,这边就不再做过多的赘述。对应到代码中,便是initServer方法。我们继续查看追中文件:
我们可以看到在不断的重复调用系统的epoll_wait方法,相信大家一定可以找到我们的实际代码块:
while (selector.select(500) > 0)
当我们代码在不断调用select方法时,实际就是在不停的发起epoll_wait的系统调用。
现在service端的代码已经在这边不停轮询等待,我们现在接入一个client端会是什么样的情况呢?
下面我们启动一个client连接:
我们可以看到当我们的一个client与service三次握手之后建立连接,service端的epoll_wait方法此时获取到的value已经不再是0了,而是1,后续的accept方法指定了socket的四元组,并且完成FD的分配19现在我们来看下这段代码结果:
代码片段:
while (true) {
while (selector.select(500) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
acceptSocket(selectionKey);
} else if (selectionKey.isReadable()) {
readData(selectionKey);
} else if (selectionKey.isWritable()) {
// dowrite
}
}
}
}
现在是不是已经很好的将多用复用器的执行过程,和我们代码建立起联系了呢?
前面我们只是建立了连接,所以我们代码isAcceptable()方法,理论应该是true,于是便会进入acceptSocket方法,我们再来看下这个方法:
public static void acceptSocket(SelectionKey selectionKey) {
try {
ServerSocketChannel clientChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = clientChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("new client accept: " + client.getRemoteAddress());
} catch (Exception ex) {
}
}
所以如果代码被执行,我们一定可以看到这个连接被注册到read的channcel上去,并且最终会打印出一行字,我们来看下追中文件:
通过这个,我们可以很明确的看到有调用系统write方法,写入了我们自定义的字符信息,并且还调用了epoll_ctl方法,方法入参,18是当前selector的fd,epoll_ctl_add 代表是在注册,19便是我们上面所提及的accpet方法后对client分配的fd,整个方法调用就是将client的fd往selector的18里面进行注册,对应的代码块便是:
client.register(selector, SelectionKey.OP_READ);
下面我们从client想service发送一条数据测试下结果如何:
和accept结果类似,epoll_wait方法返回值为1,标识fd为19的有了事件,后续通过read方法拿到了client发送的数据,并且最终打印了出来。
isWritable()部分就不再看了,结果也是大同小异。至此我们便是了解了在Java中是如何通过代码实现多路复用的。
其他复用器
等等,现在是不是有一个疑问,我们前面讲的,多路复用器有很多种,为什么这边我们追踪下来只看到了epoll的情况,那其他的复用器在Java中又是如何使用呢?
这个其实是Java对复用器做了一层封装,暴露给我们开发人员的统一是selector,当我们app在运行的时候,需要根据实际的os平台以及os版本,帮我们选择一个最合适的复用器。
比如我们现在使用的是Linux版本中,本身os就不支持epoll,对应于我们开发的代码,我们使用的依然还是selector,只是最终在os层面的实现,不再是epoll,而可能是poll或者select。
我们也可以在jvm启动时设置参数,强制要求复用器是使用epoll还是poll或者select。
但是具体的方法调用对应于实际os系统方法会有所不同:
selector.select(),在epoll情况下,对应的是epoll_wait方法
在select情况下,对应于select()方法,
在poll情况下,对应于poll()方法
其他方法也是类似情况。