Netty 是一个高性能、异步事件驱动的网络应用程序框架,它使用了 NIO 技术来提供高性能的网络通信。在 NIO 中,有三种常用的多路复用器(Multiplexor):Selector、Poll 和 Epoll。这些多路复用器可以同时监听多个网络连接的 IO 事件,从而提高网络通信的效率。

  1. Selector

Selector 是 Java NIO 中的一种多路复用器,它可以监听多个网络连接的 IO 事件,如读、写、连接等。Selector 通过 selectionKey 来注册和监听网络连接的 IO 事件,当某个连接的 IO 事件就绪时,Selector 会通知应用程序进行相应的处理。Selector 的实现是基于操作系统的选择器(select)来实现的,因此它的性能比传统的阻塞 IO 模型要高得多,但是在高并发情况下,它的性能比 Epoll 要低。

  1. Poll

Poll 是 Linux 操作系统中的一种多路复用器,它可以监听多个网络连接的 IO 事件。Poll 的工作原理类似于 Selector,但是它的实现是基于轮询(poll)来实现的。Poll 的性能比 Selector 要高,但是在高并发情况下,它的性能比 Epoll 要低。

  1. Epoll

Epoll 是 Linux 操作系统中的一种高性能多路复用器,它可以监听多个网络连接的 IO 事件。Epoll 的工作原理类似于 Selector 和 Poll,但是它的实现是基于事件驱动(event-driven)来实现的。Epoll 的特点是它可以在高并发情况下维持很高的性能,因为它不需要像 Selector 和 Poll 那样不断地轮询所有的网络连接,而是只需要监听那些真正需要处理的连接。

在 Netty 中,默认使用的是 Epoll 多路复用器,因为它的性能最高。但是,在某些情况下,Netty 也支持使用 Selector 和 Poll 多路复用器。可以通过配置 Netty 的 Transport 来选择使用哪种多路复用器。

Selector、Poll 和 Epoll 都是多路复用器,它们可以提高网络通信的效率。在选择哪种多路复用器时,需要根据实际情况来选择,通常在高并发情况下,使用 Epoll 是最好的选择。

源码解读

Epoll

由于 Netty 的代码比较庞大,因此这里我只能选择一小部分代码进行讲解。以下是 Netty 中 Epoll 多路复用器的部分代码,并添加了中文注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
 * Epoll implementation which supports edge-triggered mode.
 */
public final class EpollEventLoop extends SingleThreadEventLoop {

    // Epoll 实例
    private static final Epoll EPOLL = Epoll.newInstance();

    // Epoll 多路复用器的文件描述符
    private final int epollFd;

    // 用于存储就绪的 IO 事件的数组
    private final EpollEvent[] events;

    public EpollEventLoop() throws IOException {
        // 创建 Epoll 多路复用器
        epollFd = EPOLL.epollCreate();

        // 计算需要监听的 IO 事件的数量
        int size = config().getIoRatio() * 2;
        if (size < 64) {
            size = 64;
        }

        // 创建用于存储就绪的 IO 事件的数组
        events = new EpollEvent[size];
    }

    @Override
    protected void run() {
        int eventCnt;
        try {
            // 不断地轮询 Epoll 多路复用器,等待 IO 事件的发生
            for (;;) {
                // 计算需要等待的时间
                long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                if (curDeadlineNanos == -1L) {
                    curDeadlineNanos = NONE; // nothing on the calendar
                }
                // 调用 epoll_wait 函数,等待 IO 事件的发生
                eventCnt = EPOLL.epollWait(epollFd, events, -1, curDeadlineNanos);

                // 处理 IO 事件
                for (int i = 0; i < eventCnt; i ++) {
                    EpollEvent event = events[i];
                    processReady(event);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        } finally {
            // 关闭 Epoll 多路复用器
            EPOLL.close(epollFd);
        }
    }

    // 处理就绪的 IO 事件
    private void processReady(EpollEvent event) {
        // 获取就绪的 IO 事件的类型
        int events = event.events();

        // 获取就绪的 IO 事件对应的 Channel
        AbstractEpollChannel ch = (AbstractEpollChannel) event.attachment();

        // 处理 IO 事件
        if ((events & EPOLLHUP) != 0) {
            // 处理连接关闭事件
            ch.unsafe().close(ch.unsafe().voidPromise());
        }

        if ((events & EPOLLRDHUP) != 0) {
            // 处理对端关闭事件
            ch.unsafe().close(ch.unsafe().voidPromise());
        }

        if ((events & EPOLLERR) != 0) {
            // 处理 IO 错误事件
            ch.unsafe().close(ch.unsafe().voidPromise());
        }

        if ((events & EPOLLIN) != 0) {
            // 处理可读事件
            ch.read();
        }

        if ((events & EPOLLOUT) != 0) {
            // 处理可写事件
            ch.write();
        }
    }
}

以上代码是 Netty 中 Epoll 多路复用器的部分实现,主要包括以下几个部分:

  1. 创建 Epoll 多路复用器:在构造函数中,通过调用 Epoll.newInstance() 函数创建了一个 Epoll 实例,并通过调用 EPOLL.epollCreate() 函数创建了一个 Epoll 多路复用器的文件描述符。
  2. 创建用于存储就绪的 IO 事件的数组:在构造函数中,根据配置的 IO 比例计算出需要监听的 IO 事件的数量,并创建了一个用于存储就绪的 IO 事件的数组。
  3. 不断地轮询 Epoll 多路复用器:在 run() 函数中,通过调用 EPOLL.epollWait() 函数不断地轮询 Epoll 多路复用器,等待 IO 事件的发生。
  4. 处理 IO 事件:在 run() 函数中,当 IO 事件发生时,通过调用 processReady() 函数处理就绪的 IO 事件。
  5. 关闭 Epoll 多路复用器:在 run() 函数中,通过调用 EPOLL.close() 函数关闭 Epoll 多路复用器。

可以看出,Epoll 多路复用器的工作原理是通过不断地轮询 Epoll 多路复用器来等待 IO 事件的发生,当 IO 事件发生时,通过调用 processReady() 函数处理就绪的 IO 事件。这种工作模式可以有效地提高网络通信的效率,特别是在高并发情况下。

selector

Selector 是 Java NIO 中提供的一种多路复用器,它可以同时监听多个网络连接的 IO 事件,从而提高网络通信的效率。在 Netty 中,Selector 的实现是基于 Java NIO 的 Selector 类来实现的。以下是 Netty 中 Selector 多路复用器的部分代码,并添加了中文注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * Selector implementation which supports edge-triggered mode.
 */
public final class NioEventLoop extends SingleThreadEventLoop {

    // Selector 实例
    private final Selector selector;

    // 用于存储就绪的 IO 事件的数组
    private final SelectedSelectionKeySet selectedKeys;

    public NioEventLoop() throws IOException {
        // 创建 Selector 实例
        selector = SelectorProvider.provider().openSelector();

        // 创建用于存储就绪的 IO 事件的数组
        selectedKeys = new SelectedSelectionKeySet();
    }

    @Override
    protected void run() {
        int selectedKeysCnt;
        try {
            // 不断地轮询 Selector,等待 IO 事件的发生
            for (;;) {
                // 计算需要等待的时间
                long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                if (curDeadlineNanos == -1L) {
                    curDeadlineNanos = NONE; // nothing on the calendar
                }
                // 调用 select() 函数,等待 IO 事件的发生
                selectedKeysCnt = selector.select(curDeadlineNanos);

                // 处理 IO 事件
                processSelectedKeys(selectedKeysCnt);
            }
        } catch (Throwable t) {
            handleLoopException(t);
        } finally {
            // 关闭 Selector
            closeSelector();
        }
    }

    // 处理就绪的 IO 事件
    private void processSelectedKeys(int selectedKeysCnt) {
        Set<SelectionKey> selectedKeys = this.selectedKeys;
        // 遍历就绪的 IO 事件
        for (SelectionKey k: selectedKeys) {
            // 获取就绪的 IO 事件的类型
            int readyOps = k.readyOps();

            // 获取就绪的 IO 事件对应的 Channel
            AbstractNioChannel ch = (AbstractNioChannel) k.attachment();

            // 处理 IO 事件
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // 处理连接事件
                ch.doFinishConnect();
            }

            if ((readyOps & SelectionKey.OP_READ) != 0) {
                // 处理可读事件
                ch.doReadMessages(readBuffer);
            }

            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // 处理可写事件
                ch.doWrite(writeBuffer);
            }

            if ((readyOps & SelectionKey.OP_ACCEPT) != 0) {
                // 处理接收事件
                ch.doAccept();
            }
        }

        // 移除已处理的 IO 事件
        selectedKeys.clear();
    }

    // 关闭 Selector
    private void closeSelector() {
        try {
            selector.close();
        } catch (IOException e) {
            logger.warn("Failed to close a selector.", e);
        }
    }
}

以上代码是 Netty 中 Selector 多路复用器的部分实现,主要包括以下几个部分:

  1. 创建 Selector 实例:在构造函数中,通过调用 SelectorProvider.provider().openSelector() 函数创建了一个 Selector 实例。
  2. 创建用于存储就绪的 IO 事件的数组:在构造函数中,创建了一个用于存储就绪的 IO 事件的数组。
  3. 不断地轮询 Selector:在 run() 函数中,通过调用 selector.select() 函数不断地轮询 Selector,等待 IO 事件的发生。
  4. 处理 IO 事件:在 run() 函数中,当 IO 事件发生时,通过调用 processSelectedKeys() 函数处理就绪的 IO 事件。
  5. 关闭 Selector:在 run() 函数中,通过调用 closeSelector() 函数关闭 Selector。

可以看出,Selector 多路复用器的工作原理是通过不断地轮询 Selector 来等待 IO 事件的发生,当 IO 事件发生时,通过调用 processSelectedKeys() 函数处理就绪的 IO 事件。这种工作模式可以有效地提高网络通信的效率,特别是在高并发情况下。但是,Selector 的性能比 Epoll 要低,因为 Selector 需要不断地轮询所有的网络连接,而 Epoll 只需要监听那些真正需要处理的连接。