Java网络编程中使用epoll多路复用底层调用详解

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) {
            Set<SelectionKey> keys = selector.keys();
            System.out.println("当前epoll注册的事件:" + keys.size());
            while (selector.select() > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> 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 9090
1234567890
1234567890

服务端输出

[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 9090
1234567890
1234567890
0987654321



但是此时如果有新的连接到来,服务端还是可以接收的,因为accept事件并没有删除。

再启一个客户端连接并发送数据,服务端响应。

[root@node04 ~]# nc 192.168.70.114 9090
1234567890
1234567890

[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

阻塞的方法,直到等待注册的事件到达,到达后直接从链表中获取。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值