当前位置: 首页 > article >正文

Netty基础—2.网络编程基础四

大纲

1.网络编程简介

2.BIO网络编程

3.AIO网络编程

4.NIO网络编程之Buffer

5.NIO网络编程之实战

6.NIO网络编程之Reactor模式

5.NIO网络编程之Buffer

(1)Buffer的作用

Buffer的作用是方便读写通道(Channel)中的数据。首先数据是从通道(Channel)读入缓冲区,从缓冲区写入通道(Channel)的。应用程序发送数据时,会先将数据写入缓冲区,然后再通过通道发送缓冲区的数据。应用数据读取数据时,会先将数从通道中读到缓冲区,然后再读取缓冲区的数据。

缓冲区本质上是一块可以写入数据、可以读取数据的内存。这块内存被包装成NIO的Buffer对象,并提供了一组方法用来方便访问该块内存。所以Buffer的本质是一块可以写入数据、可以读取数据的内存。

(2)Buffer的重要属性

一.capacity

Buffer作为一个内存块有一个固定的大小值,叫capacity。我们只能往Buffer中写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(读取数据或清除数据)才能继续写数据。

二.position

当往Buffer中写数据时,position表示当前的位置,position的初始值为0。当一个数据写到Buffer后,position会移动到下一个可插入数据的位置。所以position的最大值为capacity – 1。

当从Buffer中读取数据时,需要从某个特定的position位置读数据。如果将Buffer从写模式切换到读模式,那么position会被重置为0。当从Buffer的position处读到一个数据时,position会移动到下一个可读位置。

三.limit

在写模式下,Buffer的limit表示最多能往Buffer里写多少数据。在写模式下,Buffer的limit等于Buffer的capacity。

当Buffer从写模式切换到读模式时, limit表示最多能读到多少数据。因此,当Buffer切换到读模式时,limit会被设置成写模式下的position值。

图片

(3)Buffer的分配

要想获得一个Buffer对象首先要进行分配,每一个Buffer类都有allocate()方法。可以在堆上分配,也可以在直接内存上分配。

//分配一个capacity为48字节的ByteBuffer的例子
ByteBuffer buf = ByteBuffer.allocate(48);

//分配一个capacity为1024个字符的CharBuffer的例子
CharBuffer buf = CharBuffer.allocate(1024);

一般建议使用在堆上分配。如果应用偏计算,就用堆上分配。如果应用偏网络通讯频繁,就用直接内存。

wrap()方法可以把一个byte数组或byte数组的一部分包装成ByteBuffer对象。

ByteBuffer wrap(byte [] array);
ByteBuffer wrap(byte [] array, int offset, int length);

Buffer分配的例子:

//类说明: Buffer的分配
public class AllocateBuffer {
    //输出结果如下:
    //----------Test allocate--------
    //before allocate, 虚拟机可用的内存大小: 253386384
    //buffer = java.nio.HeapByteBuffer[pos=0 lim=102400000 cap=102400000]
    //after allocate, 虚拟机可用的内存大小: 150986368
    //directBuffer = java.nio.DirectByteBuffer[pos=0 lim=102400000 cap=102400000]
    //after direct allocate, 虚拟机可用的内存大小: 150986368
    //----------Test wrap--------
    //java.nio.HeapByteBuffer[pos=0 lim=32 cap=32]
    //java.nio.HeapByteBuffer[pos=10 lim=20 cap=32]
    public static void main(String[] args) {
        System.out.println("----------Test allocate--------");
        System.out.println("before allocate, 虚拟机可用的内存大小: " + Runtime.getRuntime().freeMemory());

        //堆上分配
        ByteBuffer buffer = ByteBuffer.allocate(102400000);
        System.out.println("buffer = " + buffer);
        System.out.println("after allocate, 虚拟机可用的内存大小: " + Runtime.getRuntime().freeMemory());

        //这部分用的直接内存
        ByteBuffer directBuffer = ByteBuffer.allocateDirect(102400000);
        System.out.println("directBuffer = " + directBuffer);
        System.out.println("after direct allocate, 虚拟机可用的内存大小: " + Runtime.getRuntime().freeMemory());

        System.out.println("----------Test wrap--------");
        byte[] bytes = new byte[32];
        buffer = ByteBuffer.wrap(bytes);
        System.out.println(buffer);

        buffer = ByteBuffer.wrap(bytes, 10, 10);
        System.out.println(buffer);
    }
}

(4)Buffer的读写

一.向Buffer中写数据

将数据写到Buffer有两种方式:

方式一:从Channel读出数据写到Buffer

方式二:通过Buffer的put()方法将数据写到Buffer

//从Channel写到Buffer的例子
int bytesRead = inChannel.read(buf);//从channel读出数据写到buffer
//通过put()方法将数据写到Buffer的例子
buf.put(127);

二.从Buffer中读取数据

从Buffer中读取数据有两种方式:

方式一:从Buffer中读取数据写入到Channel

方式二:使用get()方法从Buffer中读取数据

//从Buffer读取数据到Channel的例子
int bytesWritten = inChannel.write(buf);
//使用get()方法从Buffer中读取数据的例子
byte aByte = buf.get();

三.使用Buffer读写数据常见步骤

步骤一:写入数据到Buffer

步骤二:调用flip()方法

步骤三:从Buffer中读取数据

步骤四:调用clear()方法或compact()方法

flip()方法会将Buffer从写模式切换到读模式,调用flip()方法会将position设回0,并将limit设置成之前的position值。

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。

有两种方式能清空缓冲区:调用clear()方法或compact()方法。clear()方法会清空整个缓冲区,compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

四.其他常用操作

操作一:rewind()方法

Buffer.rewind()将position设回0,所以可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。

操作二:clear()与compact()方法

一旦读取完Buffer中的数据,需要让Buffer准备好再次被写入,这时候可以通过clear()方法或compact()方法来完成。

如果调用的是clear()方法,position将被设为0,limit被设为capacity的值。此时Buffer被认为是清空了,但是Buffer中的数据并未清除,只是这些标记能告诉我们可以从哪里开始往Buffer里写数据。

如果Buffer中有一些未读的数据,调用clear()方法,数据将被遗忘。意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么可以使用compact()方法。compact()方法会将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素正后面,limit属性依然像clear()方法一样设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

操作三:mark()与reset()方法

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position,之后可以通过调用Buffer.reset()方法恢复到这个position。

//类说明: Buffer方法演示
public class BufferMethod {
    public static void main(String[] args) {
        System.out.println("------Test get-------------");
        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer
            .put((byte) 'a')//0
            .put((byte) 'b')//1
            .put((byte) 'c')//2
            .put((byte) 'd')//3
            .put((byte) 'e')//4
            .put((byte) 'f');//5
        //before flip()java.nio.HeapByteBuffer[pos=6 lim=32 cap=32]
        System.out.println("before flip()" + buffer);

        //转换为读取模式: pos置为0, lim置为转换前pos的值
        buffer.flip();
        //before get():java.nio.HeapByteBuffer[pos=0 lim=6 cap=32]
        System.out.println("before get():" + buffer);

        //get()会影响position的位置, 这是相对取;
        System.out.println((char) buffer.get());
        //after get():java.nio.HeapByteBuffer[pos=1 lim=6 cap=32]
        System.out.println("after get():" + buffer);

        //get(index)不影响position的值, 这是绝对取;
        System.out.println((char) buffer.get(2));
        //after get(index):java.nio.HeapByteBuffer[pos=1 lim=6 cap=32]
        System.out.println("after get(index):" + buffer);

        byte[] dst = new byte[10];
        //position移动两位
        buffer.get(dst, 0, 2);
        //after get(dst, 0, 2):java.nio.HeapByteBuffer[pos=3 lim=6 cap=32]
        System.out.println("after get(dst, 0, 2):" + buffer);
        System.out.println("dst:" + new String(dst));//dst:bc

        System.out.println("--------Test put-------");
        ByteBuffer bb = ByteBuffer.allocate(32);
        //before put(byte):java.nio.HeapByteBuffer[pos=0 lim=32 cap=32]
        System.out.println("before put(byte):" + bb);
        //put()不带索引会改变pos, after put(byte):java.nio.HeapByteBuffer[pos=1 lim=32 cap=32]
        System.out.println("after put(byte):" + bb.put((byte) 'z'));

        //put(2,(byte) 'c')不改变position的位置
        bb.put(2, (byte) 'c');
        //after put(2,(byte) 'c'):java.nio.HeapByteBuffer[pos=1 lim=32 cap=32]
        System.out.println("after put(2,(byte) 'c'):" + bb);
        System.out.println(new String(bb.array()));

        //这里的buffer是abcdef[pos=3 lim=6 cap=32]
        bb.put(buffer);
        //after put(buffer):java.nio.HeapByteBuffer[pos=4 lim=32 cap=32]
        System.out.println("after put(buffer):" + bb);
        System.out.println(new String(bb.array()));

        System.out.println("--------Test reset----------");
        buffer = ByteBuffer.allocate(20);
        System.out.println("buffer = " + buffer);
        buffer.clear();
        buffer.position(5);//移动position到5
        buffer.mark();//记录当前position的位置
        buffer.position(10);//移动position到10
        System.out.println("before reset:" + buffer);
        buffer.reset();//复位position到记录的地址
        System.out.println("after reset:" + buffer);

        System.out.println("--------Test rewind--------");
        buffer.clear();
        buffer.position(10);//移动position到10
        buffer.limit(15);//限定最大可写入的位置为15
        System.out.println("before rewind:" + buffer);

        buffer.rewind();//将position设回0
        System.out.println("before rewind:" + buffer);

        System.out.println("--------Test compact--------");
        buffer.clear();
        //放入4个字节,position移动到下个可写入的位置,也就是4
        buffer.put("abcd".getBytes());
        System.out.println("before compact:" + buffer);
        System.out.println(new String(buffer.array()));
        buffer.flip();//将position设回0,并将limit设置成之前position的值
        System.out.println("after flip:" + buffer);
        
        //从Buffer中读取数据的例子,每读一次,position移动一次
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        System.out.println((char) buffer.get());
        System.out.println("after three gets:" + buffer);
        System.out.println(new String(buffer.array()));
        
        //compact()方法将所有未读的数据拷贝到Buffer起始处
        //然后将position设到最后一个未读元素正后面
        buffer.compact();
        System.out.println("after compact:" + buffer);
        System.out.println(new String(buffer.array()));
    }
}

(5)Buffer常用方法总结

图片

6.NIO网络编程之实战

(1)Selector

(2)Channels

(3)SelectionKey

(4)NIO的开发流程

(5)NIO的开发例子

(1)Selector

Selector的含义是选择器,也可以称为轮询代理器、事件订阅器。Selector的作用是注册事件和对Channel进行管理。

应用程序可以向Selector对象注册它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣,Selector中会维护一个已经注册的Channel的容器。

(2)Channels

Channel可以和操作系统进行内容传递,应用程序可以通过Channel读数据,也可以通过Channel向操作系统写数据,当然写数据和读数据都要通过Buffer来实现。

所有被Selector注册的Channel都是继承SelectableChannel的子类,通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。ScoketChannel和ServerSocketChannel都是SelectableChannel类的子类。

(3)SelectionKey

NIO中的SelectionKey共定义了四种事件类型:OP_ACCEPT、OP_READ、OP_WRITE、OP_CONNECT,分别对应接受连接、读、写、请求连接的网络Socket操作。

ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时操作系统就会通知这些Channel。

每个操作类型(事件类型)的就绪条件:

图片

(4)NIO的开发流程

步骤一:服务端启动ServerSocketChannel,关注OP_ACCEPT事件。

步骤二:客户端启动SocketChannel,连接服务端,关注OP_CONNECT事件。

步骤三:服务端接受连接,然后启动一个SocketChannel。该SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件。

步骤四:客户端的SocketChannel发现连接建立后,关注OP_READ、OP_WRITE事件,一般客户端需要发送数据了才能关注OP_READ事件。

步骤五:连接建立后,客户端与服务器端开始相互发送消息(读写),然后根据实际情况来关注OP_READ、OP_WRITE事件。

图片

(5)NIO的开发例子

一.客户端的代码

//类说明: NIO通信的客户端
public class NioClient {
    private static NioClientHandle nioClientHandle;
    public static void start() {
        if (nioClientHandle != null) {
            nioClientHandle.stop();
        }
        nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP, DEFAULT_PORT);
        new Thread(nioClientHandle, "Client").start();
    }

    //向服务器发送消息
    public static boolean sendMsg(String msg) throws Exception {
        nioClientHandle.sendMsg(msg);
        return true;
    }

    public static void main(String[] args) throws Exception {
        start();
        Scanner scanner = new Scanner(System.in);
        while(NioClient.sendMsg(scanner.next()));
    }
}

//类说明: NIO通信的客户端处理器
public class NioClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean started;

    public NioClientHandle(String ip, int port) {
        this.host = ip;
        this.port = port;
        try {
            //创建选择器
            selector = Selector.open();

            //打开通道
            socketChannel = SocketChannel.open();

            //如果为true, 则此通道将被置于阻塞模式; 如果为false, 则此通道将被置于非阻塞模式;
            //另外, IO复用本身就是非阻塞模式, 所以设置false;
            socketChannel.configureBlocking(false);

            //表示连接已经打开
            started = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void stop() {
        started = false;
    }

    private void doConnect() throws IOException {
        //关于socketChannel.connect方法的说明:
        //如果此通道处于非阻塞模式, 则调用此方法将启动一个非阻塞连接的操作;
        //如果该连接建立得非常快, 就像本地连接可能发生的那样, 则此方法返回true;
        //否则, 此方法返回false, 稍后必须通过调用finishConnect方法完成连接操作
        
        //如果成功连接则什么都不做
        if (socketChannel.connect(new InetSocketAddress(host, port))) {

        } else {
            //连接还未完成, 所以注册连接就绪事件, 向selector表示关注这个事件
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    @Override
    public void run() {
        try {
            //发起连接
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        //循环遍历selector
        while(started) {
            try {
                //selector.select()会阻塞, 只有当至少一个注册的事件发生的时候才会继续
                selector.select();

                //获取当前有哪些事件可以使用
                Set<SelectionKey> keys = selector.selectedKeys();

                //转换为迭代器
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()) {
                    key = it.next();
                    it.remove();//拿到key后从迭代器移除
                    try {
                        handleInput(key);
                    } catch (IOException e) {
                        e.printStackTrace();
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //selector关闭后会自动释放里面管理的资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //具体的事件处理方法
    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            //根据SelectionKey获得关心当前事件的channel
            SocketChannel sc = (SocketChannel)key.channel();
            //处理连接事件
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else{
                    System.exit(1);
                }
            }
            //有数据可读事件
            if (key.isReadable()) {
                //创建ByteBuffer, 并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取请求码流, 返回读取到的字节数, 从channel读出来并写到buffer里去
                int readBytes = sc.read(buffer);

                //读取到字节,对字节进行编解码
                if (readBytes > 0) {
                    //将缓冲区当前的limit设置为position,position=0,
                    //用于后续对缓冲区的读取操作
                    buffer.flip();

                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String result = new String(bytes,"UTF-8");

                    System.out.println("accept message: " + result);
                } else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    //发送消息
    private void doWrite(SocketChannel channel, String request) throws IOException {
        //将消息编码为字节数组
        byte[] bytes = request.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
    }

    //写数据对外暴露的API
    public void sendMsg(String msg) throws Exception {
        socketChannel.register(selector, SelectionKey.OP_READ);
        doWrite(socketChannel, msg);
    }
}

二.服务端的代码

//类说明: NIO通信的服务端
public class NioServer {
    private static NioServerHandle nioServerHandle;
    public static void start() {
        if (nioServerHandle != null) {
            nioServerHandle.stop();
        }
        nioServerHandle = new NioServerHandle(DEFAULT_PORT);
        new Thread(nioServerHandle,"Server").start();
    }

    public static void main(String[] args) {
        start();
    }
}

//类说明: NIO通信的服务端处理器
public class NioServerHandle implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;

    //构造方法
    public NioServerHandle(int port) {
        try {
            //创建选择器
            selector = Selector.open();

            //打开通道
            serverChannel = ServerSocketChannel.open();

            //如果为true, 则此通道将被置于阻塞模式; 如果为false, 则此通道将被置于非阻塞模式;
            //另外, IO复用本身就是非阻塞模式, 所以设置false;
            serverChannel.configureBlocking(false);

            //指定监听的端口
            serverChannel.socket().bind(new InetSocketAddress(port));

            //注册服务端关心的事件: 连接事件
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            //表示连接已经打开
            started = true;

            System.out.println("服务器已启动, 端口号: " + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void stop() {
        started = false;
    }

    @Override
    public void run() {
        //循环遍历selector
        while(started) {
            try {
                //阻塞, 只有当至少一个注册的事件发生的时候才会继续
                selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while(it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch(Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch(Throwable t) {
                t.printStackTrace();
            }
        }

        //selector关闭后会自动释放里面管理的资源
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            //处理新接入的请求消息
            if (key.isAcceptable()) {
                //获得关心当前事件的channel
                ServerSocketChannel ssc = (ServerSocketChannel)key.channel();

                //通过ServerSocketChannel的accept创建SocketChannel实例
                //完成该操作意味着完成TCP三次握手, TCP物理链路正式建立
                SocketChannel sc = ssc.accept();
                System.out.println("======socket channel 建立连接");

                //设置为非阻塞的
                sc.configureBlocking(false);

                //连接已经完成了, 可以开始关心读事件了
                sc.register(selector, SelectionKey.OP_READ);
            }

            //读消息
            if (key.isReadable()) {
                System.out.println("======socket channel 数据准备完成, " + "可以去读==读取=======");
                SocketChannel sc = (SocketChannel) key.channel();

                //创建ByteBuffer, 并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);

                //读取请求码流, 返回读取到的字节数
                int readBytes = sc.read(buffer);

                //读取到字节, 对字节进行编解码
                if (readBytes > 0) {
                    //将缓冲区当前的limit设置为position=0, 用于后续对缓冲区的读取操作
                    buffer.flip();

                    //根据缓冲区可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];

                    //将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes,"UTF-8");
                    System.out.println("服务器收到消息: " + message);

                    //处理数据
                    String result = response(message) ;

                    //发送应答消息
                    doWrite(sc, result);
                } else if (readBytes < 0) {
                    //链路已经关闭, 释放资源
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    //发送应答消息
    private void doWrite(SocketChannel channel,String response) throws IOException {
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        //flip操作
        writeBuffer.flip();
        //发送缓冲区的字节数组
        channel.write(writeBuffer);
    }
}

6.NIO网络编程之Reactor模式

(1)单线程的Reactor模式

(2)单线程Reactor模式的改进

(3)多线程的Reactor模式

(1)单线程的Reactor模式

图片

一.单线程Reactor模式的流程

首先服务端的Reactor其实是一个线程对象。Reactor会启动事件循环,并使用Selector(选择器)来实现IO多路复用。

然后服务端启动时会注册一个Acceptor事件处理器到Reactor中。这个Acceptor事件处理器会关注accept事件,这样Reactor监听到accept事件就会交给Acceptor事件处理器进行处理了。

当客户端向服务器端发起一个连接请求时,Reactor就会监听到一个accept事件,于是会将该accept事件派发给Acceptor处理器进行处理。

接着Acceptor处理器通过accept()方法便能得到这个客户端对应的连接(SocketChannel),然后将该连接(SocketChannel)所关注的read事件及对应的read事件处理器注册到Reactor中,这样Reactor监听到该连接的read事件就会交给对应的read事件处理器进行处理。

当Reactor监听到客户端的连接(SocketChannel)有读写事件发生时,就会将读写事件派发给对应的读写事件处理器进行处理。比如读事件处理器会通过SocketChannel的read()方法读取数据,此时的read()方法可以直接读取到数据,不需要阻塞等待可读数据的到来。

每当Acceptor处理器和读写事件处理器处理完所有就绪的感兴趣的IO事件后,Reactor线程会再次执行select()方法阻塞等待新的事件就绪并将其分派给对应处理器进行处理。

二.单线程Reactor模式的问题

注意:单线程的Reactor模式中的单线程主要是针对IO操作而言的,也就是所有的IO的accept、read、write、connect操作都在一个线程上完成。

由于在单线程Reactor模式中,不仅IO操作在Reactor线程上,而且非IO的业务操作也在Reactor线程上进行处理,这会大大降低IO请求的响应。所以应将非IO的业务操作从Reactor线程上剥离,以提高Reactor线程对IO请求的响应。

(2)单线程Reactor模式的改进

图片

一.增加工作线程池来进行改进

为了改善单线程Reactor模式中Reactor线程还要处理业务逻辑,可以添加一个工作线程池。将非IO操作(解码、计算、编码)从Reactor线程中移出,然后转交给这个工作线程池来执行。所有IO操作依旧由单个Reactor线程来完成,如IO的accept、read、write以及connect操作。这样就能提高Reactor线程的IO响应,避免因耗时的业务逻辑而降低对后续IO请求的处理。

二.使用线程池的好处

合理使用线程池,可以带来很多好处:

好处一:减少频繁创建和销毁线程的性能开销

好处二:重复利用线程,避免对每个任务都创建线程,可以提高响应速度

好处三:合理设置线程池的大小,可以避免因为线程池过大影响性能

三.单线程Reactor不适合高并发场景

对于一些小容量的应用场景,可以使用单线程Reactor模型,但是对于一些高负载、大并发或大数据量的应用场景却不合适。

原因一:一个NIO线程同时处理成百上千的链路,性能上无法支撑。即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送。

原因二:当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时。超时之后往往会进行重发,这更加重了NIO线程的负载。最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。

(3)多线程的Reactor模式

图片

一.多线程的Reactor模式介绍

在多线程的Reactor模式下,存在一个Reactor线程池,Reactor线程池里的每一个Reactor线程都有自己的Selector和事件分发逻辑。

Reactor线程池里的主反应器线程mainReactor可以只有一个,但子反应器线程subReactor一般会有多个,通常subReactor也是一个线程池。

主反应器线程mainReactor主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给子反应器线程subReactor,由subReactor来完成和客户端的通信。

二.多线程的Reactor模式流程

首先服务端的Reactor变成了多个线程对象,分为mainReactor和subReactor。这些Reactor对象也会启动事件循环,并使用Selector(选择器)来实现IO多路复用。

然后服务端在启动时会注册一个Acceptor事件处理器到mainReactor中。这个Acceptor事件处理器会关注accept事件,这样mainReactor监听到accept事件就会交给Acceptor事件处理器进行处理。

当客户端向服务端发起一个连接请求时,mainReactor就会监听到一个accept事件,于是就会将这个accept事件派发给Acceptor处理器来进行处理。

接着Acceptor处理器通过accept()方法便能得到这个客户端对应的连接(SocketChannel),然后将这个连接SocketChannel传递给subReactor线程池进行处理。

subReactor线程池会分配一个subReactor线程给这个SocketChannel,并将SocketChannel关注的read事件及对应的read事件处理器注册到这个subReactor线程中。当然也会注册关注的write事件及write事件处理器到subReactor线程中以完成IO写操作。

总之,Reactor线程池中的每一Reactor线程都会有自己的Selector和事件分发逻辑。当有IO事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。

注意,这里subReactor线程只负责完成IO的read()操作,在读取到数据后将业务逻辑的处理放入到工作线程池中完成。若完成业务逻辑后需要返回数据给客户端,则IO的write()操作还是会被提交回subReactor线程来完成。这样所有的IO操作依旧在Reactor线程(mainReactor或subReactor)中完成,而工作线程池仅用来处理非IO操作的逻辑。

多Reactor线程模式将"接收客户端的连接请求"和"与该客户端进行读写通信"分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样就不会因为read()操作的数据量太大而导致后面的客户端连接请求得不到及时处理。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过subReactor线程池将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。

Netty服务端就是使用了多线程的Reactor模式。


http://www.kler.cn/a/581135.html

相关文章:

  • MoonSharp 文档三
  • Session、Cookie、Token的区别
  • 【每日学点HarmonyOS Next知识】状态变量、动画UI残留、Tab控件显示、ob前缀问题、文字背景拉伸
  • SICK Ranger3源码分析——断线重连
  • python之使用scapy扫描本机局域网主机,输出IP/MAC表
  • 算法面试题深度解析:LeetCode 2012.数组元素的美丽值求和计算与多方案对比
  • Acknowledgment.nack方法重试消费kafka消息异常
  • 【SpringMVC】深入解析使用 Postman 在请求中传递对象类型、数组类型、参数类型的参数方法和后端参数重命名、及非必传参数设置的方法
  • 【物联网-以太网-W5500】
  • Django ORM自定义排序的实用示例
  • 神经网络优化
  • DeepSeek-R1本地化部署(Mac)
  • 电机控制常见面试问题(四)———
  • 量子效应模拟:Python中的奇妙世界
  • DeepSeek刷力扣辅助题单 存留记录
  • 在 MAC mini4 上安装与使用 ComfyUI 文生图软件完整指南
  • 小橙优选创新发展
  • android storage_state
  • Opik - 开源 LLM 评估平台
  • 你使用过哪些 Java 并发工具类?