探究Redis网络模型究竟有多强大(下)

电子说

1.3w人已加入

描述

4. 非阻塞I/O(NonBlocking I/O)

上文花了太多的笔墨描述BIO,接下来的非阻塞IO我们只抓主要矛盾,其余参考BIO即可。

如果你看过其他介绍非阻塞IO的文章,下面这个图片你多少会有点眼熟。

Redis

NIO模型

非阻塞IO指的是进程发起系统调用之后,内核不会将进程投入睡眠,而是会立即返回一个结果,这个结果可能恰好是我们需要的数据,又或者是某些错误。

你可能会想,这种非阻塞带来的轮询有什么用呢?大多数都是空轮询,白白浪费CPU而已,还不如让进程休眠来的合适。

4.1 Java的非阻塞实现

这个问题暂且搁置一下,我们先看Java在语法层面是如何提供非阻塞功能的,细节慢慢聊。

public class NoBlockingServer {

    public static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {

        try {
            // 相当于serverSocket
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 将监听socket设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(8099));
            while (true) {
                // 这里将不再阻塞
                SocketChannel socketChannel = serverSocketChannel.accept();

                if (socketChannel != null) {
                    // 将连接socket设置为非阻塞
                    socketChannel.configureBlocking(false);
                    channelList.add(socketChannel);
                } else {
                    System.out.println("没有客户端连接!!!");
                }

                for (SocketChannel client : channelList) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // read也不阻塞
                    int num = client.read(byteBuffer);
                    if (num > 0) {
                        System.out.println("收到客户端【" + client.socket().getPort() + "】数据:" + new String(byteBuffer.array()));
                    } else {
                        System.out.println("等待客户端【" + client.socket().getPort() + "】写数据");
                    }
                }

                // 加个睡眠是为了避免strace产生大量日志,否则不好追踪
                Thread.sleep(1000);

            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Java提供了新的API,ServerSocketChannel以及SocketChannel,相当于BIO中的ServerSocketSocket。此外,通过下面两行的配置,将监听socket和连接socket设置为非阻塞。

// 将监听socket设置为非阻塞
serverSocketChannel.configureBlocking(false);

// 将连接socket设置为非阻塞
socketChannel.configureBlocking(false);

我们上文强调过, Java自身并没有将socket设置为非阻塞的本事,一定是在某个时间点上,操作系统内核提供了这个功能,才使得Java设计出了新的API来提供非阻塞功能

之所以需要上面两行代码的显式设置,也恰好说明了内核是默认将socket设置为阻塞状态的,需要非阻塞,就得额外调用其他系统调用。我们通过man命令查看一下socket()这个方法(截图的中间省略了一部分内容):

man 2 socket

Redis

image-20221225144028751

我们可以看到socket()函数提供了SOCK_NONBLOCK这个类型,可以通过fcntl()这个方法将socket从默认的阻塞修改为非阻塞,不管是对监听socket还是连接socket都是一样的。

4.2 Java的非阻塞解释

现在解释上面提到的问题:这种非阻塞带来的轮询有什么用?观察一下上面的代码就可以发现,我们全程只使用了1个main线程就解决了所有客户端的连接以及所有客户端的读写操作。

serverSocketChannel.accept();会立即返回调用结果。

返回的结果如果是一个SocketChannel对象(系统调用底层就是个socket描述符),说明有客户端连接,这个SocketChannel就表示了这个连接;然后利用socketChannel.configureBlocking(false);将这个连接socket设置为非阻塞。这个设置非常重要,设置之后对连接socket所有的读写操作都变成了非阻塞,因此接下来的client.read(byteBuffer);并不会阻塞while循环,导致新的客户端无法连接。再之后将该连接socket加入到channelList队列中。

如果返回的结果为空(底层系统调用返回了错误),就说明现在还没有新的客户端要连接监听socket,因此程序继续向下执行,遍历channelList队列中的所有连接socket,对连接socket进行读操作。而读操作也是非阻塞的,会理解返回一个整数,表示读到的字节数,如果>0,则继续进行下一步的逻辑处理;否则继续遍历下一个连接socket。

下面给出一张accept()返回一个连接socket情况下的动图,希望对大家理解整个流程有帮助。

4.3 掀开非阻塞IO的底裤

我将上面的程序在CentOS下再次用strace程序追踪一下,具体步骤不再赘述,下面是out日志文件的内容(我忽略了绝大多数没用的)。

Redis

非阻塞IO的系统调用分析

4.4 非阻塞IO总结

Redis

NIO模型

再放一遍这个图,有一个细节需要大家注意,系统调用向内核要数据时,内核的动作分成两步:

  1. 等待数据(从网卡缓冲区拷贝到内核缓冲区)
  2. 拷贝数据(数据从内核缓冲区拷贝到用户空间)

只有在第1步时,系统调用是非阻塞的,第2步进程依然需要等待这个拷贝过程,然后才能返回,这一步是阻塞的。

非阻塞IO模型仅用一个线程就能处理所有操作,对比BIO的一个客户端需要一个线程而言进步还是巨大的。但是他的致命问题在于会不停地进行系统调用,不停的进行accept(),不停地对连接socket进行read()操作,即使大部分时间都是白忙活。要知道,系统调用涉及到用户空间和内核空间的多次转换,会严重影响整体性能。

所以,一个自然而言的想法就是,能不能别让进程瞎轮询。

比如有人告诉进程监听socket是不是被连接了,有的话进程再执行accept();比如有人告诉进程哪些连接socket有数据从客户端发送过来了,然后进程只对有数据的连接socket进行read()

这个方案就是 I/O多路复用

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分