新io:nio的学习

总结摘要
新io:nio的学习

OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。

与 Java IO 不同,Java NIO 是一个面向缓冲区的包。这意味着数据是从通道读取到缓冲区中,然后进一步处理。例如,一个线程请求通道将数据读取到缓冲区中,而在通道同时将数据读取到缓冲区的过程中,线程可以去做其他工作。一旦数据被读取到缓冲区,线程就可以继续处理在读取操作期间留下的工作。因此,NIO 是一种双向数据传输。

OIO的操作是阻塞的,而NIO的操作是非阻塞的。

与 Java IO 不同,Java NIO 是一种非阻塞 IO。这意味着如果一个线程正在调用 read() 或 write() 操作,该线程不会被阻塞,直到有数据可读或数据完全写入,而是可以继续执行其他任务。这就是它被称为异步 IO 或非阻塞 IO 的原因。

基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点像流。

数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。

Channel的实现

这些是Java NIO中最重要的通道的实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

1
FileChannel inChannel = aFile.getChannel();

从file对象获取filechannel通道对象,FileChannel是javanio中用于文件读写操作的通道,提供比传统文件流读写更高效灵活

buffer的实现

以下是Java NIO里关键的Buffer实现:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

这些Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。

Buffer的分配

要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个allocate方法。下面是一个分配48字节capacity的ByteBuffer的例子。

1
ByteBuffer buf = ByteBuffer.allocate(48);

这是分配一个可存储1024个字符的CharBuffer:

1
CharBuffer buf = CharBuffer.allocate(1024);

NIO使用示例

基本的 Channel 示例

下面是一个使用FileChannel读取数据到Buffer中的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
    System.out.println("Read " + bytesRead);
    buf.flip();
    while(buf.hasRemaining()){
        System.out.print((char) buf.get());
    }
    buf.clear();
    bytesRead = inChannel.read(buf);
}
aFile.close();

注意 buf.flip() 的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。

  1. 初始化:创建 RandomAccessFile 对象用于读写文件,然后通过该对象获取 FileChannel
  2. 分配缓冲区:创建一个指定大小的 ByteBuffer 用于存储从文件中读取的数据。
  3. 读取数据:从 FileChannel 中读取数据到 ByteBuffer 中,并检查是否读取到文件末尾。
  4. 处理数据:如果读取到数据,将 ByteBuffer 翻转,将其中的数据输出,然后清空 ByteBuffer 准备下一次读取。
  5. 关闭文件:当读取完所有数据后,关闭 RandomAccessFile

flip()

用 flip() 方法将 Buffer buf从写入模式切换到读取模式。调用 flip() 将 position 设置回0,并将 limit 置为刚才的位置。

换句话说,position 现在标记了读取位置,limit 标记了写入缓冲区的字节、字符数等——可以读取的字节数、字符数等的限制。

Nio实现高效Socket通信,以下是实现逻辑和步骤:

  1. Channel(通道)
    • ServerSocketChannel:服务端监听TCP连接
    • SocketChannel:客户端与服务端的数据传输通道
    • 支持非阻塞模式(configureBlocking(false)
  2. Buffer(缓冲区)
    • 数据读写通过ByteBuffer完成,需调用flip()切换读写模式
    • 常用方法:put()get()clear()compact()
  3. Selector(选择器)
    • 单线程管理多个通道的事件(如连接、读、写)
    • 监听事件类型:OP_ACCEPTOP_READOP_WRITE

服务器端

初始化服务器

1
2
3
ServerSocketChannel serverChannel = ServerSocketChannel.open(); 
serverChannel.bind(new  InetSocketAddress(8080));
serverChannel.configureBlocking(false);  // 非阻塞模式 

注册到Selector

1
2
Selector selector = Selector.open(); 
serverChannel.register(selector,  SelectionKey.OP_ACCEPT); // 监听连接事件 

事件循环处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 clientChannel = serverChannel.accept(); 
            clientChannel.configureBlocking(false); 
            clientChannel.register(selector,  SelectionKey.OP_READ);
        } else if (key.isReadable())  {
            // 读取数据 
            SocketChannel channel = (SocketChannel) key.channel(); 
            ByteBuffer buffer = ByteBuffer.allocate(1024); 
            int bytesRead = channel.read(buffer); 
            if (bytesRead == -1) {
                channel.close();  // 客户端关闭连接 
            } else {
                buffer.flip(); 
                // 处理数据(如回写)
                channel.write(buffer); 
                buffer.clear(); 
            }
        }
        iter.remove();  // 移除已处理事件 
    }
}

客户端

连接服务器

1
2
3
4
5
6
7
8
SocketChannel clientChannel = SocketChannel.open(); 
clientChannel.configureBlocking(false);  // 非阻塞模式 
clientChannel.connect(new  InetSocketAddress("localhost", 8080));

// 等待连接完成(非阻塞模式下需手动检查)
while (!clientChannel.finishConnect())  {
    // 可执行其他任务 
}

发送和接受数据

1
2
3
4
5
6
ByteBuffer buffer = ByteBuffer.wrap("Hello  Server".getBytes());
clientChannel.write(buffer); 

buffer.clear(); 
clientChannel.read(buffer);  // 非阻塞模式下可能返回0 
buffer.flip(); 

关键注意事项

  1. 资源管理
    • 需手动关闭通道和选择器(close()
    • 避免内存泄漏:及时释放不再使用的Buffer
  2. 粘包与拆包问题
    • 需自定义协议(如消息头声明长度)
    • 使用ByteBuffercompact()合并不完整数据
  3. 性能优化
    • 使用直接缓冲区(ByteBuffer.allocateDirect() )减少内存拷贝
    • 避免在事件循环中执行阻塞操作
  4. NIO与BIO对比
    • 优势:单线程处理高并发连接,减少线程上下文切换
    • 劣势:编程复杂度高,需处理异步逻辑和边界条件

适用场景

  • 高并发短连接(如即时通讯、实时推送)
  • 需要低延迟和大吞吐量的网络服务(如游戏服务器)

通过合理利用NIO的非阻塞特性,可在单线程中高效管理数千个并发连接,但需注意代码健壮性和异常处理。