网络编程学习笔记——IO篇

112 阅读5分钟

BIO与NIO学习笔记

一、BIO(Blocking I/O)

1. BIO的概念

BIO(Blocking I/O)是传统的I/O操作模型,指的是在进行I/O操作时,线程会被阻塞,等待I/O操作完成。每个线程处理读写操作时对应一个socket,处理一个请求(io)。

BIO.jpg

2. BIO的特点

  • 线程对应socket:每个线程都有一个socket,处理一个连接。
  • 阻塞模型:线程在进行I/O操作时会被阻塞,等待数据读写完成。
  • 上下文切换开销大:每次I/O操作需要切换到kernel态,开销较大。
  • 内存占用高

3. BIO的工作流程

  1. 客户端发送读请求。
  2. 服务器线程等待数据到达。
  3. 服务器线程读取数据。
  4. 客户端线程处理数据。

4. 不阻塞处理的实现

如果想要实现不阻塞,可以通过创建子线程来处理I/O操作。例如:

public class BioServer {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(8080);
        while (true) {
            Socket socket = server.accept();
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    try {
                        byte[] data = new byte[1024];
                        int read = socket.read(data);
                        System.out.println("读取到数据:" + read);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            new Thread(task).start();
        }
    }
}

二、NIO(Non-Blocking I/O)

1. NIO的概念

NIO(Non-Blocking I/O)是现代I/O操作模型,允许一个线程同时处理多个I/O操作(因为有selector,所以不用创建多个线程)。通过selectorchannel,实现多路复用,提升效率。

NIO0.jpg

image.png

2. NIO的核心概念

  • Selector(选择器):监控多个channel的读写状态,非阻塞地通知事件。
  • Channel(通道):替代传统的InputStream和OutputStream,支持非阻塞I/O操作。
  • 多路复用(Multiplexing):通过Selector同时处理多个I/O事件。

下面这段代码是NIO创建ServerSocketChannel的代码:

  • 创建
  • 绑定端口号
  • 设置非阻塞false,如果设置true就跟BIO差不多了 NIO.jpg

3. NIO的工作流程

  1. 创建Selector和多个通道。
  2. 注册通道到Selector。
  3. Selector等待I/O事件。
  4. 处理 Selector报告的事件(读、写、连接事件)。

4. NIO的优势

  • 提高效率:一个线程可以同时处理多个I/O操作。
  • 减少阻塞:避免线程上下文切换,性能更高。
  • 灵活性:支持多种I/O操作方式。
  • 减少内存:不需要多线程连接socket,可以一个selector来选择连接的客户端和服务端

5. 简单的NIO服务器示例(多路复用)

Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);  // 非阻塞
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT); // 注册 Accept 事件

while (true) {
    int readyChannels = selector.select();  // 阻塞,直到有事件
    if (readyChannels == 0) continue;

    // 获取所有就绪的事件
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) { // 接受新连接
            ServerSocketChannel ssc1 = (ServerSocketChannel) key.channel();
            SocketChannel sc = ssc1.accept();
            sc.configureBlocking(false);
            sc.register(selector, SelectionKey.OP_READ); // 注册 Read 事件
        } else if (key.isReadable()) { // 读取数据
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int length = sc.read(buffer);
            if(length == -1){
                channel.close();
            } else {
                buffer.flip();
                byte[] remainbuffer = new byte[buffer.remaining()];
                buffer.get(remainbuffer);
                System.out.println("收到数据: " + new String(buffer.array()));
            }
        }
    }
    keys.clear(); // 清空处理过的事件
}

流程

  1. selector.select() 只监听注册过的 Channel
  2. selectedKeys() 遍历 就绪事件(Accept/Read/Write)
  3. Accept(新连接)  → 注册 OP_READ 监听数据。
  4. Read(数据到达)  → 处理数据。

其实在上面的代码中,我们能够看到NIO的三大组件channel、buffer、selector

  • channel是一个一个的通道,大的通道叫ServerSocketChannel,小的叫SocketChannel;比如上面的代码中ssc是大的channel,readchannels就是小的channel
  • buffer里面是读取数据的
  • selector监听选择

image.png

上面列举的NIO方法是多路复用的方法,其实NIO还有轮询的办法。轮询的办法不需要注册channel

🔹 特点
  • 不依赖 Selector,完全手动检查每个 Channel 的状态
  • 非阻塞模式configureBlocking(false)
  • 轮询所有连接(效率低,适合少量连接) 手动轮询的办法(不推荐使用)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

public class NioPollingServer {
    public static void main(String[] args) throws IOException {
        // 1. 开启非阻塞的 ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false); // 非阻塞模式
        ssc.bind(new InetSocketAddress(8080));

        // 2. 维护所有客户端连接的列表
        List<SocketChannel> clientChannels = new ArrayList<>();

        System.out.println("服务端启动,监听 8080 端口...");

        while (true) {
            // 3. 手动轮询检查新连接
            SocketChannel sc = ssc.accept(); // 非阻塞,如果没有连接会立即返回 null
            if (sc != null) {
                sc.configureBlocking(false); // 新客户端也要非阻塞
                clientChannels.add(sc);
                System.out.println("新客户端连接: " + sc.getRemoteAddress());
            }

            // 4. 手动轮询检查所有客户端是否有数据可读
            for (SocketChannel client : clientChannels) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int bytesRead = client.read(buffer); // 非阻塞读取
                if (bytesRead > 0) {
                    buffer.flip();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    System.out.println("收到客户端数据: " + new String(data));
                } else if (bytesRead == -1) {
                    // 客户端断开连接
                    System.out.println("客户端断开: " + client.getRemoteAddress());
                    client.close();
                    clientChannels.remove(client);
                    break; // 避免并发修改异常
                }
                // bytesRead == 0 表示当前无数据,继续轮询
            }

            // 5. 避免 CPU 空转(可选)
            try {
                Thread.sleep(100); // 减少轮询频率
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


三、BIO与NIO的对比

特点BIONIO
模型阻塞模型非阻塞模型
线程模式线程对应socket支持多线程同时处理多个I/O
效率I/O操作阻塞,效率较低非阻塞,效率更高
开发难度简单实现,但线程开销大复杂,需要学习Selector和Channel

四、ThreadPool(线程池)

1. 线程池的概念

ThreadPool(线程池)是一种管理线程的工具类,允许开发者通过提交任务来执行多个线程,避免了手动管理线程。

2. 线程池的特点

  • 线程复用:线程可以重复使用,减少线程创建的开销。
  • 任务队列:支持任务的排队和执行,避免线程饥饿。
  • 核心线程和线程池大小:可以配置核心线程数和最大线程数。

3. 简单的线程池实现代码

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SimpleThreadPool {
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS);
      
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "处理任务:" + i);
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "任务" + i);
        }
      
        executor.shutdown();
    }
}

五、总结

  • BIO:适合传统的单线程、单socket模型,简单实现但效率较低。
  • NIO:适合需要高效处理多个I/O操作的场景,通过Selector和Channel实现多路复用,提升性能。
  • 线程池:用于管理线程资源,避免线程饥饿和创建开销大问题,常用于BIO和NIO中。