打牢Netty基础,一文带你彻底搞懂Java的Nio多路复用!

一、nio简介

        当涉及到高效的网络编程时,Java的NIO(New Input/Output)多路复用是一个重要的概念。NIO多路复用允许单个线程管理多个连接(Socket通道),通过选择器(Selector)同时监控多个通道的状态,从而实现高效的网络通信。在传统的Java I/O模型中,每个连接都需要一个独立的线程来处理,这会导致线程的创建和销毁开销较大,同时线程之间的切换也会消耗大量的资源。而NIO多路复用通过使用选择器(Selector)来监控多个通道的状态,将连接的管理交由一个单独的线程来处理,从而大大减少了线程的数量。选择器(Selector)是NIO多路复用的核心组件,它会不断地轮询注册在其上的通道(Channel),并且只选择处于就绪状态的通道进行处理。通道可以被注册到选择器上,以便在通道准备好进行读、写或接受连接时得到通知。通过NIO多路复用,我们可以使用单个线程同时管理多个连接,而不需要为每个连接创建一个独立的线程。这种方式可以极大地提高系统的并发处理能力,减少线程的开销,并且减少了线程切换的消耗

java传统BIO 

再java传统的Bio网络编程中,一个线程处理一个连接,如果连接发生阻塞的话,线程也会随之一起陷入阻塞,这样会在一定程度下降低网络通信的效率。

java nio多路复用

在NIO中,线程会操作一个选择器(selector),而selector将会调度其内部注册的channel,channel将会处理各种各样的操作,当某个操作陷入阻塞时,selector将会进行调度,将其他channel的操作调度由线程处理,这种将多个channel交由selector在一个线程中调度处理,这种架构处理方式就是多路复用。通过多路复用,可以在极大程度上提升网络数据传输的效率。

接下来,笔者将详细介绍java nio中的三大组件,Buffer,Channel和Selector。

二、nio三大组件

1.ByteBuffer

Java中的ByteBuffer类是NIO包中的一个类,用于对字节数据进行高效操作。ByteBuffer 类提供了一种灵活的方式来操作字节数据,可以读取、写入、复制、截取等操作,主要用于实现数据的缓存

 (1)ByteBuffer的创建

ByteBuffer是一个抽象类,所以无法直接使用new关键字来创建对象。创建ByteBuffer对象主要使用ByteBuffer类中的两个静态方法来创建,ByteBuffer.allocate(int capacity)和ByteBuffer.allocateDirect(int capacity)

//导入包
import java.nio.ByteBuffer;

public class MainTest {
    public static void main(String ... args) {

        //使用allocate开辟空间,其内存分配由jvm负责,受到jvm堆内存机制的影响
        ByteBuffer buffer1 = ByteBuffer.allocate(10);

        //方法会在本地内存中创建一个新的ByteBuffer对象,
        //这种分配方式是在操作系统的本地内存中进行的,不受 Java 堆内存管理机制的影响
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);
    }
}

 (2)position和limit

        在 Java 中的 ByteBuffer 类中,position和 limit是ByteBuffer中的两个重要的属性,用于控制读写操作的位置和范围。

        position 表示当前的读写位置,在进行写操作时,数据将会被写入到 position 所指定的位置;在进行读操作时,数据将会从 position 所指定的位置开始读取。初始时,position 通常为 0。

       limit 表示在读取操作时的数据范围限制,即可以读取的数据范围从 0 到 limit-1。在写入数据时,limit 通常会被设置为 ByteBuffer 的容量,表示可以写入的最大数据量

        通过控制 position 和 limit,可以有效地进行读写操作,确保不会读取或写入超出范围的数据。 在使用 ByteBuffer 时,通常会结合使用其他方法来处理 position 和 limit,比如 flip 方法用于切换读写模式,rewind 方法用于重置 position,clear 方法用于清空并重置 position 和 limit 等。

当向ByteBuffer中写入数据时: 

当从ByteBuffer中读取数据时:

(3)flip()、rewind()、clear()

 flip():将缓冲区从写模式切换到读模式,重置 position 为 0。

rewind():将 position 设置为 0,限制保持不变,可以重新读取缓冲区中的数据。

clear():清空缓冲区,重置 position 和 limit 为初始位置,数据不会被清除,但处于“已遗忘”状态。

 为了方便理解,ByteBuffer的其余介绍及用法将会结合Channel一起介绍。

2.Channel

在 Java NIO 中,一个重要的概念是 Channel(通道),它代表了与数据源或数据目标之间

的连接,可以进行读取和写入数据。 Channel 和传统的流相比,具有以下特点:

1.双向性:Channel 既可以用于读取数据,也可以用于写入数据,而流是单向的。

2.非阻塞:Channel 支持非阻塞模式,可以进行异步 I/O 操作,提高了程序的性能。

3.可选择性:Channel 可以注册到 Selector上,实现多路复用,可以同时管理多个 Channel 的 I/O 操作。

Java 中常用的 Channel 类型包括:

1.FileChannel:用于文件的读写操作。

2.SocketChannel:用于 TCP 网络通信中的客户端。

3.ServerSocketChannel:用于 TCP 网络通信中的服务器端。

4.DatagramChannel:用于 UDP 网络通信。

通过 Channel,Java NIO 提供了一种更灵活、高效的 I/O 编程方式,特别适用于网络通信和文件操作等场景。

 这里使用FileChannel结合ByteBuffer来简单实现一下读取文本文件的功能步骤如下:

1)首先获取文件传输的channel,可以通过FileInputStream来获取

2)创建ByteBuffer缓冲区,大小为4字节,方便演示当缓冲区大小不足时,如何循环读取文件数据

3)调用channel的read方法循环读取channel中的字节流,并放进ByteBuffer中。channel的read方法返回读取数据的长度,当没有读取到数据时会返回-1,可以用于作为循环终止条件。

4)循环读取ByteBuffer中缓存的数据,调用ByteBuffer的hasRemaining方法来判断是否还有剩余数据没有读取,作为循环终止的条件。

public class Test {
    public static void main(String ... args) throws Exception {
        //获取文件传输通道(channel)
        FileChannel channel = new FileInputStream("F:\\java\\java_work\\Netty\\src\\main\\resources\\data.txt").getChannel();
        
        //创建ByteBuffer缓冲数组,大小为4字节
        ByteBuffer buffer = ByteBuffer.allocate(4);

        //读取数据放入buffer并判断channel中的数据是否读完
        while (channel.read(buffer) != -1) {

            //当要读取ByteBuffer中的数据是,一定切记要调用flip切换为读模式
            buffer.flip();

            //判断buffer中是否还有数据未读取
            while (buffer.hasRemaining()) {

                //从buffer的position指针的位置下获取一个字节数据,并向后移动position指针
                byte b = buffer.get();

                //打印
                System.out.print((char) b);
            }

            //读取完毕后要清空buffer
            buffer.clear();
        }
    }
}

当然,这样单个字节读取打印遇到中文是会乱码的,如果buffer缓冲空间足够大,且需要打印中文等字符,可以使用java的StandardCharsets类来进行解码。

public class Test {
    public static void main(String ... args) throws Exception {
        FileChannel channel = new FileInputStream("F:\\java\\java_work\\Netty\\src\\main\\resources\\data.txt").getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(256);
        channel.read(buffer);

        //切换为读模式
        buffer.flip();

        //使用StandCharsets中的静态工具类来对其进行解码并打印
        System.out.println(StandardCharsets.UTF_8.decode(buffer));
    }
}

3.Selector

Selector是java nio编程中的核心组件,是nio多路复用的高效的 I/O 监听器。它用于监听多个通道的事件,例如读、写和连接就绪等,从而在单个线程中处理多个通道的 I/O 操作。这种机制可以大大减少线程的数量,提高系统的吞吐量和响应性能。

(1)Selector的获取 

通过Selector的open方法,即可获取Selector类 

Selector selector = Selector.open();

(2)channel的注册

获取到了Selector类之后,需要向其注册channel,而selector将会监听和调度这些channel的事件

//获取服务器通信的channel
ServerSocketChannel ssc = ServerSocketChannel.open();

//将其设置为非阻塞模式
ssc.configureBlocking(false);

//为channel绑定监听端口
ssc.bind(new InetSocketAddress(8080));

//获取selector类
Selector selector = Selector.open();

//调用channel的register方法来注册进指定的selector中,并声明该channel对什么事件感兴趣
//当该事件发生时,selector将会调度该channel进行处理,这里的事件为accept事件
/*该代码也可等同于
    ssc.register(selector, SelectionKey.OP_ACCEPT);
*/
ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);

以上代码使用的channel是用于服务器通信的,需将其设置为非阻塞模式,并且注册进selector中,selector中可以注册多个channel,并且需要声明该channel感兴趣的事件,方便selector调度。接下来,我们将完善以上代码,实现Server和Client之间的Nio通信。

三、实现简单的客户端服务器通信

1.Server

 我们从channel注册部分的代码开始完善。首先,因为服务器需要持续处理请求,所以需要写入一个死循环,然后在循环中调用selector的select()方法,调用该方法将会陷入阻塞,直到事件发生时,代码才会往下执行

public class Test {
    public static void main(String ... args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //循环监听
        while (true) {
            //阻塞监听事件,事件发生时向下执行
            selector.select();
            
        }
    }
}

当事件触发时,获取selector中注册的channel的key的列表对每个channel进行处理,需要注意的是,这里需要使用迭代器,而不能简单的使用循环,因为当处理完一个channel事件时,需要立即把该channel从SelectionKey列表中移除,否则可能会重复处理造成异常。

public class Test {
    public static void main(String ... args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //循环监听
        while (true) {
            //阻塞监听事件,事件发生时向下执行
            selector.select();

            //获取selector中channel的SelectionKey列表迭代器,依次进行处理
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            
            //循环处理迭代器的元素
            while (iter.hasNext()) {
                
                //获取selectionKey
                SelectionKey key = iter.next();

                //移除当前Key,防止重复处理
                iter.remove();

                //...
            }
            
        }
    }
}

 对触发了事件的channel需要对其事件进行判断并进行处理,我们先处理触发了accep事件的逻辑。当触发了accept时,先通过key获取channel并强转为需要的channel类型,因为我们之前注册通道时,对accept感兴趣的channel为ServerSocketChannel,所以需要强转为该类型,然后调用其accept方法建立连接,并会返回SocketChannel,需要将该channel也注册进selector中,并且设置感兴趣的事件为read,通过该channel进行事件的处理。

public class Test {
    public static void main(String ... args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //循环监听
        while (true) {
            //阻塞监听事件,事件发生时向下执行
            selector.select();

            //获取selector中channel的SelectionKey列表迭代器,依次进行处理
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            
            //循环处理迭代器的元素
            while (iter.hasNext()) {
                
                //获取selectionKey
                SelectionKey key = iter.next();

                //移除当前Key,防止重复处理
                iter.remove();
                

                //判断该channel感兴趣的事件是否是accept
                if (key.isAcceptable()) {
                    
                    //通过key获取channel并强转为需要的类型
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

                    //通过accept方法建立连接,并返回SocketChannel
                    SocketChannel accept = serverSocketChannel.accept();

                    //将该channel也设置为非阻塞模式
                    accept.configureBlocking(false);

                    //将该channel注册进selector中,并指明感兴趣的事件为读事件
                    accept.register(selector, SelectionKey.OP_READ);
                }
            }
            
        }
    }
}

 然后处理读事件,同样通过key获取并强转为指定的channel,创建ByteBuffer缓冲数组存放数据,调用channel的read方法来向buffer中读取数据,读取完毕后再向客户端写数据,这里需要注意的是,为了防止客户端无法一次性读取完毕所有的服务端发送的数据,所以可能需要多次发送(即多次向SocketChannel中写数据),所以我们需要把没有读取完的buffer挂载到key上,并再为其指定一个写事件,用于下次循环时继续处理该写事件。需要注意的时,如果客户端中途断开的话,会触发异常,所以这里需要使用try-catch,当异常触发时,需要调用key的cancel方法,否则selector会重复处理。

public class Test {
    public static void main(String ... args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //循环监听
        while (true) {
            //阻塞监听事件,事件发生时向下执行
            selector.select();

            //获取selector中channel的SelectionKey列表迭代器,依次进行处理
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            
            //循环处理迭代器的元素
            while (iter.hasNext()) {
                
                //获取selectionKey
                SelectionKey key = iter.next();

                //移除当前Key,防止重复处理
                iter.remove();
                

                //判断该channel感兴趣的事件是否是accept
                if (key.isAcceptable()) {
                    
                    //通过key获取channel并强转为需要的类型
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

                    //通过accept方法建立连接,并返回SocketChannel
                    SocketChannel accept = serverSocketChannel.accept();

                    //将该channel也设置为非阻塞模式
                    accept.configureBlocking(false);

                    //将该channel注册进selector中,并指明感兴趣的事件为读事件
                    accept.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    try {
                        //获取ByteBuffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);

                        //获取channel
                        SocketChannel sc = (SocketChannel) key.channel();

                        //从channel中读取数据
                        int len = sc.read(buffer);
                    
                        //如果没有数据读取到就跳过
                        if (len <= 0) continue;

                        //切换为读模式
                        buffer.flip();
                        
                        //对数据进行解码并打印
                        System.out.println(StandardCharsets.UTF_8.decode(buffer));

                        //向客户端发送的数据
                        String content = "message from server....";

                        //对数据编码为字节数组
                        ByteBuffer writeBuffer = StandardCharsets.UTF_8.encode(content);

                        //写入channel
                        sc.write(buffer);

                        //如果客户端一次性无法读取完毕,需将其挂载到key上,并增加写事件,用于下次处理
                        if (writeBuffer.hasRemaining()) {

                            //将buffer挂载到key上,下次写事件处理
                            key.attach(writeBuffer);

                            //增加写事件的监听
                            key.interestOps(key.interestOps() + SelectionKey.OP_WRITE);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();

                        //如果触发异常需调用该方法,否则会进入重复处理
                        key.cancel();
                    }
                } 
            }
            
        }
    }
}

最后,我们处理写事件的逻辑。首先获取key上挂载的ByteBuffer对其继续处理,判断ByteBuffer是否还有剩余数据,如果已经读取完毕则将buffer从key上移除,然后去除key上的读事件,如果没有写入完毕,则继续写入。

public class Test {
    public static void main(String ... args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, 0).interestOps(SelectionKey.OP_ACCEPT);
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        //循环监听
        while (true) {
            //阻塞监听事件,事件发生时向下执行
            selector.select();

            //获取selector中channel的SelectionKey列表迭代器,依次进行处理
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            
            //循环处理迭代器的元素
            while (iter.hasNext()) {
                
                //获取selectionKey
                SelectionKey key = iter.next();

                //移除当前Key,防止重复处理
                iter.remove();
                

                //判断该channel感兴趣的事件是否是accept
                if (key.isAcceptable()) {
                    
                    //通过key获取channel并强转为需要的类型
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

                    //通过accept方法建立连接,并返回SocketChannel
                    SocketChannel accept = serverSocketChannel.accept();

                    //将该channel也设置为非阻塞模式
                    accept.configureBlocking(false);

                    //将该channel注册进selector中,并指明感兴趣的事件为读事件
                    accept.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    try {
                        //获取ByteBuffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);

                        //获取channel
                        SocketChannel sc = (SocketChannel) key.channel();

                        //从channel中读取数据
                        int len = sc.read(buffer);
                    
                        //如果没有数据读取到就跳过
                        if (len <= 0) continue;

                        //切换为读模式
                        buffer.flip();
                        
                        //对数据进行解码并打印
                        System.out.println(StandardCharsets.UTF_8.decode(buffer));

                        //向客户端发送的数据
                        String content = "message from server....";

                        //对数据编码为字节数组
                        ByteBuffer writeBuffer = StandardCharsets.UTF_8.encode(content);

                        //写入channel
                        sc.write(buffer);

                        //如果客户端一次性无法读取完毕,需将其挂载到key上,并增加写事件,用于下次处理
                        if (writeBuffer.hasRemaining()) {

                            //将buffer挂载到key上,下次写事件处理
                            key.attach(writeBuffer);

                            //增加写事件的监听
                            key.interestOps(key.interestOps() + SelectionKey.OP_WRITE);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();

                        //如果触发异常需调用该方法,否则会进入重复处理
                        key.cancel();
                    }
                } else if (key.isWritable()) {

                    //获取key上挂载的buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    
                    //如果没有剩余数据
                    if (!buffer.hasRemaining()) {

                        //移除key上挂载的buffer
                        key.attach(null);

                        //去除写事件
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
                        continue;
                    }
                    socketChannel.write(buffer);
                } 
            }
            
        }
    }
}

至此,服务端代码完毕,接下来处理客户端的逻辑。

2.Client

这里的客户端我们做简单处理,只发送单条数据,所以不必使用Selector。

public class Client {
    public static void main(String ... args) throws IOException {

        //创建通道
        SocketChannel sc = SocketChannel.open();

        //建立连接
        sc.connect(new InetSocketAddress("localhost", 8080));

        //向服务器端发送数据
        sc.write(StandardCharsets.UTF_8.encode("message from client"));

        //接受服务器端数据的缓存(这里我们故意将大小开辟的小一点,验证服务器端代码能正常处理
        //客户端缓冲区过小的问题
        ByteBuffer readBuffer = ByteBuffer.allocate(2);
        while (true) {
    
            //读取数据
            int len = sc.read(readBuffer);

            //若读取完毕则关闭连接
            if (len <= 0) {
                sc.close();
                break;
            }

            //将buffer切换为读模式
            readBuffer.flip();

            //打印读取到的数据
            System.out.print(StandardCharsets.UTF_8.decode(readBuffer));

            //清空buffer,确保下次读入数据正常
            readBuffer.clear();
        }
    }
}

3.测试

先启动服务器,再启动客户端,最终两边结果正常打印,代码正常。

四、总结

nio多路复用在极大程度上提高了网络通信的效率,nio主要由Channel, Buffer,Selector三大组件组成,其作用分别为,数据传输的通道,数据缓存,调度Channel,作为Netty框架的底层实现,只有掌握了基本的java nio编程,才能为之后Netty的学习打下坚实的基础

  • 41
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是基于nio实现多路复用的示例代码: ```java 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.util.Iterator; import java.util.Set; public class NioMultiplexingServer { private Selector selector; public void init(int port) throws IOException { // 创建selector selector = Selector.open(); // 创建ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); // 绑定端口 serverSocketChannel.socket().bind(new InetSocketAddress(port)); // 将ServerSocketChannel注册到selector上,并监听OP_ACCEPT事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("Server started at port " + port); // 循环处理selector上的事件 while (true) { // 阻塞等待事件发生 int readyChannels = selector.select(); if (readyChannels == 0) { continue; } // 处理事件 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // 处理OP_ACCEPT事件 handleAccept(key); } else if (key.isReadable()) { // 处理OP_READ事件 handleRead(key); } keyIterator.remove(); } } } private void handleAccept(SelectionKey key) throws IOException { // 获取ServerSocketChannel ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); // 接受连接 SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); // 将SocketChannel注册到selector上,并监听OP_READ事件 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("Connection accepted: " + socketChannel.getRemoteAddress()); } private void handleRead(SelectionKey key) throws IOException { // 获取SocketChannel SocketChannel socketChannel = (SocketChannel) key.channel(); // 读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = socketChannel.read(buffer); if (bytesRead == -1) { // 连接已关闭 socketChannel.close(); System.out.println("Connection closed: " + socketChannel.getRemoteAddress()); return; } // 处理数据 buffer.flip(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); String message = new String(bytes); System.out.println("Message received from " + socketChannel.getRemoteAddress() + ": " + message); // 回复客户端 ByteBuffer responseBuffer = ByteBuffer.wrap(("Received message: " + message).getBytes()); socketChannel.write(responseBuffer); } public static void main(String[] args) throws IOException { NioMultiplexingServer server = new NioMultiplexingServer(); server.init(8080); } } ``` 上述代码实现了一个简单的nio多路复用服务器,其中: 1. 初始化方法中创建了一个Selector和一个ServerSocketChannel,并将ServerSocketChannel注册到selector上,监听OP_ACCEPT事件; 2. 循环处理selector上的事件,阻塞等待事件发生; 3. 对于每个事件,根据事件类型进行处理,处理完毕后从selector的selectedKeys集合中移除该事件。 在handleAccept和handleRead方法中,分别处理了OP_ACCEPT和OP_READ事件: 1. handleAccept方法中,获取ServerSocketChannel并接受连接,将SocketChannel注册到selector上,监听OP_READ事件; 2. handleRead方法中,获取SocketChannel并读取数据,处理数据并回复客户端。 通过以上代码,我们实现了一个简单的nio多路复用服务器,可以同时处理多个连接请求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值