NIO 注意点集锦

本文探讨了NIO在处理网络通信时的各种注意事项和技术细节,包括如何正确处理读取不完整、写入失败的情况,如何合理注册Channel及更新Interest,以及如何避免因OP_WRITE处理不当而导致的CPU占用过高问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一直很奇怪,为啥没有close事件,终于在一次实验的时候发现:

1.启动一个客户端和服务端

2.关闭客户端,服务端会发生一个read事件,并且在read的时候抛出异常,来表示关闭 

 

另外,这个事件会不断发生,就算从已准备好的集合移除也没有,必须将该channel关闭或者调用哪个该keycancel方法,因为SelectionKey代表的是SelectorChannel之间的联系,所以在Channel关闭了之后,对于Selector来说,这个Channel永远都会发出关闭这个事件,表明自己关闭了,直到从该Selector移除去

 

3.服务端关闭,client端在write的时候会抛出异常

java.io.IOException: 远程主机强迫关闭了一个现有的连接。
    at sun.nio.ch.SocketDispatcher.write0(Native Method)
    at sun.nio.ch.SocketDispatcher.write(Unknown Source)



NIOSelectableChannel关闭的一个问题

如果在取消SelectionKey(这时候只是加入取消的键集合,下一次select才会执行)后没有调用到selectorselect方法(因为Client一般在取消key后,我们都会终止调用select的循环,当然,server关闭一个注册的channel我们是不会终止select循环的),那么本地socket将进入CLOSE-WAIT状态(等待本地Socket关闭)。

简单的解决办法是在 SelectableChannel.close方法之后调用Selector.selectNow方法

 

Netty在超过256连接关闭的时候主动调用一次selectNow


NIO就绪处理之OP_WRITE

一开始很多人以为write事件,表示在调用channelwrite方法之后,就会发生这个事件,然后channel再会把数据真正写出,但是实际上,写操作的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的,所以当你注册写事件后,写操作一直是就绪的,选择处理线程全占用整个CPU资源。所以,只有当你确实有数据要写时再注册写操作,并在写完以后马上取消注册,一般的,Client端需要注册OP_CONNECT,OP_READ;Server端需要注册OP_ACCEPT并且连接之后注册OP_READ

当有数据在写时,将数据写到缓冲区中,并注册写事件。

 publicvoid write(byte[] data) throws IOException { 

         writeBuffer.put(data); 

         key.interestOps(SelectionKey.OP_WRITE); 

 }  

注册写事件后,写操作就绪,这时将之前写入缓冲区的数据写入通道,并取消注册。

 

 channel.write(writeBuffer); 

key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);  

大部分情况下,其实直接用write方法写就好了,没必要用写事件。



读不满

因为我们的数据都是偏业务性的,比如使用开头一个字节来表示后面数据的长度,接着就会等待读取到那么多数据,但是TCP是流式的协议,100字节的数据可能是一段段发送过来的,所以在没有读到完整的数据前需要等待。

这时候可以将buffer attachkey上,下次read发生的时候再继续读取


写不出去

在发送缓冲区空间不够的情况下,write方法可能会返回能够写出去的字节数,比如只剩50字节,你写入100字节,这时候write会返回50,即往缓冲区写入了50字节

 

在网络较好的情况下,这应该是不太可能发生的,一般都是网络有问题,重传率很高

详细的情况可以参考:java nioOP_WRITE的处理解决网速慢的连接

 

while (bb.hasRemaining()) {
    int len = socketChannel.write(bb);
    if (len < 0) {
        throw new EOFException();
    }
}

由于缓冲区一直蛮,下面的代码会一直执行,占用CPU100%,因此推荐的方式如下
while (bb.hasRemaining()) {
    int len = socketChannel.write(bb);
    if (len < 0){
        throw new EOFException();
    }
    if (len == 0) {
        selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
        mainSelector.wakeup();
        break;
    }
}

如果返回0,表示缓冲区满,那么注册WRITE事件,缓冲区不满的情况下,就会触发WRITE事件,在那时候再写入,可以避免不要的消耗。


Selector返回的key集合非线程安全

Selector.selectedKeys/keys 返回的集合都是非线程安全的
Selector.selectedKeys返回的可移除
Selector.keys 不可变
对selected keys的处理必须单线程处理或者适当同步


正确注册Channel和更新interest
直接注册不可吗?
channel.register(selector, ops, attachment);
不是不可以,效率问题
至少加两次锁,锁竞争激烈
Channel本身的regLock,竞争几乎没有
Selector内部的key集合,竞争激烈
更好的方式:加入缓冲队列,等待注册,reactor单线程处理


If(isReactorThread()){
    channel.register(selector,ops,attachment);
}
else{
    register.offer(newEvent(channel,ops,attachment));
    selector.wakeup();
}

同样,SelectionKey.interest(ops)
在linux上会阻塞,需要获取selector内部锁做同步
在win32上不会阻塞
屏蔽平台差异,避免锁的激烈竞争,采用类似注册channel的方式:

if (this.isReactorThread()) {
    key.interestOps(key.interestOps() | SelectionKey.OP_READ);
} 
else {
    this.register.offer(new Event(key,SelectionKey.OP_READ));
    selector.wakeup();
}


正确处理OP_WRITE
OP_WRITE处理不当很容易导致CPU 100%
OP_WRITE触发条件:
   前提:interest了OP_WRITE
   触发条件:
        socket发送缓冲区可写
        远端关闭
        有错误发生
正确的处理方式:
   仅在已经连接的channel上注册
   仅在有数据可写的时候才注册
   触发之后立即取消注册,否则会继续触发导致循环
   处理完成后视情况决定是否继续注册
     没有完全写入,继续注册
     全部写入,无需注册

正确取消注册channel
SelectableChannel一旦注册将一直有效直到明确取消
怎么取消注册?
   channel.close(),内部会调用key.cancel()
   key.cancel();
   中断channel的读写所在线程引起的channel关闭
但是这样还不够!
   key.cancel()仅仅是将key加入cancelledKeys
   直到下一次select才真正处理
   并且channel的socketfd只有在真正取消注册后才会close(fd)

后果是什么?
  服务端,问题不大,select调用频繁
  客户端,通常只有一个连接,关闭channel之后,没有调用select就关闭了selector
  sockfd没有关闭,停留在CLOSE_WAIT状态
正确的处理方式,取消注册也应当作为事件交给reactor处理,及时wakeup做select
适当的时候调用selector.selectNow()
  Netty在超过256连接关闭的时候主动调用一次selectNow

static final int CLEANUP_INTERVAL=256;
private boolean cleanUpCancelledKeys()throws IOException{
    if(cancelledKeys>=CLEANUP_INTERVAL){
        cancelledKeys=0;
        selector.selectNow();
        returntrue;
    }
    returnfalse;
}
//channel关闭的时候
channel.socket.close();
cancelledKeys++;







<think>我们根据用户需求,需要提供Java NIO的教程或网络编程详解。结合引用内容,我们可以从以下几个方面进行讲解: 1. Java NIO的核心概念(通道、缓冲区、选择器) 2. NIO与BIO(传统IO)的区别 3. NIO网络编程实例(包括客户端和服务端) 注意:用户要求回答结构清晰,逐步解决问题,并保证真实可靠。同时,按照指令要求,数学公式使用$...$或$$...$$格式,但我们这里主要是代码和概念,所以重在代码展示和文字描述。 由于引用中提供了部分内容,我们将整合这些内容并扩展。 步骤: 1. 介绍Java NIO 2. 核心概念:通道、缓冲区、选择器 3. 代码示例:客户端和服务端 注意:引用中已经有一个客户端代码(引用[3]),我们可以补充一个服务端代码,并解释。 另外,根据系统指令,我们需要在最后生成相关问题。 开始回答:</think>### Java NIO 网络编程详解 Java NIO(New I/O)是 Java 1.4 引入的非阻塞 I/O API,通过通道(Channel)、缓冲区(Buffer)和选择器(Selector)实现高性能网络通信。与传统 BIO(Blocking I/O)相比,NIO 能单线程处理数千并发连接[^1]。 #### 一、核心组件 1. **通道 (Channel)** 双向数据传输管道,支持异步读写操作。常用实现类: - `SocketChannel`:TCP 客户端通道 - `ServerSocketChannel`:TCP 服务端通道 - `FileChannel`:文件操作通道 2. **缓冲区 (Buffer)** 数据存储容器,核心属性: - `capacity`:最大容量(只读) - `position`:当前读写位置 - `limit`:可操作数据边界 ```java // 创建缓冲区示例 ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配堆内存 ``` 3. **选择器 (Selector)** 多路复用器,监控通道的 I/O 事件(如连接就绪、数据到达): ```java Selector selector = Selector.open(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); // 注册读事件 ``` #### 二、NIO 与 BIO 关键差异 | **特性** | **BIO** | **NIO** | |----------------|-----------------------|-----------------------| | 阻塞模式 | 同步阻塞 | 同步非阻塞 | | 线程模型 | 1 连接 = 1 线程 | 单线程处理多连接 | | 数据传输单位 | 流(Stream) | 块(Buffer) | | 核心优势 | 编程简单 | 高并发、低资源消耗 | #### 三、网络编程实战 1. **NIO 客户端示例**(基于引用[3]优化): ```java public class NIOClient { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); // 非阻塞模式 socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080)); while (!socketChannel.finishConnect()) { // 连接建立中,可执行其他任务 } ByteBuffer buffer = ByteBuffer.wrap("Hello Server".getBytes()); socketChannel.write(buffer); // 异步发送数据 socketChannel.close(); } } ``` 2. **NIO 服务端示例**: ```java public class NIOServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8080)); serverChannel.configureBlocking(false); // 非阻塞模式 Selector selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 监听连接事件 while (true) { selector.select(); // 阻塞直到有就绪事件 Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> iter = keys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isAcceptable()) { // 处理新连接 SocketChannel client = serverChannel.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 处理数据读取 SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(128); client.read(buffer); System.out.println("Received: " + new String(buffer.array())); } iter.remove(); // 移除已处理事件 } } } } ``` #### 四、性能优化建议 1. **使用直接缓冲区**:通过 `ByteBuffer.allocateDirect()` 分配堆外内存,减少 JVM 堆与操作系统间的数据拷贝[^2]。 2. **批量处理事件**:单次 `selector.select()` 调用处理多个就绪通道,降低系统调用开销。 3. **避免频繁注册**:对频繁读写的通道保持持久注册状态。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值