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端的代码就更是简单了,

java 多路io复用 java实现多路复用_java 多路io复用

做的事情很简单,就是跟服务端建立连接,并且发送数据

run epoll

我们还是一样,先来看下代码的运行情况:

通过命令监控系统调用:

java 多路io复用 java实现多路复用_复用器_02


epoll_create,epoll_ctl这些在前面已经讲解过了,这边就不再做过多的赘述。对应到代码中,便是initServer方法。我们继续查看追中文件:

java 多路io复用 java实现多路复用_java 多路io复用_03


我们可以看到在不断的重复调用系统的epoll_wait方法,相信大家一定可以找到我们的实际代码块:

while (selector.select(500) > 0)

当我们代码在不断调用select方法时,实际就是在不停的发起epoll_wait的系统调用。

现在service端的代码已经在这边不停轮询等待,我们现在接入一个client端会是什么样的情况呢?

下面我们启动一个client连接:

java 多路io复用 java实现多路复用_复用器_04


我们可以看到当我们的一个client与service三次握手之后建立连接,service端的epoll_wait方法此时获取到的value已经不再是0了,而是1,后续的accept方法指定了socket的四元组,并且完成FD的分配19现在我们来看下这段代码结果:

java 多路io复用 java实现多路复用_java 多路io复用_05


代码片段:

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上去,并且最终会打印出一行字,我们来看下追中文件:

java 多路io复用 java实现多路复用_java 多路io复用_06


通过这个,我们可以很明确的看到有调用系统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发送一条数据测试下结果如何:

java 多路io复用 java实现多路复用_多路复用_07

和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()方法

其他方法也是类似情况。