认识NIO

NIO

一、NIO概述

	是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO APINIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。

1. 阻塞IO

  • 通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同 样,写入调用将会阻塞直至数据能够写入

2. 非阻塞IO(NIO)

  • NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反 是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定 事件时,系统再通知我们
  • 理解起来就是:NIO全双工,双向的,阻塞IO只能是单向的

3. 核心部分

  • Channels
  • Buffers
  • Selectors

二、Channels

  • Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上 移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于 读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底 层操作系统的 API。

1 FileChannel

  • FileChannel从文件中读写数据
1.1 方法介绍
方法介绍
int read(ByteBuffer dst)从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts)将Channel中的数据“分散”到ByteBuffer[]
int write(ByteBuffer src)将ByteBuffer中的数据写入到Channel
long position()返回次通道的文件位置
FileChannel position(long p)设置次通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void fore(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中
1.2 读文件
@Test
public void read() throws Exception {
    //打开一个通道(InputStream/OutputStream/RandomAccessFile)来获取一个FileChannel的实例
    RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\aa.txt","rw");
    FileChannel inchannel = randomAccessFile.getChannel();
    //new一个缓冲区,设置大小
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    //将inchannel中的数据读取到buffer(代表由多少字节被读到了buffer中,-1表示到了文件末尾)
    int bytesRead = inchannel.read(buffer);
    //当返回-1的时候代表读完
    while (bytesRead!=-1){
        System.out.println("读取"+bytesRead);
        //反转此缓冲区,读写转换
        buffer.flip();
        //判断缓冲区是否还有剩余
        while (buffer.hasRemaining()){
            System.out.print((char)buffer.get());
        }
        //缓冲区清空
        buffer.clear();
        //清空后返回bytesRead=-1,退出循环,不写这行死循环
        bytesRead = inchannel.read(buffer);
    }
    randomAccessFile.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QY2F2WtH-1669130362008)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211002110822392.png)]

1.3 写文件
@Test
public void write() throws Exception{
    //打开一个通道(InputStream/OutputStream/RandomAccessFile)来获取一个FileChannel的实例
    RandomAccessFile aFile = new RandomAccessFile("D:\\bb.txt","rw");
    FileChannel inChannel = aFile.getChannel();
    //要准备写入的数据
    String newData = "今天天气不错"+System.currentTimeMillis();
    //new一个缓冲区,设置大小
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    //清空缓冲区
    buffer.clear();
    buffer.put(newData.getBytes());
    //反转读写模式
    buffer.flip();
    //判断缓冲区是否还有剩余
    while (buffer.hasRemaining()){
        //写入数据
        inChannel.write(buffer);
    }
    inChannel.close();
}

2 Socket通道

2.1 通道的定义
  • 通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中 已经存在的 socket 通道都可以被大多数协议操作重复使用。
2.2 全部socket通道类
  • ServerSocketChannel
  • SocketChannel
  • DatagramChannel
2.2.1 ServerSocketChannel
  • ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞 模式下运行。
  • 由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用 它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的 API 来根 据需要设置其他的 socket 选项。
  • ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象, SocketChannel 可以在非阻塞模式下运行。
1. 如何使用
public static void main(String[] args) throws Exception {
    //端口号
    int port = 8080;
    //把数据放到一个buffer缓冲区
    ByteBuffer buf = ByteBuffer.wrap("hello jerry".getBytes());
    //ServerSocketChannel
    //打开服务器套接字通道
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.socket().bind(new InetSocketAddress(port));
    //设置非阻塞模式
    ssc.configureBlocking(false);

    while (true){
        SocketChannel sc = ssc.accept();
        //监听是否有新连接接入
        if(sc == null){
            System.out.println("Waiting fro connections");
            Thread.sleep(2000);
        }else {
            System.out.println("Incoming connection from:"+sc.socket().getRemoteSocketAddress());
            buf.rewind();//指针0
            sc.write(buf);
            sc.close();
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-euXcbhiC-1669130362009)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003140446514.png)]

  1. 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  1. 关闭ServerSocketChannel

    通过调用

ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.
  1. 监听新的连接

    ServerSocketChannel 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的 SocketChannel 是否是 null.如:

while (true){
    SocketChannel sc = ssc.accept();
    //监听是否有新连接接入
    if(sc == null){
        System.out.println("Waiting fro connections");
        Thread.sleep(2000);
    }else {
    }
}
2.2.2 SocketChannel
  • Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用的
  • SocketChannel 是用来连接 Socket 套接字
1. 如何使用
  1. 创建 SocketChannel
SocketChannel socketChannel = SocketChannel.open(new 
InetSocketAddress("www.baidu.com", 80));
//或者
SocketChannel socketChanne2 = SocketChannel.open();
socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
  • 二者区别:直接使用有参 open api 或者使用无参 open api,但是在无参 open 只是创建了一个 SocketChannel 对象,并没有进行实质的 tcp 连接。
  1. 连接校验
socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态
socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接
socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行
连接
socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel
是否已经完成连接
  1. 读写模式
socketChannel.configureBlocking(false);
  1. 读写
SocketChannel socketChannel = SocketChannel.open(
 new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");

public static void main(String[] args) throws Exception {
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
    //设置阻塞和非阻塞
    socketChannel.configureBlocking(false);//非阻塞  //阻塞的话会执行不了下面的代码,阻塞状态
    ByteBuffer buf = ByteBuffer.allocate(16);
    socketChannel.read(buf);
    socketChannel.close();;
    System.out.println("readOver");
}
2.2.3 DataGramChannel
  • DatagramChannel 则模拟包 导向的无连接协议(如 UDP/IP)
  • DatagramChannel 是无连接的,每个数据报 (datagram)都是一个自包含的实体
1. 如何使用
  1. 打开DatagramChannel
DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086))
  1. 接收数据
  • 通过receive()接收UDP包
ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);
//SocketAddress 可以获得发包的 ip、端口等信息,用 toString 查看,格式如下/127.0.0.1:57126
  1. 发送数据
  • 通过send()发送UDP包
DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));
  1. 连接
  • UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收 发送数据包
client.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize= client.read(sendBuffer);
server.write(sendBuffer);

//连接read和write
@Test
public void testConnect() throws Exception{

    DatagramChannel connChannel = DatagramChannel.open();

    //绑定
    connChannel.bind(new InetSocketAddress(9999));
    //连接
    connChannel.connect(new InetSocketAddress("127.0.0.1",9999));
    //write方法
    connChannel.write(ByteBuffer.wrap("发送:jerry".getBytes("UTf-8")));
    //buffer
    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    while (true){
        readBuffer.clear();
        connChannel.read(readBuffer);
        readBuffer.flip();
        System.out.println(Charset.forName("UTF-8").decode(readBuffer));
    }
}

三、Buffer

1. 定义

  • 用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmwAW3Vi-1669130362010)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003145627582.png)]

  • 理解缓冲区
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装
成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是
一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲
区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到
缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O
系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3PhfzpQw-1669130362016)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003145713660.png)]

2. 基本用法

1. 步骤
  1. 写入数据到buffer
  2. 调用file方法
  3. 从buffer中读取数据
  4. 调用clear()方法或者compact()方法

当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip()方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有 两种方式能清空缓冲区:调用 clear()或 compact()方法。clear()方法会清空整个缓冲 区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起 始处,新写入的数据将放到缓冲区未读数据的后面。

  • 读数据
@Test
public void bufferRead() throws Exception{
    RandomAccessFile aFile = new RandomAccessFile("E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\aa.txt","rw");
    FileChannel fileChannel = aFile.getChannel();
    //创建buffer,大小
    ByteBuffer buf = ByteBuffer.allocate(1024);
    //读
    int bytesRead = fileChannel.read(buf);
    while (bytesRead != -1){
        //read模式
        buf.flip();
        while (buf.hasRemaining()){
            System.out.println((char)buf.get());
        }
        buf.clear();
        bytesRead = fileChannel.read(buf);
    }
    aFile.close();
}
  • 写数据
@Test
public void write() throws Exception{

    IntBuffer buf = IntBuffer.allocate(8);
    //buffer放
    for (int i = 0; i < buf.capacity(); i++) {
        int j = 2*(i+1);
        buf.put(j);
    }
    //重置缓冲区
    buf.flip();
    //获取
    while (buf.hasRemaining()){
        int value = buf.get();
        System.out.println(value);
    }
}

3. 工作原理

3.1 常用属性
  • capacity
    • 作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数 据或者清除数据)才能继续写数据往里写数据。
  • positon
    • 1)写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为 0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入 数据的 Buffer 单元。position 最大可为 capacity – 1(因为 position 的初始值为 0).
    • 2)读数据到 Buffer 中时,position 表示读入数据的当前位置,如 position=2 时表 示已开始读入了 3 个 byte,或从第 3 个 byte 开始读取。通过 ByteBuffer.flip()切换 到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position 会 下移到下一个可读入的数据 Buffer 单元。
  • limit
    • 1)写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于 Buffer 的 capacity。
    • 2)读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到 之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsOtXCpj-1669130362019)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211004103452699.png)]

4. 常用类型

ByteBuffer 
MappedByteBuffer 
CharBuffer DoubleBuffer 
FloatBuffer 
IntBuffer 
LongBuffer 
ShortBuffer

5. Buffer分配和写/读数据方法解读

5.1 Buffer分配
  • 要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate 方 法。

    • ByteBuffer buf = ByteBuffer.allocate(48);
      
5.2 写数据到Buffer的两种方法
5.2.1 方式一:从Channel写到buffer的例子
int bytesRead=inChannel.read(buf);//读到buffer
5.2.2 方式二:通过put方法写buffer的例子
buf.put(127);
5.3 file()方法
  • flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并 将 limit 设置成之前 position 的值。换句话说,position 现在用于标记读的位置, limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。
5.4 从buffer中读取数据的两种方式
5.4.1 方式一:从 Buffer 读取数据到 Channel 的例子
int bytesWritten = inChannel.write(buf)
5.4.2 方式二:使用 get()方法从 Buffer 中读取数据的例子
byte aByte = buf.get();
5.5 Buffer的常用方法
方法名说明
rewind()Buffer.rewind()将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保 持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。
clear()position 将被设回 0,limit 被设置成 capacity 的值。换 句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从 哪里开始往 Buffer 里写数据。
compact()compact()方法将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设到最后一 个未读元素正后面。limit 属性依然像 clear()方法一样,设置成 capacity。现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
mark()可以标记 Buffer 中的一个特定 position
reset()可以通过调用 Buffer.reset()方法恢复到这个 position

6. 缓冲区操作

6.1 缓冲区分片
  • 定义:在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象 来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的 缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当 于是现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区。
@Test
    public void sliceBuf(){
        ByteBuffer buf = ByteBuffer.allocate(10);
        for (int i = 0; i < buf.capacity(); i++) {
            buf.put((byte)i);
        }
        //创建子缓冲区
        buf.position(3);
        buf.limit(7);
        //创建子缓冲区
        ByteBuffer slice = buf.slice();
        //改变子缓冲区内容
        for (int i = 0; i < slice.capacity(); i++) {
            byte b = slice.get(i);
            b*=10;
            slice.put(i,b);
        }
        //回到原位
        buf.position(0);
        buf.limit(buf.capacity());
        while (buf.remaining() > 0){
            System.out.print(buf.get()+",");
        }
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EyOba2Y-1669130362021)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211004105208645.png)]

6.2 只读缓冲区
  • 只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲 区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返 回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。 如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:
@Test
public void asReadOnlyBuf(){
    ByteBuffer buf = ByteBuffer.allocate(10);
    for (int i = 0; i < buf.capacity(); i++) {
        byte b = buf.get(i);
        b = (byte) (i * 10);
        buf.put(i,b);
    }
    //创建制度缓冲区
    ByteBuffer asReadOnlyBuffer = buf.asReadOnlyBuffer();
    asReadOnlyBuffer.position(0);
    asReadOnlyBuffer.limit(buf.limit());
    while (asReadOnlyBuffer.remaining() > 0){
        System.out.println(asReadOnlyBuffer.get());
    }
}
  • 如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。只读缓冲 区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是 否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只 可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。
6.3 直接缓冲区
  • 直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档 中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后), 尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数 据。要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使 用方式与普通缓冲区并无区别。

  • 拷贝一个文件

@Test
public void allocateDirectBuf() throws Exception{
    String inFile = "E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\aa.txt";
    FileInputStream fin = new FileInputStream(inFile);
    FileChannel finChannel = fin.getChannel();

    String outFile = "E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\dd.txt";
    FileOutputStream fou = new FileOutputStream(outFile);
    FileChannel fouChannel = fou.getChannel();

    //创建直接缓冲区
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true){
        buffer.clear();
        int r = finChannel.read(buffer);
        if (r == -1){
            break;
        }
        buffer.flip();
        fouChannel.write(buffer);
    }
}
6.4 内存映射
  • 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通 道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的数据出现为 内存数组的内容来 完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这 样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。
static private final int start = 0;
        static private final int size = 1024;
        static public void main(String args[]) throws Exception {
            RandomAccessFile raf = new RandomAccessFile("d:\\atguigu\\01.txt",
                    "rw");
            FileChannel fc = raf.getChannel();
            MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
                    start, size);
            mbb.put(0, (byte) 97);
            mbb.put(1023, (byte) 122);
            raf.close();
        }

四、Selector选择器

1. 定义

  • Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个

2. Selector与Channel的关系

  • 用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如 此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1vWfRXmV-1669130362022)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211005100637487.png)]

3. 优点

  • 使用 Selector 的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个 线程,避免了线程上下文切换带来的开销。

4. 可选择通道(SelectableChannel)

  • (1)不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能 被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继 承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复 用,否则不能。 (
  • 2)SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有 支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类 都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继 承 SelectableChannel,因此是不是可选通道。
  • (3)一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通 道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。

5. Channel注册到Selector

  • (1)使用 Channel.register(Selector sel,int ops)方法,将一个通道注册到一个 选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询的通道操作。
  • (2)可以供选择器查询的通道操作,从类型来分,包括以下四种:
    • 可读 : SelectionKey.OP_READ
    • 可写 : SelectionKey.OP_WRITE
    • 连接 : SelectionKey.OP_CONNECT
    • 接收 : SelectionKey.OP_ACCEPT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值