首页 > 其他分享 >【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式

【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式

时间:2023-06-06 16:31:45浏览次数:43  
标签:Netty java 0.1 阻塞 模式 channels 线程 SocketChannel

前言

本篇博文是《从0到1学习 Netty》系列的第二篇博文,主要内容是通过 NIO 来理解阻塞模式与非阻塞模式,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;


介绍

阻塞模式

在 Java NIO 中,阻塞模式是一种传统的 I/O 处理方式,当我们试图从通道进行读取或向通道写入数据时,这种模式会使线程阻塞直到操作完成。具体来说,在阻塞模式下,当没有可用的数据时,读操作将被阻塞,直到有数据可读;同样地,当通道已满时,写操作将被阻塞,直到有空间可用于写入。

在网络编程中,使用阻塞模式可以很方便地实现简单、易于理解的代码逻辑。但是,它也有一些缺点。首先,当存在大量连接时,由于每个连接都需要一个线程来处理 I/O,因此阻塞模式可能导致系统资源耗尽。其次,在高负载条件下,I/O 操作可能会变得非常缓慢,因为线程必须等待操作完成。因此,对于高并发应用程序,通常使用非阻塞和异步 I/O 模式来提高性能。

非阻塞模式

在 Java NIO 中,非阻塞模式是一种非常重要的概念。在传统的阻塞模式中,当一个线程调用输入或输出操作时,它会一直等待,直到操作完成为止。这意味着,如果有多个客户端请求连接或发送数据,服务器将不得不创建多个线程来处理每个请求,从而可能导致系统资源耗尽。

非阻塞 I/O(NIO)解决了这个问题,因为它允许应用程序异步地处理多个通道。在非阻塞模式下,当一个线程向通道发出请求并没有立即得到响应时,该线程可以继续处理其他任务。只有当数据准备好读取或写入时,线程才会返回通道。这样就可以使用单个线程处理多个连接和请求。

接下来结合代码进行深入理解;


Block Server

  1. 创建一个 ByteBuffer 用来接收数据;
ByteBuffer buffer = ByteBuffer.allocate(16);
  1. 创建服务器;
ServerSocketChannel ssc = ServerSocketChannel.open();
  1. 绑定监听端口;
ssc.bind(new InetSocketAddress(7999));
  1. 创建连接集合;
ArrayList<SocketChannel> channels = new ArrayList<>();
  1. accept() 建立与客户端连接,SocketChannel() 用来与客户端之间通信;
SocketChannel sc = ssc.accept();
  1. 接收客户端发送的数据;
channel.read(buffer);

完整代码如下:

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;

import static com.sidiot.netty.c1.ByteBufferUtil.debugRead;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        // 使用 nio 来理解阻塞模式,单线程

        // 1. ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);

        // 2. 创建服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();

        // 3. 绑定监听端口
        ssc.bind(new InetSocketAddress(7999));

        // 4. 创建连接集合
        ArrayList<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 5. accept 建立与客户端连接,SocketChannel 用来与客户端之间通信
            log.debug("connecting...");
            SocketChannel sc = ssc.accept();
            log.debug("connected... {}", sc);
            channels.add(sc);
            for (SocketChannel channel : channels) {
                // 6. 接收客户端发送的数据
                log.debug("before read... {}", channel);
                channel.read(buffer);
                buffer.flip();
                debugRead(buffer);
                buffer.clear();
                log.debug("after read... {}", channel);
            }
        }
    }
}

Non Block Server

与 Block Server 的代码相类似,但是将 ServerSocketChannelSocketChannel 都设置成了非阻塞模式:

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;

import static com.sidiot.netty.c1.ByteBufferUtil.debugRead;

@Slf4j
public class NonBlockServer {
    public static void main(String[] args) throws IOException {
        // 使用 nio 来理解阻塞模式,单线程

        // 1. ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);

        // 2. 创建服务器
        try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
            // 设置为非阻塞模式,没有连接时返回 null,不会阻塞线程
            ssc.configureBlocking(false);

            // 3. 绑定监听端口
            ssc.bind(new InetSocketAddress(7999));

            // 4. 创建连接集合
            ArrayList<SocketChannel> channels = new ArrayList<>();
            while (true) {
                // 5. accept 建立与客户端连接,SocketChannel 用来与客户端之间通信
                SocketChannel sc = ssc.accept();
                if (sc != null) {
                    log.debug("connected... {}", sc);
                    // 设置为非阻塞模式,若通道中没有数据,会返回 -1,不会阻塞线程
                    sc.configureBlocking(false);
                    channels.add(sc);
                }

                for (SocketChannel channel : channels) {
                    // 6. 接收客户端发送的数据
                    int read = channel.read(buffer);
                    if (read > 0){
                        buffer.flip();
                        debugRead(buffer);
                        buffer.clear();
                        log.debug("after read... {}", channel);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 7999));
        System.out.println("waiting...");
    }
}

分析

阻塞模式

先启动服务端,会发现因为 accept(),即没有客户端,导致阻塞发送,控制台输出如下:

10:25:37 [DEBUG] [main] c.s.n.c2.Server - connecting...

接着在客户端的 System.out.println("waiting..."); 处设置断点,启用调试模式,控制台输出如下:

10:26:26 [DEBUG] [main] c.s.n.c2.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]
10:26:26 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]

线程第二次进入阻塞状态,这是 channel.read() 导致的,因为没有读取到数据,因此,在客户端的 DEBUG 模式下,右键 sc 属性,选择 “对表达式求值”:

【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_java

写入如下代码,点击 “求值”:

【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_java_02

控制台输出如下:

【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_java_03

当我们继续 “求值” 时,发现控制台没有输出,这是因为又被 accept() 阻塞了,再新创建一个客户端就能收到消息了,控制台输出如下:

10:29:53 [DEBUG] [main] c.s.n.c2.Server - connecting...
10:37:06 [DEBUG] [main] c.s.n.c2.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:55084]
10:37:06 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [3]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 69 21                                        |Hi!             |
+--------+-------------------------------------------------+----------------+
10:37:06 [DEBUG] [main] c.s.n.c2.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]
10:37:06 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:55084]

如果不能启动两个客户端实例的话,需要在配置里进行设置,如下:

【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_非阻塞_04

非阻塞模式

多启动几个客户端,控制台输出如下:

15:36:52 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53122]
15:36:56 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53129]
15:37:08 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]

依照之前的步骤,通过客户端向服务端发送消息,控制台输出如下:

+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [6]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 73 69 64 69 6f 74                               |sidiot          |
+--------+-------------------------------------------------+----------------+
15:37:35 [DEBUG] [main] c.s.n.c.NonBlockServer - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]

+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [12]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21             |Hello World!    |
+--------+-------------------------------------------------+----------------+
15:37:48 [DEBUG] [main] c.s.n.c.NonBlockServer - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]

上述代码存在一个问题,因为设置为了非阻塞,会一直执行 while(true) 中的代码,【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_java_05 一直处于忙碌状态,会使得性能变低,所以实际情况中不使用这种方法处理请求;


小结

阻塞模式

  • 阻塞模式下,相关方法都会导致线程暂停:
  • ServerSocketChannel.accept() 会在没有连接建立时让线程暂停;
  • SocketChannel.read() 会在通道中没有数据可读时让线程暂停;
  • 阻塞的表现就是让线程暂停,暂停期间不会占用 CPU,但线程相当于闲置;
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持;
  • 但多线程下,会有一些新的问题:
  • 32 位 jvm 一个线程的大小为 320k,64 位 jvm 一个线程的大小为 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低;
  • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,长时间 inactive 会阻塞线程池中所有线程,因此不适合长连接,只适合短连接;

非阻塞模式

  • 可以通过 ServerSocketChannelconfigureBlocking(false) 方法将获得连接设置为非阻塞的。此时若没有连接,accept 会返回 【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_java_06
  • 可以通过 SocketChannelconfigureBlocking(false) 方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读,read 会返回 【Netty】从0到1(二):NIO-阻塞模式与非阻塞模式_客户端_07


后记

以上就是 从0到1(二):NIO-阻塞模式与非阻塞模式 的所有内容了,希望本篇博文对大家有所帮助!


标签:Netty,java,0.1,阻塞,模式,channels,线程,SocketChannel
From: https://blog.51cto.com/sidiot/6421978

相关文章

  • 责任链模式
    一、定义多个对象都有机会处理某个请求,将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。二、UML类图 Handler:抽象处理者角色,是一个处理请求的接口或抽象类;ConcreteHandler:具体的处理者角色,具体的处理者接收到请求后可以选择将请求处理掉,或者将请求传......
  • 外观(门面)模式--Facade
    一、代码示例#include<iostream>usingnamespacestd;classCarmera{public:voidturnOn(){cout<<"相机启动"<<endl;}voidturnOff(){cout<<"相机关闭"<<endl;}};classLig......
  • 小话设计模式
    设计模式1关系......
  • 代理模式
    代理模式在代理模式(ProxyPattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。介绍意图:为其他对象提供一种代理以控制对这个对象的访问。主要解决:在直接访问对象时带来的问题,比如说:要访......
  • 抽象工厂模式
    抽象工厂模式抽象工厂模式(AbstractFactoryPattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成......
  • 工厂模式
    工厂模式工厂模式(FactoryPattern)是Java中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。介绍意图:定义一个创建对象的接......
  • 单例模式
    单例模式单例模式(SingletonPattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不......
  • 设计模式目录
    目录抽象工厂模式代理模式单例模式工厂模式......
  • 观察者模式
    一、定义多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。二、示例下面用委托、事件,实现观察者模式1.Publisher.cs//定义发布者publicclassPublisher{//声明事件publiceventEv......
  • 记一次线上问题,Netty接收到的报文一次有数据一次没有数据
    最近线上遇到一个问题,客户端发送的tcp报文第一次连接成功后没有数据,第二次连接后正常带数据,第三次又没有数据...问题排查1:是否有负载均衡,其中有一台机器出现了异常,会出现一次成功一次失败的情况经过排查,本服务是没有负载均衡的,排除问题排查2:抓包分析 根据抓包数据,异常情况时......