二Netty--NIO入门

二Netty--NIO入门

第二章 NIO入门

网络编程基本模型:

​ server和client模型,两个进程相互通信。server端提供位置(ip+port),client端通过connect向服务端监听的地址(ip+port)发起连接请求。通过三次握手,建立连接,然后通过socket进行通信。

2.1 传统BIO编程

传统同步阻塞IO开发模型:

​ serverSocket绑定IP、启动监听端口port;socket发起连接操作。连接成功后,双方通过inputStream和outputStream进行同步阻塞通信。

2.1.1 BIO通信模型图

image-20220911154418828

server端:一个独立的acceptor线程,监听client端的连接,它收到client端连接请求后,为每个client创建一个新的线程进行链路处理,处理完毕,通过outputStream返回给client,然后线程销毁。

缺点:缺乏弹性伸缩能力。服务端线程数和客户端并发访问数1:1,系统性能急剧下降,直至对外停止服务。

2.1.2 同步阻塞IO涉及的流程

BIO的输入/输出流进行 同步阻塞通信:

(BIO的代码,可以在其中找到和上面BIO通信模型对应的部分)

    //client端,创建socket连接IP和PORT
    Socket socket = new Socket("192.111", 80);
    //server端,绑定监听端口port
    ServerSocket ss = new ServerSocket(80);
    Socket accSocket = ss.accept();
    // server端接收连接请求

    /**读入流*/
    BufferedReader in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
    String body = in.readLine();

   /** 输出流*/
    PrintWriter out= new PrintWriter(this.accSocket.getOutputStream(), true);
    out.println("search");

BIO核心的组件及流程如上。

注意:

​ BIO的阻塞,是由于InputStream.read和OutputStream.write(输入、输出流的读取和写入)是同步阻塞的。

2.1.3 同步阻塞IO源码

2.1.3.1 Server端

public class TimerServer  {
    public static void main(String[] args) throws IOException {
        int port=8080;
        ServerSocket server = null;

        try {
            /** serverSocket绑定端口port*/
            server = new ServerSocket(port);
            Socket socket = null;
            /** server端启动单独accptor线程,while循环,处理连接请求*/
            while (true) {
                socket = server.accept();
                /**处理的client连接建立后,为client创建新线程*/
                new Thread(new TimerServerHandler(socket)).start();

            }
        }finally {
            /** finally,关闭server*/
            if (server!=nullX)
                server.close();
                server = null;
        }

    }
}

11行,应用程序进程用while(true)来循环监听客户端连接,没有client接入,主线程阻塞在ServerSocket的accept上。

TimeServerHandler处理新建立socket链路的runnable的线程。

public class TimeServerHandler implements Runnable {

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    private Socket socket;

    @Override
    public void run() {
//        读取InputStream的reader
        BufferedReader in = null;
//        输出响应的writer
        PrintWriter out = null;

        try {
//            获取socket对应的reader
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream(), String.valueOf(true)));
//            创建输出writer
            out = new PrintWriter(this.socket.getOutputStream(), Boolean.parseBoolean(String.valueOf(true)));
//            输出内容body
            String body = null;

            while (true) {
                body = in.readLine();
//            跳出循环条件——in读到输入流尾部时,返回值为null
                if (body==null) break;
                /** 对读到的body内容,进行server端业务处理*/
                //// TODO: 2022/9/16  业务逻辑代码处理
                out.println("输出业务处理结果");
            }
        } catch (IOException e) {
            //程序结束,关闭和清理资源
          //释放输入、输出、socket的句柄资源,才能够让线程最后自动销毁并被虚拟机回收(需要对各个资源=null)
            in.close();
            out.close();
            socket.close();
          in,out,socket=null;
        }
    }
}

2.1.3.2 Client端

public class TimeClient {
    public static void main(String[] args) {
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;

        try {
          //client端socket指定要访问的IP和port,进行通信连接
            socket = new Socket("127.0.0.1", 80);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            //client发送信息、指令
            out.println("指令");
            
            //client等待server的回复
            /** in.readLine为同步阻塞,直至有信息到来*/
            String resp = in.readLine();
            
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            /** 释放资源以及句柄*/
            in.close();
            out.close();
            socket.close();
            in,out,socket.close();
        }
    
}

总结:(一连接一线程模型)

BIO问题在于,每一个client请求接入,server需要创建一个thread处理接入的client。BIO的通信模型,无法满足高性能的服务器,需要面对高性能、高并发接入的场景。

2.2 伪异步IO编程

改造BIO,将一个acceptor线程创建1:1的连接线程改进,通过acceptor把连接请求包装成Task,投递给后端的线程池ThreadPool或者消息队列MQ,通过N个线程处理M个client连接请求的模型。由于底层通信仍然是同步阻塞的IO(因为仍旧使用BIO的inputStream、outputStream的阻塞模型),所以被称为伪异步IO

2.2.1 伪异步IO通信模型图

客户端M:线程池最大线程N,其中M可以远大于N。

image-20220916133247117

伪异步IO模型原理:

当有新的client接入请求,server端把socket包装成Task(该任务实现Runnable接口),投递到后端ThreadPool中进行处理。线程池中有消息队列和N个线程,来处理传入消息队列的请求。

ThreadPool可以设置消息队列和活跃线程数量,因此可以控制占用的资源。

2.2.2 伪异步IO源码

仍旧以TimeServer为例,进行改造。只有server端需要改造

需要改造Server服务端、再增加后端的线程池

public class TimeServerFIO {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        
        try {
        serverSocket = new ServerSocket(80);
        Socket socket = null;
        //创建后端的任务线程池
        TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool(50, 10000);
        //单独的acceptor线程,处理client连接,包装成Task,投递给后端的线程池
        while (true) {
            socket = serverSocket.accept();
            //包装为task,投递给后端线程池
            singleExecutor.execute(new TimeServerHandler(socket));
        } 
        }
        finally {
            serverSocket.close();
            serverSocket = null;
        }
    }
}

封装的线程池ExecutePool,如下:

public class TimeServerHandlerExecutePool {
    private ExecutorService executor;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize) {
        executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), 
                maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
    }
	
  //实现pool的execute方法
    public void execute(java.lang.Runnable task) {
        executor.execute(task);
    }
}

​ 由于线程池和消息队列有界,因此,无论client并发连接多大,都不会导致线程个数过于膨胀或者内存溢出。相比BIO是一种改进。

但是,由于伪IO底层通信依然是同步阻塞模型,因此无法从根本解决BIO的问题。

总结

对于BIO的InputStream.read和OutputStream.write方法都是同步阻塞的,阻塞时间取决于对方IO线程处理速度和网络IO传输速度。因此,无论BIO,还是线程池改进的伪IO,底层通信都使用输入和输出流,因此都无法解决同步IO导致的通信线程阻塞问题。层层的IO同步阻塞,会导致系统级联故障,使系统崩溃。

2.3 NIO编程

NIO称为非阻塞IO。

NIO提供了Channel通道,包括SocketChannel(对应Socket)和ServerSocketChannel(对应ServerSocket)。这两种新增的通道,支持BIO和NIO两种通信模式。

使用:

低负载、低并发,使用阻塞模式,可以降低代码复杂度;高负载、高并发,使用NIO的非阻塞模式,性能和可靠性更高。

2.3.1 NIO类库介绍

NIO引入新的功能和组件:

首先,提供高速、面向块(缓冲区buffer为数据,构成成块的结构)的IO功能;其次,定义了包含数据的类,通过以块的形式(块,就是缓冲区构成的块结构),处理数据,且NIO不用本机代码就可以低级优化。

1 缓冲区Buffer

buffer作为对象,包含要写入和读出的数据。

区别BIO与NIO中:

BIO:数据直接write或者read到Stream对象;

NIO:所有数据用buffer处理,数据分别read或write到buffer中,然后任何时刻访问NIO的数据,都是通过buffer操作。

Buffer本质:数组,通常是字节数组ByteBuffer,且buffer提供了对数据块的结构化访问,以及维护读写位置limit等信息。

image-20220917015116069

每个buffer类,不只是存储数据的数组,还提供了一系列的方法,对数组访问。

2 通道channel

网络数据通过channel读取和写入数据。channel是双全工的,可以双向传输,可以更好的映射底层操作系统API。

区别channel和stream:

stream只是在一个方向上移动(只能是inputStream或者outputStream的子类);channel是双向的,且读和写可以同时进行。

image-20220917020819161

主要的类如下:

image-20230310153607709

3 多路复用器Selector

多路复用器能够选择已经就绪的任务(channel),是NIO多路复用模型的核心。且JDK使用epoll,没有连接数限制,一个selector可以负责上万client的请求。

原理:

selector轮询注册在其上的channel,如果某些channel发生write或者read事件,这个channel会处于就绪状态(调用channel的回调函数,通知selector当前channel就绪),会被selector轮询出来,然后通过SelectionKey可以获取就绪的channel集合,再进行后续的IO操作。

区别BIO:

BIO用while(true)内的serverSocket.accept,来循环的逐个获取就绪的连接请求。while以及accept的阻塞,以及新开启的处理线程,都是系统性能的瓶颈。

2.3.2 NIO服务端序列图

image-20220917023729570

        //step1 打开ServerSockerChannel,监听client的连接,是所有client连接的父管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //step2 绑定监听端口,设置连接非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(getByName("IP")), 80);
        serverSocketChannel.configureBlocking(false);

        //step3 创建多路复用器selector并打开;创建Reactor线程并启动
        //reactor线程启动,会调用selector来监听和轮询事件(可知,NIO中,reactor也是单独开启的线程,负责通过selector轮询就绪状态)
        Selector selector = Selector.open();
        new Thread(new ReactorTask()).start();

        //step4 serverSocketChannel注册到Reactor线程的selector上,并设置监听ACCEPT事件
        SelectionKey key = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, ioHandler);

        //step5 多路复用器selector,在reactor线程中,在while(it.hasNext)的无限循环体内,轮询就绪的Key
        int num = selector.select();
        Set selectedKeys = selector.selectedKeys();
        Iterator it = selectedKeys.iterator();
        while (it.hasNext()) {
            SelectionKey key = (SelectionKey) it.next();
            //处理IO中就绪的channel对应的key
        }

        //step6 selector监听到有client的连接请求,此时调用serverSocketChannel建立链路连接
        SocketChannel channel = serverSocketChannel.accept();

        //step7 设置client端链路为非阻塞模式
        channel.configureBlocking(false);
        channel.socket().setReuseAddress(true);

        //step8 新接入client端channel注册到reactor线程的selector上,监听读操作,用来read客户端发送的信息
        //理解:server端,socketChannel在selector上注册read监听,就是server端用来读取client发来的信息
        SelectionKey key = channel.register(selector, SelectionKey.OP_READ, ioHandler);

        //step9 异步读取客户端channel请求消息,到缓冲区receivedBuffer中
        ByteBuffer receivedBuffer = ByteBuffer.allocate(1024);
        int readNumber = channel.read(receivedBuffer);

        //step10 对读取到缓冲区buffer消息进行解码decode,将解码成功的消息,封装成Task
        //投递到业务线程池中,进行业务的逻辑处理
        Object message = null;
        while (receivedBuffer.hasRemaining()) {
            Object message = decode(receivedBuffer);
            messageList.add(message);
        }
        for (Object messageE : messageList)
            handleTask(messageE);

        //step11 将处理过的结果POJO对象,encode成ByteBuffer,调用socketChannel.write的异步写入方法,将消息异步发送给client
        channel.write(buffer);

注意:

如果轮询到的channel为write,则说明数据没有发送完成,需要继续发送。

2.3.3 NIO客户端序列图

image-20220917121615572

        //step1 打开SocketChannel,会绑定client的本地地址
        SocketChannel clientChannel = SocketChannel.open();

        //step2 设置SocketChannel为非阻塞模式,同时设置socketChannel.socket()的客户端连接的TCP参数
        clientChannel.configureBlocking(false);
        clientChannel.socket().setReuseAddress(true);
        clientChannel.socket().setReceiveBufferSize(1024);
        clientChannel.socket().setSendBufferSize(1024);

        //step3 异步连接服务器端
        boolean connected = clientChannel.connect(new InetSocketAddress("111", 80));

        //step4 判断client是否连接成功,如果true,则直接注册读READ状态到多路复用器
        if (connected) {
            clientChannel.register(selector, SelectionKey.OP_READ, ioHandler);
        } else {
            //step5 如果没有连接成功,向Reactor线程的selector注册CONNECT状态,监听server端的ACK连接响应
            clientChannel.register(selector,SelectionKey.OP_CONNECT,ioHandler);
        }

        //step6 创建多路复用器selector,并创建reactor线程,并启动
        Selector selector = Selector.open();
        new Thread(new ReactorTask()).start();

        //step7 selector在线程run方法的无限循环内,轮询就绪的key
        /** 这里的selector是client端的,它轮询的是是否有连接自身channel的CONNECT事件*/
        int num = selector.select();
        Set selectorKeys = selector.selectedKeys();
        Iterator it = selectorKeys.iterator();
        while (it.hasNext()) {
            SelectionKey key = (SelectionKey) it.next();
            //然后对轮询出的就绪状态的channel,通过key,进行IO处理
            //// TODO: 2022/9/17

            //step8 接收connect事件,并进行处理
            if (key.isConnectable())
                handlerConnect();
        }

        //step9 判断连接结果,如果连接成功,把当前socketChannel注册到selector上,监听READ事件
        /** 这里的监听READ事件,是监听server端返回的响应信息*/
        if (clientChannel.finishConnect())
            registerRead();

        //step10 注册读事件到selector(step10是step9的方法内容)
        clientChannel.register(selector,SelectionKey.OP_READ,ioHandler);

        //step11 异步读read客户端client请求消息到缓冲区buffer(这是读取准备发送的消息,先存入缓冲区)
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int readNum=clientChannel.read(readBuffer);

        //step12 对readBuffer进行编解码,读取消息,然后封装message到Task,投递到后端业务处理线程池中
        Object message = null;
        while (receivedBuffer.hasRemaining()) {
            Object message = decode(receivedBuffer);
            messageList.add(message);
        }
        for (Object messageE : messageList)
            handleTask(messageE);
        
        //step13 将POJO对象encode成byteBuffer,调用socketChannel的异步writer方法,消息异步发送给client端
        clientChannel.write(sendbuffer);

2.5 4种IO对比

2.5.1 概念梳理

1 异步非阻塞IO

NIO,是多路复用器模型(主要由selector实现),其只是IO复用模型实现的非阻塞IO,并不是异步IO。java1.5虽然底层使用epoll代替poll,提升了性能,但是没有改变底层模型,仍是多路复用模型;

AIO,才是真正的异步非阻塞IO模型。JDK1.7的NIO2.0提供了异步的socketChannel,是真正的异步IO。在异步IO操作时候,会传递信号变量,当操作完成后会操作对应的回调方法。

2.5.2 IO模型对比

image-20220917184348467

posted @ 2023-03-10 17:20  LeasonXue  阅读(32)  评论(0编辑  收藏  举报