Reactor 网络模型、Java代码实例

news2024/10/7 19:17:01

文章目录

  • 1. 概述
  • 2. Reactor 单线程模型
    • 2.1 ByteBufferUtil
    • 2.2 服务端代码
    • 2.3 客户端
    • 2.4 运行截图
  • 3. Reactor多线程模型
    • 3.1 服务端代码
    • 3.2 运行截图
  • 4. 主从 Reactor多线程模型
    • 4.1 服务端代码
    • 4.2 运行截图
  • 参考文献

1. 概述

在 I/O 多路复用的场景下,当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。

Reactor 模型主要分为三种

  • Reactor 单线程模型
  • Reactor 多线程模型
  • 主从 Reactor 多线程模型

Doug Lea 教授的课件 : https://gee.cs.oswego.edu/dl/cpjslides/nio.pdf

Java Socket 网络编程实例(阻塞IO、非阻塞IO、多路复用Selector、AIO)

2. Reactor 单线程模型

Reactor 单线程模型,是指所有I/O操作(监听服务端, 接受客户端连接请求;消息的读取、解码、编码、发送)都在同一个NIO线程上面完成
在这里插入图片描述

2.1 ByteBufferUtil

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;

/**
 * ByteBufferUtil类提供了ByteBuffer和String之间转换的便捷方法。
 * 这些方法使用UTF-8编码进行转换,确保了数据的正确性和一致性。
 */
public class ByteBufferUtil {

    /**
     * 从ByteBuffer中读取字符串。
     *
     * @param byteBuffer 待读取的ByteBuffer,应确保其为读模式。
     * @return 从ByteBuffer解码得到的字符串。
     * @throws CharacterCodingException 如果解码过程中发生错误。
     */
    public static String read(ByteBuffer byteBuffer) throws CharacterCodingException {
        // 使用UTF-8解码器将ByteBuffer中的字节解码为CharBuffer。
        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
        // 将CharBuffer转换为字符串并返回。
        return charBuffer.toString();
    }

    /**
     * 将字符串写入ByteBuffer。
     *
     * @param string 待写入的字符串。
     * @return 编码后的ByteBuffer。
     * @throws CharacterCodingException 如果编码过程中发生错误。
     */
    public static ByteBuffer read(String string) throws CharacterCodingException {
        // 使用UTF-8编码器将字符串编码为ByteBuffer。
        return StandardCharsets.UTF_8.encode(string);
    }

    /**
     * 主函数用于演示ByteBuffer和字符串之间的相互转换。
     *
     * @param args 命令行参数。
     * @throws CharacterCodingException 如果编码或解码过程中发生错误。
     */
    public static void main(String[] args) throws CharacterCodingException {
        // 将字符串"test"编码为ByteBuffer,然后从ByteBuffer解码回字符串并打印。
        System.out.println(ByteBufferUtil.read(ByteBufferUtil.read("test")));
    }

}

2.2 服务端代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.util.Set;

public class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocket;

    /**
     * 初始化Reactor,打开选择器和服务器套接字通道,并注册接受操作。
     *
     * @param port 服务器监听的端口号。
     * @throws IOException 如果打开选择器或服务器套接字通道失败。
     */
    public Reactor(int port) throws IOException {
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        serverSocket.configureBlocking(false);
        SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        sk.attach(new Acceptor());
    }

    /**
     * Reactor的主要运行方法,负责循环监听选择器上的事件,并分派处理。
     */
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                System.out.println("selected:" + selected.size());
                for (SelectionKey selectionKey : selected) {
                    dispatch(selectionKey);
                }
                selected.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 根据选择键分派相应的处理逻辑。
     *
     * @param k 选择键。
     * @throws IOException 如果发生I/O错误。
     */
    void dispatch(SelectionKey k) throws IOException {
        Run r = (Run) (k.attachment());
        if (r != null)
            r.run();
    }

    /**
     * 接受者类,负责接受新的客户端连接。
     */
    class Acceptor implements Run { // inner
        public void run() {
            try {
                SocketChannel c = serverSocket.accept();
                if (c != null)
                    new Handler(selector, c);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 处理者类,负责处理客户端的读写操作。
     */
    final class Handler implements Run {
        final SocketChannel socket;
        final SelectionKey sk;
        ByteBuffer input = ByteBuffer.allocate(1024);
        ByteBuffer output = ByteBuffer.allocate(1024);

        /**
         * 初始化处理者,注册读操作兴趣。
         *
         * @param sel 选择器。
         * @param c   客户端套接字通道。
         * @throws IOException 如果注册操作失败。
         */
        Handler(Selector sel, SocketChannel c)
                throws IOException {
            socket = c;
            c.configureBlocking(false);
            sk = socket.register(sel, 0);
            sk.attach(this);
            sk.interestOps(SelectionKey.OP_READ);
            sel.wakeup();
        }

        /**
         * 检查输入缓冲区是否已完成读取。
         *
         * @return 如果输入缓冲区还有剩余数据,则返回true;否则返回false。
         */
        boolean inputIsComplete() {
            return input.hasRemaining();
        }

        /**
         * 检查输出缓冲区是否已完成写入。
         *
         * @return 如果输出缓冲区没有剩余空间,则返回true;否则返回false。
         */
        boolean outputIsComplete() {
            return !output.hasRemaining();
        }

        /**
         * 处理输入缓冲区的数据。
         *
         * @throws CharacterCodingException 如果字符编码转换失败。
         */
        void process() throws CharacterCodingException {
            // 否则,将缓冲区反转并打印读取的数据
            input.flip();
            String request = ByteBufferUtil.read(input);

            System.out.println(request);
            input.clear();

            output = ByteBufferUtil.read("你好: " + request);
        }

        /**
         * 执行处理逻辑,包括读取数据、处理数据和准备写操作。
         *
         * @throws IOException 如果发生I/O错误。
         */
        public void run() throws IOException {
            socket.read(input);
            if (inputIsComplete()) {
                process();
                sk.attach(new Sender());
                sk.interestOps(SelectionKey.OP_WRITE);
                sk.selector().wakeup();
            }
        }

        /**
         * 发送者类,负责将处理后的数据写回客户端。
         */
        class Sender implements Run {
            public void run() throws IOException {
                socket.write(output);
                if (outputIsComplete())  {
                    new Handler(selector, socket);
                }
            }
        }
    }

    /**
     * 接口Run定义了所有处理逻辑的运行方法。
     */
    public interface Run {
        public abstract void run() throws IOException;
    }

    /**
     * 程序入口点,创建并启动Reactor线程。
     *
     * @param args 命令行参数。
     * @throws IOException 如果创建Reactor失败。
     */
    public static void main(String[] args) throws IOException {
        Reactor reactor = new Reactor(6666);
        new Thread(reactor).start();
        while (true) ;
    }
}

2.3 客户端

import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class SelectorClient {

    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建Socket通道并连接到服务器
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 6666));

        // 初始化输入和输出ByteBuffer
        ByteBuffer inputBuffer = ByteBuffer.allocate(512);
        ByteBuffer serverOutput = ByteBuffer.allocate(512);

        // 循环接收用户输入并发送给服务器
        while (true) {
            // 使用Scanner获取用户输入
            Scanner in = new Scanner(System.in);
            String input = in.nextLine();
            System.out.println("user input: " + input);

            if (StringUtils.isBlank(input)) {
                continue;
            }

            // 清空输入缓冲区,放入用户输入,然后反转准备写入
            inputBuffer.clear();
            inputBuffer.put(input.getBytes(StandardCharsets.UTF_8));
            inputBuffer.flip();

            // 将输入数据写入Socket通道
            sc.write(inputBuffer);
            System.out.println("send to server " + input);

            // 循环读取服务器响应
            int times = 1;
            while (true) {
                // 清空服务器响应缓冲区,准备读取数据
                serverOutput.clear();
                // 从Socket通道读取数据
                sc.read(serverOutput);

                // 如果没有读取到数据,继续尝试读取
                if (!serverOutput.hasRemaining()) {
                    TimeUnit.SECONDS.sleep(1);
                    times++;
                    System.out.println(times);
                    if (times > 10) {
                        break;
                    }
                    continue;
                }

                // 反转缓冲区,读取数据并打印
                serverOutput.flip();
                System.out.println("server response " + ByteBufferUtil.read(serverOutput));

                // 读取完成后退出内层循环
                break;
            }
        }
    }
}

2.4 运行截图

在这里插入图片描述
在这里插入图片描述

3. Reactor多线程模型

Reactor多线程模型 和 Reactor单线程模型最大的区别就是有一组NIO线程来处理I/O操作:

  • 有一个NIO线程 Acceptor线程,监听服务端, 接受客户端连接请求
  • 网络I/O操作,读写(消息的读取、解码、编码、发送)等由一个NIO线程池负责
  • 一个NIO线程可以处理N条链路, 一个链路之对应一个NIO线程, 防止出现并发问题

在这里插入图片描述

3.1 服务端代码

服务端端代码如下,客户端同上:

import lombok.SneakyThrows;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 该类实现了使用线程池处理NIO服务器的逻辑。
 */
public class ReactorWithThreadPool implements Runnable {

    /**
     * 处理器线程池,用于执行具体的处理任务。
     */
    static ThreadPoolExecutor HANDLER_POOL = new ThreadPoolExecutor(2, 4,
            10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(20), new ThreadPoolExecutor.CallerRunsPolicy());

    final Selector selector;
    final ServerSocketChannel serverSocket;

    /**
     * 创建一个NIO服务器,监听指定端口。
     *
     * @param port 服务器监听的端口号。
     * @throws IOException 如果打开选择器或服务器套接字失败。
     */
    public ReactorWithThreadPool(int port) throws IOException {
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        serverSocket.configureBlocking(false);
        SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        sk.attach(new Acceptor());
    }

    /**
     * 主循环,负责监听选择器上的事件。
     */
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                System.out.println("selected:" + selected.size());
                for (SelectionKey selectionKey : selected) {
                    if (selectionKey.isReadable()) {
                        System.out.println("selectionKey read");
                    }
                    if (selectionKey.isWritable()) {
                        System.out.println("selectionKey write");
                    }
                    dispatch(selectionKey);
                }
                selected.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 分派选择键对应的处理程序。
     *
     * @param k 需要处理的选择键。
     * @throws IOException 如果操作通道失败。
     */
    void dispatch(SelectionKey k) throws IOException {
        Run r = (Run) (k.attachment());
        if (r != null)
            r.run();
    }

    /**
     * 接受者处理程序,负责接受新的客户端连接。
     */
    class Acceptor implements Run { // inner
        public void run() {
            try {
                SocketChannel c = serverSocket.accept();
                if (c != null)
                    new Handler(selector, c);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 处理客户端请求的处理程序。
     */
    final class Handler implements Run {
        final SocketChannel socket;
        final SelectionKey sk;
        ByteBuffer input = ByteBuffer.allocate(1024);
        ByteBuffer output = ByteBuffer.allocate(1024);

        /**
         * 创建一个新的处理程序实例。
         *
         * @param sel 选择器。
         * @param c   客户端套接字通道。
         * @throws IOException 如果注册选择键或配置套接字失败。
         */
        Handler(Selector sel, SocketChannel c)
                throws IOException {
            socket = c;
            c.configureBlocking(false);
            sk = socket.register(sel, 0);
            sk.attach(this);
            sk.interestOps(SelectionKey.OP_READ);
            sel.wakeup();
        }

        /**
         * 检查输入缓冲区是否已完成读取。
         *
         * @return 如果输入缓冲区还有剩余,则为true;否则为false。
         */
        boolean inputIsComplete() {
            return input.hasRemaining();
        }

        /**
         * 检查输出缓冲区是否已完成发送。
         *
         * @return 如果输出缓冲区没有剩余,则为true;否则为false。
         */
        boolean outputIsComplete() {
            return !output.hasRemaining();
        }

        /**
         * 处理输入数据。
         *
         * @throws CharacterCodingException 如果字符编码失败。
         */
        void process() throws CharacterCodingException {
            // 否则,将缓冲区反转并打印读取的数据
            input.flip();
            String request = ByteBufferUtil.read(input);

            System.out.println(request);
            input.clear();

            output = ByteBufferUtil.read("你好: " + request);
        }

        /**
         * 读取客户端输入,并根据情况启动处理程序或发送器。
         *
         * @throws IOException 如果读取通道失败。
         */
        public void run() throws IOException {
            socket.read(input);
            if (inputIsComplete()) {
                HANDLER_POOL.execute(new Processor());
            }
        }

        /**
         * 发送器处理程序,负责向客户端发送数据。
         */
        class Sender implements Run {
            public void run() throws IOException {
                socket.write(output);
                if (outputIsComplete()) {
                    new Handler(selector, socket);
                }
            }
        }

        /**
         * 处理请求的处理程序,负责处理输入数据并准备输出。
         */
        class Processor implements Runnable {
            @Override
            @SneakyThrows
            public void run() {
                process();
                sk.attach(new Sender());
                sk.interestOps(SelectionKey.OP_WRITE);
                sk.selector().wakeup();
            }
        }
    }

    /**
     * 处理器接口,定义了处理程序应实现的运行方法。
     */
    public interface Run {
        public abstract void run() throws IOException;
    }

    /**
     * 程序入口点。
     *
     * @param args 命令行参数。
     * @throws IOException 如果启动服务器失败。
     */
    public static void main(String[] args) throws IOException {
        ReactorWithThreadPool reactor = new ReactorWithThreadPool(6666);
        new Thread(reactor).start();
        while (true) ;
    }
}

3.2 运行截图

在这里插入图片描述
在这里插入图片描述

4. 主从 Reactor多线程模型

主从 Reactor多线程模型的特点:服务端接受客户端连接,不再是一个单独的NIO线程,而是一个独立的NIO线程池。

Acceptor 接收到客户端TCP连接请求并处理完成后, 将新创建的SocketChannel 注册到 I/O线程池 (sub Reactor)。

Acceptor线程池仅负责客户端的登陆、握手、安全认证, 一旦链路建立成功, 就将链路注册到 I/O线程池 (sub Reactor), I/O线程池 (sub Reactor)负责后续的 I/O操作
在这里插入图片描述

4.1 服务端代码

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.SneakyThrows;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MultiReactor implements Runnable {

    Selector selector = null;
    ServerSocketChannel serverSocket;

    static ThreadPoolExecutor REACTOR_THREAD_POOL = new ThreadPoolExecutor(2, 16,
            10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(20),
            new ThreadFactoryBuilder().setNameFormat("REACTOR_THREAD_POOL-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy());

    /**
     * 构造函数,初始化多线程反应器。
     *
     * @param port 服务器监听端口。
     * @throws IOException 如果打开selector或服务器SocketChannel时发生错误。
     */
    public MultiReactor(int port) throws IOException {
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        serverSocket.configureBlocking(false);
        SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        sk.attach(new Acceptor(serverSocket));
    }

    /**
     * 构造函数,使用已有的selector。
     *
     * @param selector 已打开的selector。
     */
    public MultiReactor(Selector selector) throws IOException {
        this.selector = selector;
    }

    /**
     * 主运行方法,负责监听和分发事件。
     */
    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println(Thread.currentThread() + " select start");
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                System.out.println(Thread.currentThread() + " " + "selected:" + selected.size());
                for (SelectionKey selectionKey : selected) {
                    if (selectionKey.isReadable()) {
                        System.out.println("selectionKey read");
                    }
                    if (selectionKey.isWritable()) {
                        System.out.println("selectionKey write");
                    }
                    dispatch(selectionKey);
                }
                selected.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 分发已选择的事件到相应的处理程序。
     *
     * @param k 选择的关键。
     * @throws IOException 如果操作通道时发生错误。
     */
    void dispatch(SelectionKey k) throws IOException {
        Run r = (Run) (k.attachment());
        if (r != null)
            r.run();
    }

    /**
     * Acceptor类负责接受新的客户端连接,并将它们分配给子反应器处理。
     */
    class Acceptor implements Run {

        private final ServerSocketChannel listenSocketChannel;
        private final List<MultiReactor> subReactors = new ArrayList<>(ACCEPTOR_POOL_NUM);

        private static final int ACCEPTOR_POOL_NUM = 4;

        private final ThreadPoolExecutor ACCEPTOR_POOL = new ThreadPoolExecutor(ACCEPTOR_POOL_NUM, ACCEPTOR_POOL_NUM,
                10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(20),
                new ThreadFactoryBuilder().setNameFormat("ACCEPTOR_POOL-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy());

        Acceptor(ServerSocketChannel listenSocketChannel) throws IOException {
            this.listenSocketChannel = listenSocketChannel;
            for (int i = 0; i < ACCEPTOR_POOL_NUM; i++) {
                MultiReactor subReactor = new MultiReactor(Selector.open());
                subReactors.add(subReactor);
                ACCEPTOR_POOL.execute(subReactor);
            }
        }

        /**
         * 接受新的客户端连接,并分配给子反应器处理。
         */
        @Override
        public void run() {
            try {
                SocketChannel clientSocketChannel = listenSocketChannel.accept();
                // 设置为非阻塞
                // 任意选择一个从Reactor,让其监听连接的客户端的READ事件
                Optional<MultiReactor> anySubReactor = subReactors.stream().findAny();
                if (anySubReactor.isPresent() && clientSocketChannel != null) {
                    MultiReactor subReactor = anySubReactor.get();
                    System.out.println(Thread.currentThread() + ": "+ subReactor);
                    new Handler(subReactor.selector, clientSocketChannel);
                }

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

    /**
     * Handler类负责处理与客户端的通信,包括读取请求和发送响应。
     */
    final class Handler implements Run {
        final SocketChannel socket;
        final SelectionKey sk;
        ByteBuffer input = ByteBuffer.allocate(1024);
        ByteBuffer output = ByteBuffer.allocate(1024);

        Handler(Selector sel, SocketChannel c)
                throws IOException {
            socket = c;
            c.configureBlocking(false);
            sel.wakeup();
            sk = socket.register(sel, SelectionKey.OP_READ);
            sk.attach(this);
            sk.interestOps(SelectionKey.OP_READ);
        }

        /**
         * 检查输入缓冲区是否已完成读取。
         *
         * @return 如果输入缓冲区还有剩余,则返回true;否则返回false。
         */
        boolean inputIsComplete() {
            return input.hasRemaining();
        }

        /**
         * 检查输出缓冲区是否已完成发送。
         *
         * @return 如果输出缓冲区没有剩余,则返回true;否则返回false。
         */
        boolean outputIsComplete() {
            return !output.hasRemaining();
        }

        /**
         * 处理输入数据,将其解码并准备生成响应。
         *
         * @throws CharacterCodingException 如果字符编码发生错误。
         */
        void process() throws CharacterCodingException {
            // 否则,将缓冲区反转并打印读取的数据
            input.flip();
            String request = ByteBufferUtil.read(input);

            System.out.println(Thread.currentThread() + ": " + request);
            input.clear();
            System.out.println(input.toString());

            System.out.println(output.toString());
            output = ByteBufferUtil.read("你好: " + request);
        }

        /**
         * 读取客户端请求,并根据需要启动处理过程。
         *
         * @throws IOException 如果读取通道时发生错误。
         */
        @Override
        public void run() throws IOException {
            socket.read(input);
            if (inputIsComplete()) {
                REACTOR_THREAD_POOL.execute(new Processor(sk.selector()));
            }
        }

        /**
         * Sender类负责发送响应给客户端。
         */
        class Sender implements Run {
            private Selector selector;

            public Sender(Selector selector) {
                this.selector = selector;
            }

            /**
             * 发送输出缓冲区中的数据到客户端。
             *
             * @throws IOException 如果写入通道时发生错误。
             */
            public void run() throws IOException {
                System.out.println("start write");
                socket.write(output);
                if (outputIsComplete()) {
                    new Handler(this.selector, socket);
                }
            }
        }

        /**
         * Processor类负责处理请求并准备响应。
         */
        class Processor implements Runnable {

            private Selector selector;

            public Processor(Selector selector) {
                this.selector = selector;
            }

            @Override
            @SneakyThrows
            public void run() {
                process();
                sk.attach(new Sender(this.selector));
                sk.interestOps(SelectionKey.OP_WRITE);
                sk.selector().wakeup();
            }
        }
    }

    /**
     * Run接口定义了处理事件的运行时行为。
     */
    public interface Run {
        void run() throws IOException;
    }

    /**
     * 程序入口点。
     *
     * @param args 命令行参数。
     * @throws IOException 如果初始化反应器时发生错误。
     */
    public static void main(String[] args) throws IOException {
        MultiReactor reactor = new MultiReactor(6666);
        new Thread(reactor).start();
        while (true) ;
    }
}

4.2 运行截图

在这里插入图片描述
在这里插入图片描述

参考文献

  • Doug Lea 教授的课件 : https://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
  • Netty权威指南(第2版)李林锋 / 著
  • https://juejin.cn/post/7210375522512666679?searchId=20240612213218FE474007F2FADD0130AA

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1826431.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatTTS-WebUI测试页面项目

概述 分享可以一个专门为对话场景设计的文本转语音模型ChatTTS&#xff0c;例如LLM助手对话任务。它支持英文和中文两种语言。最大的模型使用了10万小时以上的中英文数据进行训练。在HuggingFace中开源的版本为4万小时训练且未SFT的版本. 该模型能够预测和控制细粒度的韵律特…

跪求大数据把我推给做投资交易的红薯!

在qq群里认识了君诺金融Juno Markets外汇交易平台的业务经理&#xff0c;平台上大剌剌的打出20%交易返现活动&#xff0c;一时听信了他们的话在该平台有开户入金做交易&#xff0c;做了这家平台的代理&#xff0c;然而君诺金融Juno Markets平台却不给佣金&#xff0c;我都是属于…

浏览器必备插件:最新Allow copy万能网页复制下载,解锁网页限制!

今天阿星给大家安利一个超级实用的小工具&#xff0c;专治那些“禁止复制”的网页文字。学生党、资料搜集狂人&#xff0c;你们有福了&#xff01; 想象一下&#xff0c;你在网上冲浪&#xff0c;突然遇到一篇干货满满的文章&#xff0c;正想复制下来慢慢品味&#xff0c;结果…

值传递和址传递

值传递 上面的代码是想要交换x&#xff0c;y的值&#xff0c;把x&#xff0c;y传递给swap函数之后&#xff0c;执行下面的操作&#xff1a; 在swap中a和b交换了&#xff0c;但是和x&#xff0c;y没有关系&#xff0c;所以x&#xff0c;y在main中不会变。 址传递 下面再看把x…

springcloud gateway转发websocket请求的404问题定位

一、问题 前端小程序通过springcloud gateway接入并访问后端的诸多微服务&#xff0c;几十个微服务相关功能均正常&#xff0c;只有小程序到后端推送服务的websocket连接建立不起来&#xff0c;使用whireshark抓包&#xff0c;发现在小程序通过 GET ws://192.168.6.100:8888/w…

Apple Intelligence 横空出世!它的独家秘诀在哪里?

在 WWDC 2024 大会上&#xff0c;苹果公司揭晓了自家的生成式 AI 项目——Apple Intelligence&#xff0c;其策略核心在于采用 ⌈ 更为聚焦的小型模型 ⌋ &#xff0c;而非盲目追求大模型的普遍趋势。横空出世的它究竟有什么过人之处&#xff1f;一文带你探究竟&#xff01;生成…

[DDR4] DDR1 ~ DDR4 发展史导论

依公知及经验整理&#xff0c;原创保护&#xff0c;禁止转载。 专栏 《深入理解DDR4》 内存和硬盘是电脑的左膀右臂&#xff0c; 挑起存储的大梁。因为内存的存取速度超凡地快&#xff0c; 但内存上的数据掉电又会丢失&#xff0c;一直其中缓存的作用&#xff0c;就像是我们的工…

2786. 访问数组中的位置使分数最大

这并不是一个难题,但是我确实在做题中得到了一些启发,所以记录一下 先讲一讲这个题目的做法: 首先不难想到这是一个dp问题,(由 i 可以跳到 j ) 而且应该不难, 要不然就不是medium了,doge 那么,暴力的dp就是: dp[j] max (dp[i] nums OR dp[j] dp[i] nums - x) , i<j, 前…

mongodb 集群安装

1. 配置域名 Server1&#xff1a; OS version: CentOS Linux release 8.5.2111 hostnamectl --static set-hostname mongo01 vi /etc/sysconfig/network # Created by anaconda hostnamemong01 echo "192.168.88.20 mong1 mongo01.com mongo02.com" >> /…

【笔记】【矩阵的二分】668. 乘法表中第k小的数

力扣链接&#xff1a;题目 参考地址&#xff1a;参考 思路&#xff1a;二分查找 把矩阵想象成一维的已排好序的数组&#xff0c;用二分法找第k小的数字。 假设m行n列&#xff0c;则对应一维下标范围是从1到mn&#xff0c;初始&#xff1a; l1; rmn; mid(lr)/2 设mid在第i行&a…

【C++11】第一部分(一万六千多字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 C11简介 统一的列表初始化 &#xff5b;&#xff5d;初始化 std::initializer_list 声明 auto decltype 右值引用和移动语义 左值引用和右值引用 左值引…

C#实现WMI获取硬盘参数

文章目录 背景涉及框架及库WMI查询小工具参数解释U盘移动硬盘本机设备 总结 背景 因为需求需要涉及获取硬盘的SN参数&#xff0c;但是又不想要获取到U盘或移动硬盘设备的SN&#xff0c;所以就浅浅的研究了一下。 以下就是我目前发现的一些参数的作用&#xff0c;够我用了。。。…

QT QFileDialog文件选择对话框

QT QFileDialog文件选择对话框 选择txt或者cpp文件&#xff0c;读取内容并显示 参考&#xff1a; QT写入文件与读取文件内容_qt往一个文件写东西-CSDN博客 #include "QtFilePreview.h" #include "qfiledialog.h" #include "qfile.h" #includ…

【记录】ChatGLM3-6B大模型部署、微调(二):微调

前言 上文记录了ChatGLM3-6B大模型本地化部署过程&#xff0c;本次对模型进行微调&#xff0c;目的是修改模型自我认知。采用官方推荐微调框架&#xff1a;LLaMA-Factory 安装LLaMA-Factory # 克隆项目 git clone https://github.com/hiyouga/LLaMA-Factory.git 安装依赖 # 安装…

Android入门第68天-自动更新/升级怎么做(生产级实例)

开篇 今天我们进入第68讲。 在第60天左右其实很多同学们已经进入了APP应用开发了,因为60天内容足以让大家踏上正实的Android开发生涯。 随着开发的深入,我们发觉日常工作中无非就是一些组件的嵌套、合理应用。当代码迭代、功能迭代越来越频繁后我们面临着另一个问题,即:…

Flutter项目,Xcode15, 编译正常,但archive报错

错误提示 PhaseScriptExecution [CP]\ Embed\ Pods\ Frameworks /Users/目录/Developer/Xcode/DerivedData/Runner-brgnkruocugbipaswyuwsjsnqkzm/Build/Intermediates.noindex/ArchiveIntermediates/Runner/IntermediateBuildFilesPath/Runner.build/Release-iphoneos/Runner…

世界酒中国菜全球组委会发布2024年度VIS视觉融合潘通柔和桃色调

世界酒中国菜全球组委会发布2024年度VIS视觉 融合潘通柔和桃色调引领全球风尚 2023年12月7日&#xff0c;国际色彩权威机构&#xff08;潘通&#xff09;Pantone公司发布了2024年度代表色&#xff1a;Peach Fuzz&#xff08;PANTONE 13-1023&#xff09;柔和桃色调&#xff0…

Mybatis动态sql标签

动态SQL标签简介: MyBatis的一个强大的特性之一通常是它的动态SQL能力。如果你有使用JDBC或其他相似框架的经验,你就明白条件地串联SQL字符串在一起是多么的痛苦,确保不能忘了空格或在列表的最后省略逗号。动态SQL可以彻底处理这种痛苦。 Mybatis中实现动态sql的标签有&#x…

重生之 SpringBoot3 入门保姆级学习(22、场景整合 远程调用阿里云天气服务获取天气)

重生之 SpringBoot3 入门保姆级学习&#xff08;22、场景整合 远程调用阿里云天气服务获取天气&#xff09; 6.3 远程调用三方 API 6.3 远程调用三方 API 1、创建项目时需要选择 Spring Reactive Web 2、0元购买天气服务 API &#xff0c;我这里买了是生产中没有购买的话会显示…

洗地机哪个品牌最好用?洗地机品牌排行榜前十名

如今&#xff0c;备受大家喜爱的清洁家电莫过于洗地机啦&#xff0c;洗地机它能够一次性完成吸尘、拖地、洗地的工作&#xff0c;节省了大量的时间和精力。但是近年来&#xff0c;清洁领域的品牌基本都开始做洗地机了&#xff0c;各大品牌各式各样的洗地机可谓来势汹汹&#xf…