前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty02:NIO如何解决I/O的阻塞问题

Netty02:NIO如何解决I/O的阻塞问题

原创
作者头像
叫我阿柒啊
修改2024-06-25 09:21:44
1910
修改2024-06-25 09:21:44
举报

前言

在上篇文章写I/O的时候,从最基础的文件读取和socket讲述了I/O存在的线程阻塞问题。

可能纯理论的东西,对于很多人(包括我)来说,还是挺难理解的,所以这篇文章就从代码入手,还原I/O和NIO下,如何实现socket的的通信。本篇文章从I/O和NIO之间的传输模式、线程分配以及数据容器方面入手,同是围绕着下面的数据交互图,来对比两者的区别于联系。

I/O数据交互:

NIO数据交互:

Socket

先使用传统的Socket方式,实现一个客户端和服务端,且看两者的通信过程。

服务端和客户端

实现一个服务端,监听7777端口,等待客户端的连接,然后发起会话。

代码语言:java
复制
ServerSocket serverSocket = new ServerSocket(7777);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);

String message;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

while ((message = in.readLine()) != null) {
    System.out.println("客户端:" + message);
    System.out.print("请输入回复:");
    String response = br.readLine();
    out.println(response);
}
serverSocket.close();

因为是对话,所以要使用while来读取客户端发送的每行数据,然后再从服务端键盘输入回复客户端。接着我们来实现一个客户端,与服务端进行数据交互。

代码语言:java
复制
Socket socket = new Socket("localhost", 7777);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String message;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
    System.out.print("客户端:");
    message = br.readLine();
    out.println(message);

    String response = in.readLine();
    System.out.println("服务端:" + response);
}

先启动服务端,启动客户端。我现在客户端1输入hello。

服务端收到消息之后回复hi,然后就开始进行交互。

这时候,又启动了客户端2连接服务端,然后礼貌性的和服务端打了个招呼。

结果再看服务端,还在等待客户端1的消息呢,根本没空去处理客户端2的消息。

从这里问题就浮现出来了。在传统的socket中,想要实现对话,就得使用while循环等待消息,这样,服务端的一个线程,只能处理一个客户端的连接

I/O弊端

在当前情况下,要想处理多个客户端的消息,就得使用多线程或者线程池,将处理客户端的消息的逻辑封装在Runnable的run()中。但是,这种情况下,也是1个线程处理一个客户端。

假如你的服务器主机CPU有40个core,不考虑超线程的情况,最多也只能同时处理40个线程,也就是40个客户端。 在上面的那种情况,当第41个客户端连接的时候,就会陷入等待。除非在服务端增加关闭客户端连接的逻辑。

所以,socket的IO总结就是:连接与线程绑定,一个连接会独占一个线程、一个cpu

而且,在I/O中,不论是文件读写还是socket,在链路上数据都是以二进制byte存在的,数据容器使用的都是字节数组byte[]

NIO

从上面的分析中,我们发现传统I/O最大的弊端就是:同步阻塞,独占cpu。所以为了解决这个弊端,NIO出现了。

IO重新定义了socket的概念,NIO刚开始有很多人翻译成Non-blocked I/O,即非阻塞的I/O,从功能上理解是没错,但是翻译成New I/O比较贴切,一个全新的I/O。

1. 缓冲区(Buffer)

数据是以byte的形式传输的。在I/O中,使用byte[]数组来存放读取的byte,而NIO则使用Buffer缓冲区来作为数据容器,而且Buffer是一个对象,意味着提供了很多方法可以处理这个数据载体。

在Buffer中,提供了对数据的结构化访问、清空、重置以及维护读写位置等信息

2. 通道(Channel)

在I/O中,是基于Stream(流)来读写数据。读数据需要InputStream,写数据需要OutputStream,都是单向传输数据。而在NIO中,基于Channel(通道)来读写数据,与流不同之处是Channel是双向的,也就意味着我在这个channel中既可以读,也可以写。

3. 多路复用器(Selector)

NIO引入了Selector,它是一种高效的多路复用器,一个线程可以管理多个通道,而非I/O中一个线程只能读取一个流的数据,这样线程就不会在交互空闲时被占用。

NIO重构服务端

按照上面的NIO的概念,使用NIO来重构上面服务端的代码:

代码语言:java
复制
// 创建 Selector
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(7777));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

// 等待客户端连接
while (true) {
    selector.select();
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();

        if (key.isAcceptable()) {
            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
            SocketChannel client = channel.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
            System.out.println("客户端连接:" + client.getRemoteAddress());
        } else if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int bytesRead = client.read(buffer);

            if (bytesRead > 0) {
                buffer.flip();
                byte[] data = new byte[bytesRead];
                buffer.get(data);
                System.out.println("客户端:" + new String(data));

                // 回复客户端消息
                System.out.print("请输入回复:");
                String response = br.readLine();
                ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
                client.write(responseBuffer);
            }
        }

        keyIterator.remove();
    }
}

通道可以以非阻塞模式进行读写操作,这意味着如果通道中,对端没有发送数据,无法立即进行读取或写入操作,读取或者写入操作将立即返回,而不是阻塞等待。可以通过configureBlocking(false)来设置非阻塞模式。

这时候,在服务端同样是一个线程在读取客户端的数据,当我启动多个客户端发送数据,服务端将所有的客户端发送的数据都进行了处理,如图所示:

这也就意味着,一个线程是可以读取多个连接中的数据。假如我刚结束读取channel A的数据,刚开始读取channel B的数据,这时候A再来数据,只能下一次循环再处理了。从代码中可以看出:NIO服务端将所有客户端连接的处理,都交给了Selector,对比在while循环逻辑,I/O中只是对一个连接进行数据等待,而NIO是对Selector中所有的连接进行处理。

同时,如图所示,如上面缓冲区所讲,在链路(TCP)上,数据以byte二进制的形式进行传输,但是在NIO接收和发送的内部流程中,使用Buffer来进行存放。我调用NIO的write()写入Buffer,NIO自动会将Buffer中的数据,转换成链路上的byte进行传输,这个我们无需担心。

结语

温故而知新,学有所获。这也是在五六年后再次学习NIO,写完这篇博客也算是对以前学习的零散NIO知识的一个整理。

同时,学习NIO也是为了引出为什么要使用Netty,从I/O到NIO再到Netty,而不是从I/O直接到Netty实现一个大的跨越,让使用Netty的人只知道我要用Netty,而非为什么要用Netty。NIO既出,下一篇写的就是为什么要用Netty。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Socket
    • 服务端和客户端
      • I/O弊端
      • NIO
        • 1. 缓冲区(Buffer)
          • 2. 通道(Channel)
            • 3. 多路复用器(Selector)
            • NIO重构服务端
            • 结语
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档