NIO之Selector的使用

一、Selector

A multiplexor of {@link SelectableChannel} objects.

参照Java doc中Selector描述的第一句话,Selector的作用是Java NIO中管理一组多路复用的SelectableChannel对象,并能够识别通道是否为诸如读写事件做好准备的组件。

selector 版
selector
thread
channel
channel
channel

使用Selector的好处:

  • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
  • 让这个线程能够被充分利用
  • 节约了线程的数量
  • 减少了线程上下文切换

二、读事件

1、Selector的使用

(1)创建Selector

// 创建Selector
Selector selector = Selectoe.open();

(2)绑定 Channel 事件

也称之为注册事件,绑定的事件 selector 才会关心

// 设置channel的模式为非阻塞模式
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);

下面是注册方法的介绍
在这里插入图片描述

  • channel 必须工作在非阻塞模式
  • FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
  • 绑定的事件类型可以有
    • connect - 客户端连接成功时触发
    • accept - 服务器端成功接受连接时触发
    • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

(3)监听Channel事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

2、处理完key需要手动移除

下面我们模拟使用selector来进行服务端和客户端的事件处理行为。
代码中注释都比较详细,大家仔细阅读反复推敲即可。

(1)Server与Client

客户端代码为

public class Client {
    
    
    public static void main(String[] args) throws IOException {
    
    
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",8080));
        // debug模式下
        // 执行表达式  socketChannel.write(Charset.defaultCharset().encode("hello"))
        // 可以看到运行的结果
        System.out.println("waiting ....");
    }
}

服务器端代码为

@Slf4j
public class Server {
    
    
    public static void main(String[] args) throws IOException {
    
    
        // 1、创建selector,管理多个channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        // 2、建立selector和channel的联系(注册时间)
        //SelectorKey就是将来事件发生时,通过它可以得知事件类型和哪个channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
        sscKey.interestOps(SelectionKey.OP_ACCEPT); // 监控请求连接事件
        log.debug("register key : {}", sscKey);

        ssc.bind(new InetSocketAddress(8080));
        while (true) {
    
    
            // 3、select方法,没有事件发生,线程阻塞,有事件线程会恢复运行
            // select方法在事件未处理时,不会阻塞,事件一直会被反复加入key
            // 事件发生时,要么处理,要么取消,不能置之不理
            selector.select();
            // 4、处理事件,selectorKey内部包含了所有发生的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey key = iterator.next();
                log.debug("key : {}", key);
                // 区分事件类型,给予不同的处理
                if (key.isAcceptable()) {
    
    
                    // 处理连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    log.debug("sc :{}", sc);
                    // 如果我们不想处理一个事件,我们可以使用cancel()取消事件处理
                    //key.cancel();
                    SelectionKey scKey = sc.register(selector, 0, null);// 注册事件
                    scKey.interestOps(SelectionKey.OP_READ); //关注读取事件
                } else if (key.isReadable()) {
    
    
                    // 处理读取事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    channel.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugRead(buffer);
                }

            }
        }
    }
}

然后我们以正常模式启动服务端Server,控制台打印信息如下:

09:40:20 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0

控制台输出的就是我们代码中给ServerSocketChannel注册的Selector,它监控的是SelectionKey.OP_ACCEPT(请求连接)事件。

接着我们以debug模式启动客户端Client,发现控制台输出信息如下:

09:40:20 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0
09:43:03 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
09:43:03 [DEBUG] [main] c.e.n.c.t.Server - sc :java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1146]

可以看到,两次注册得到的SelectionKey是同一个,只是监听的事件不同,后面注册的添加了监听读请求的事件。
在这里插入图片描述
我们使用客户端Client的debug工具来发送一条数据,并且查看相应的结果,如下:
在这里插入图片描述
执行这样一条表达式:

socketChannel.write(Charset.defaultCharset().encode("hello"))

在这里插入图片描述
发现控制台报错,打印如下信息:

09:46:39 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
Exception in thread "main" java.lang.NullPointerException
	at com.example.nettydemo.c4.test.Server.main(Server.java:42)

这是怎么一回事呢?

(2)异常分析

我们重新过一遍我们写过的代码,首先我们看第一行代码:

		// 1、创建selector,管理多个channel
        Selector selector = Selector.open();

这行代码执行后,我们会创建一个Selector对象,这个对象的内部是有一个集合用来存放SelectionKey的,初始化的时候集合为空,下面是其内部的一个方法:
在这里插入图片描述
那么什么时候这个集合会被填充内容呢?

当执行到下面这行代码的时候,也就是将channel注册到selector的时候,就会把生成的sscKey放入到Selector的集合中。

		// 2、建立selector和channel的联系(注册时间)
        //SelectorKey就是将来事件发生时,通过它可以得知事件类型和哪个channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);

这个sscKey管理的就是ssc的请求连接事件,当事件发生时,服务端就会做出响应。

然后程序就会一直运行到select()方法这里,因为刚开始没有任何事件,所以我们的select()方法就会阻塞线程,进入等待状态。

	  // 3、select方法,没有事件发生,线程阻塞,有事件线程会恢复运行
     // select方法在事件未处理时,不会阻塞,事件一直会被反复加入key
     // 事件发生时,要么处理,要么取消,不能置之不理
     selector.select();

当有新的客户端Client请求连接时,服务端Server就会进行处理,那么在这个处理过程中又发生了什么事件呢?

这个时候Selector又会创建一个新的集合,这个集合中存放的是我们的selectedKeys,当新的会话进行请求连接时,这个selectedKeys就会被填充到集合中,并且将之前集合中的sscKey也加入到该集合中。
也就是说,sscKey的对象是同一个对象,却同时存在两个集合中。

注意:Selector会在发生事件后,新的请求连接的时候,新创建的selectedKeys只会加入到该集合中,但是不会从该集合中进行移除。

然后就进入了迭代器的循环,处理:

		// 4、处理事件,selectorKey内部包含了所有发生的事件
     	Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

如果是请求连接的事件,执行完accept()方法之后,那么该事件被处理过后,事件被标记已被处理过。

紧接着我们又注册了一个selector,并且监控的是读取事件,这个事件的SelectionKey 也会像前面的执行过程一样加入到相应的集合中,并且监管相应的channel。

		SelectionKey scKey = sc.register(selector, 0, null);// 注册事件
		scKey.interestOps(SelectionKey.OP_READ); //关注读取事件

当我们循环发生事件并且进行注册的时候,循环遍历取出的key就有可能是之前已经被处理过的事件的SelectionKey,所以在建立连接的时候,也就是执行accpet()方法的时候就会返回null,这时候就会报空指针异常,这也就是为什么我们在客户端Client发送消息的时候,程序就会报错。

解决方法:

处理完一个SelectionKey,必须手动将该key进行移除。
在这里插入图片描述

3、处理客户端断开问题

同样是上面案例的服务端Server和客户端Client,我们模拟客户端断开的这种情况。

(1)强制断开

首先我们还是以正常模式启动服务端Server,以debug模式启动客户端Client,然后我们将Client强制关掉,会发现控制台打印如下信息:

10:26:46 [DEBUG] [main] c.e.n.c.t.Server - register key : channel=sun.nio.ch.ServerSocketChannelImpl[unbound], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=0
10:26:50 [DEBUG] [main] c.e.n.c.t.Server - key : channel=sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=16, readyOps=16
10:26:50 [DEBUG] [main] c.e.n.c.t.Server - sc :java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1475]
10:26:58 [DEBUG] [main] c.e.n.c.t.Server - key : channel=java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1475], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=1, readyOps=1
Exception in thread "main" java.io.IOException: 远程主机强迫关闭了一个现有的连接。
	at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
	at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
	at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:276)
	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:245)
	at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:223)
	at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:358)
	at com.example.nettydemo.c4.test.Server.main(Server.java:54)

Process finished with exit code 1

解决方法,我们需要处理这个异常,将客户端的key进行一个移除。
try-catch之后,程序并不会抛出异常而终止,而是会在控制台打印信息
在这里插入图片描述

(2)正常断开

正常断开的话,我们就需要使用debug的小工具执行下面的代码了,让程序正常执行结束。

socketChannel.close()

在这里插入图片描述

然后对程序进行判断处理,代码如下:
在这里插入图片描述
正常断开之后,触发了一次读事件,控制台打印信息如下:

11:35:20 [DEBUG] [main] c.e.n.c.t.Server - key : channel=java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:1256], selector=sun.nio.ch.WindowsSelectorImpl@67d48005, interestOps=1, readyOps=1

4、消息边界问题

(1)情景分析

其实上述代码中还存在着消息边界问题,比如我们进行测试:

测试一: 发送hi,并在控制台打印输出
测试二:发送你好,并在控制台打印输出

为了方便显示效果,我们将缓冲区的大小设置的小一点,方便我们进行测试。

 ByteBuffer buffer = ByteBuffer.allocate(4);

我们以正常模式启动服务器端Server,以debug模式启动客户端Client,然后从客户端向服务端发送一条消息hi

socketChannel.write(Charset.defaultCharset().encode("hi"))

在这里插入图片描述
服务器端正常打印输出:
在这里插入图片描述
我们再进行测试二,向服务端发送数据你好

socketChannel.write(Charset.defaultCharset().encode("你好"))

这时看到控制台就发现出现了问题:
在这里插入图片描述

字被拆分成了两段数据,并且还没有能够正常合并,这就是我们所说的数据边界问题。

(2)处理消息的边界

① 如何处理消息的边界

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaHI2hC9-1665995083570)(img/0023.png)]

  • 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  • 另一种思路是按分隔符拆分,缺点是效率低
  • TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    • Http 1.1 是 TLV 格式
    • Http 2.0 是 LTV 格式

在这里插入图片描述

客户端1 服务器 ByteBuffer1 ByteBuffer2 发送 01234567890abcdef3333\r 第一次 read 存入 01234567890abcdef 扩容 拷贝 01234567890abcdef 第二次 read 存入 3333\r 01234567890abcdef3333\r 客户端1 服务器 ByteBuffer1 ByteBuffer2

② 案例改进 – 附件与扩容

这里我们使用第二种思路来进行优化,第三种思路后续学习Netty的时候会讲到。
我们使用attachment附件,就相当于channel的一个附属体,每个单独的channel与自己的附件一一对应。

服务器端

private static void split(ByteBuffer source) {
    
    
    source.flip();
    for (int i = 0; i < source.limit(); i++) {
    
    
        // 找到一条完整消息
        if (source.get(i) == '\n') {
    
    
            int length = i + 1 - source.position();
            // 把这条完整消息存入新的 ByteBuffer
            ByteBuffer target = ByteBuffer.allocate(length);
            // 从 source 读,向 target 写
            for (int j = 0; j < length; j++) {
    
    
                target.put(source.get());
            }
            debugAll(target);
        }
    }
    source.compact(); // 0123456789abcdef  position 16 limit 16
}

public static void main(String[] args) throws IOException {
    
    
    // 1. 创建 selector, 管理多个 channel
    Selector selector = Selector.open();
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    // 2. 建立 selector 和 channel 的联系(注册)
    // SelectionKey 就是将来事件发生后,通过它可以知道事件和哪个channel的事件
    SelectionKey sscKey = ssc.register(selector, 0, null);
    // key 只关注 accept 事件
    sscKey.interestOps(SelectionKey.OP_ACCEPT);
    log.debug("sscKey:{}", sscKey);
    ssc.bind(new InetSocketAddress(8080));
    while (true) {
    
    
        // 3. select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
        // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
        selector.select();
        // 4. 处理事件, selectedKeys 内部包含了所有发生的事件
        Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); // accept, read
        while (iter.hasNext()) {
    
    
            SelectionKey key = iter.next();
            // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
            iter.remove();
            log.debug("key: {}", key);
            // 5. 区分事件类型
            if (key.isAcceptable()) {
    
     // 如果是 accept
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                SocketChannel sc = channel.accept();
                sc.configureBlocking(false);
                ByteBuffer buffer = ByteBuffer.allocate(16); // attachment
                // 将一个 byteBuffer 作为附件关联到 selectionKey 上
                SelectionKey scKey = sc.register(selector, 0, buffer);
                scKey.interestOps(SelectionKey.OP_READ);
                log.debug("{}", sc);
                log.debug("scKey:{}", scKey);
            } else if (key.isReadable()) {
    
     // 如果是 read
                try {
    
    
                    SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
                    // 获取 selectionKey 上关联的附件   attachment附件
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int read = channel.read(buffer); // 如果是正常断开,read 的方法的返回值是 -1
                    if(read == -1) {
    
    
                        key.cancel();
                    } else {
    
    
                        split(buffer);
                        // 需要扩容
                        if (buffer.position() == buffer.limit()) {
    
    
                            ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                            buffer.flip();
                            newBuffer.put(buffer); // 0123456789abcdef3333\n
                            key.attach(newBuffer);
                        }
                    }

                } catch (IOException e) {
    
    
                    e.printStackTrace();
                    key.cancel();  // 因为客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
                }
            }
        }
    }
}

客户端

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
SocketAddress address = sc.getLocalAddress();
// sc.write(Charset.defaultCharset().encode("hello\nworld\n"));
sc.write(Charset.defaultCharset().encode("0123\n456789abcdef"));
sc.write(Charset.defaultCharset().encode("0123456789abcdef3333\n"));
System.in.read();

5、ByteBuffer大小分配

  • 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗

三、写事件

我们之前的测试的demo都是内容比较少,而且都是服务器读取数据过程,下面我们来模拟测试一下服务端写数据的过程,其中存在的一些问题。

1、写入内容过多问题

  • 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)
  • 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略
    • 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上
    • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册
    • 如果不取消,会每次可写均会触发 write 事件

服务端代码

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.Charset;
import java.util.Iterator;

public class WriteServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        // 打开socketChannel
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        // 将selector注册到socketChannel
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        socketChannel.bind(new InetSocketAddress(8080));
        while (true) {
    
    
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
    
    
                    SocketChannel sc = socketChannel.accept();
                    sc.configureBlocking(false);

                    // 向客户端发送大量数据
                    StringBuffer sb = new StringBuffer();
                    for (int i = 0; i < 30000000; i++) {
    
    
                        sb.append('a');
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    while (buffer.hasRemaining()) {
    
    
                        // 返回值代表实际写入的字符
                        int write = sc.write(buffer);
                        System.out.println(write);
                    }
                }
            }
        }
    }
}

客户端代码

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

public class WriteClient {
    
    
    public static void main(String[] args) throws IOException {
    
    
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
        // 客户端接收数据
        int count = 0;
        while(true) {
    
    
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            count += socketChannel.read(buffer);
            System.out.println(count);
            buffer.clear();
        }
    }
}

这里我们模拟服务端向客户端写一次超大的数据,而客户端方则接收统计接收的数据,我们分别运行客户端和服务端的代码:

服务器端控制台打印如下:

4063201
0
0
3014633
4063201
0
...

客户端控制台打印如下:

131071
262142
393213
524284
655355
...

服务器端之所以会出现0的情况是因为:网络的发送能力是有限的,当网络缓冲区写满之后,第二次写就写不进去了,但是我们使用的是while(true),所以会一直重试,直到内容全部发送完毕。

这样虽然能够将大量的数据完整的发送到客户端,但是与非阻塞模式却是大相径庭,可以直到我们while循环中一直使用的SocketChannel,如果发送数据的同时,有新的客户端请求连接或者发送数据,那么这时候SocketChannel是不能同时接收的,因为while循环使SocketChannel一直处于运行状态,这实际上是不符合非阻塞的思想的。

2、处理写事件

我们可以通过第一次数据传输后,判断是否还有后续的数据,如果有的话就监控写模式,在下次循环的时候进行判断传输的模式,同时对写模式单独处理。
解决了上述SocketChannel不能被其它客户端使用的问题。

服务端代码

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.Charset;
import java.util.Iterator;

public class WriteServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        // 打开socketChannel
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        // 将selector注册到socketChannel
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);

        socketChannel.bind(new InetSocketAddress(8080));
        while (true) {
    
    
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
    
    
                    SocketChannel sc = socketChannel.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    // scKey关注读事件
                    scKey.interestOps(SelectionKey.OP_READ);

                    // 1、向客户端发送大量数据
                    StringBuffer sb = new StringBuffer();
                    for (int i = 0; i < 30000000; i++) {
    
    
                        sb.append('a');
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    // 2、返回值代表实际写入的字符
                    int write = sc.write(buffer);
                    System.out.println(write);
                    // 3、判断是否还有剩余内容
                    if (buffer.hasRemaining()) {
    
    
                        // 4、关注可写事件
                        //scKey.interestOps(SelectionKey.OP_WRITE);
                        // 有可能scKey之前关注了其它的事件,所以我们不能将其覆盖掉
                        scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
                        // 5、把未写完的事件使用附件挂载到scKey上
                        scKey.attach(buffer);
                    }
                } else if (key.isWritable()) {
    
    
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel sc = (SocketChannel) key.channel();
                    int write = sc.write(buffer);
                    System.out.println(write);
                    // 6、清理操作
                    if (!buffer.hasRemaining()) {
    
    
                        key.attach(null);
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
                    }
                }
            }
        }
    }
}

客户端代码

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

public class WriteClient {
    
    
    public static void main(String[] args) throws IOException {
    
    
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
        // 客户端接收数据
        int count = 0;
        while(true) {
    
    
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            count += socketChannel.read(buffer);
            System.out.println(count);
            buffer.clear();
        }
    }
}

服务端控制台输出:

2359278
2621420
4325343
5767124
4718556
2490349
2621420
2621420
2475090

客户端控制台输出:

131071
262142
393213
524284
655355
...

为什么要取消write?
只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注。

猜你喜欢

转载自blog.csdn.net/z318913/article/details/127343918