网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

BIO ​

字数: 0 字 时长: 0 分钟

BIO 通常指 Blocking I/O(阻塞式 IO)。

在 Java 网络编程中,BIO 主要使用:

text
ServerSocket
Socket
InputStream
OutputStream

进行网络通信。

它最大的特点就是:

当线程执行 IO 操作时,如果数据还没有准备好,线程会一直阻塞等待。

例如:

java
Socket socket = serverSocket.accept();

如果没有客户端连接:

text
当前线程
    ↓
阻塞在 accept()
    ↓
等待客户端连接

读取数据也是一样:

java
int len = inputStream.read(buffer);

如果客户端还没有发送数据:

text
当前线程
    ↓
阻塞在 read()
    ↓
等待数据到达

1. BIO 的基本模型 ​

传统 BIO 网络服务器通常采用:

text
一个连接
    ↓
一个线程

大致模型:

text
客户端 1 ──→ Socket ──→ 线程 1
客户端 2 ──→ Socket ──→ 线程 2
客户端 3 ──→ Socket ──→ 线程 3
客户端 4 ──→ Socket ──→ 线程 4

服务器通过:

java
ServerSocket

监听客户端连接。

每当有新的客户端连接:

java
Socket socket = serverSocket.accept();

服务器获得一个:

java
Socket

然后通过 Socket 的:

text
InputStream
OutputStream

与客户端进行数据通信。

整体流程:

text
ServerSocket
    ↓
accept()
    ↓
Socket
    ↓
InputStream / OutputStream
    ↓
读写数据

2. BIO 服务端 ​

来看一个最基础的 BIO 服务端:

java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BIOServer {

    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8080);

        System.out.println("BIO Server started...");

        while (true) {

            // 阻塞等待客户端连接
            Socket socket = serverSocket.accept();

            System.out.println("客户端连接:" + socket.getRemoteSocketAddress());

            // 每个客户端交给一个线程处理
            new Thread(() -> {
                try (
                        InputStream in = socket.getInputStream();
                        OutputStream out = socket.getOutputStream()
                ) {

                    byte[] buffer = new byte[1024];

                    int len;

                    // 阻塞等待客户端发送数据
                    while ((len = in.read(buffer)) != -1) {

                        String message = new String(buffer, 0, len);

                        System.out.println("收到消息:" + message);

                        // 将数据原样写回客户端
                        out.write(buffer, 0, len);
                        out.flush();
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

这里存在两个比较明显的阻塞点。

第一个:

java
serverSocket.accept();

没有客户端连接时:

text
线程阻塞
    ↓
等待连接

第二个:

java
in.read(buffer);

客户端没有发送数据时:

text
线程阻塞
    ↓
等待数据

这就是 BIO 的典型特点。


3. BIO 客户端 ​

客户端使用:

java
Socket

连接服务器:

java
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class BIOClient {

    public static void main(String[] args) throws Exception {

        try (
                Socket socket = new Socket("localhost", 8080);
                OutputStream out = socket.getOutputStream();
                InputStream in = socket.getInputStream()
        ) {

            out.write("Hello BIO".getBytes());
            out.flush();

            byte[] buffer = new byte[1024];

            int len = in.read(buffer);

            System.out.println(
                    new String(buffer, 0, len)
            );
        }
    }
}

整个通信过程:

text
客户端
    ↓
Socket
    ↓
OutputStream
    ↓
网络
    ↓
ServerSocket / Socket
    ↓
InputStream
    ↓
服务端

4. 一个连接一个线程 ​

如果只有一个线程处理所有客户端:

text
客户端 A
    ↓
连接成功
    ↓
read() 阻塞
    ↓
客户端 B 无法及时处理

所以传统 BIO 服务端通常会采用:

text
一个客户端连接
        ↓
创建一个线程
        ↓
线程负责这个 Socket

例如:

java
while (true) {

    Socket socket = serverSocket.accept();

    new Thread(() -> {
        handle(socket);
    }).start();
}

这样多个客户端就可以同时处理。

模型变成:

text
                 ServerSocket
                      │
        ┌─────────────┼─────────────┐
        │             │             │
      Socket        Socket        Socket
        │             │             │
      Thread        Thread        Thread

但是这种模型也带来了新的问题。

假设同时有:

text
10000 个连接

理论上可能需要维护大量线程。

大量线程会带来:

text
线程创建和销毁成本
线程栈内存占用
线程上下文切换
CPU 调度压力

更关键的问题是:

很多线程大部分时间可能什么都没干,只是在阻塞等待网络数据。

例如:

text
线程 1 → read() → 等待
线程 2 → read() → 等待
线程 3 → read() → 等待
线程 4 → read() → 等待
...

这就是 BIO 在大量长连接场景下的主要问题。


5. 使用线程池优化 BIO ​

当然,我们也不一定要:

text
一个连接创建一个新线程

可以使用线程池控制线程数量。

例如:

java
ExecutorService executor = Executors.newFixedThreadPool(10);

while (true) {

    Socket socket = serverSocket.accept();

    executor.execute(() -> {
        handle(socket);
    });
}

模型变成:

text
多个 Socket
     ↓
   线程池
     ↓
固定数量线程

相比不断创建线程:

text
new Thread()

线程池可以减少:

text
线程频繁创建
线程频繁销毁

同时限制服务器中的线程数量。

这种模型有时也被称为:

text
伪异步 IO

但是它没有改变 BIO 的本质。

线程执行:

java
socket.getInputStream().read();

时:

仍然会发生阻塞。

因此:

text
线程池
    ↓
解决线程无限创建问题

但没有解决
    ↓
IO 本身的阻塞问题

6. BIO 的主要问题 ​

BIO 最大的问题并不是:

text
它不能工作

而是大量网络连接情况下:

text
连接数增加
    ↓
需要更多线程
    ↓
大量线程阻塞等待 IO
    ↓
内存占用增加
    ↓
上下文切换增加
    ↓
系统性能下降

特别是这种场景:

text
连接很多
+
真正发送数据的连接很少

例如:

text
10000 个客户端保持连接
    ↓
只有几十个客户端正在发送数据

BIO 仍然可能需要大量线程维护这些连接。

而大量线程实际上都在:

text
read()
    ↓
阻塞等待

线程资源利用率并不高。


7. BIO 适合什么场景 ​

BIO 并不是完全没有价值。

如果系统:

text
连接数量较少
通信逻辑简单
连接生命周期较短
对并发要求不高

BIO 的代码反而非常直观。

它最大的优点就是:

编程模型简单,代码容易理解。

例如:

text
accept()
    ↓
read()
    ↓
处理业务
    ↓
write()

整个程序基本就是按照正常的代码执行顺序运行。

因此 BIO 的特点可以概括为:

text
优点
    ↓
简单
直观
容易编写

缺点
    ↓
线程容易阻塞
大量连接需要大量线程
高并发场景资源消耗较大

8. BIO 与 NIO ​

BIO 的典型模型:

text
一个连接
    ↓
一个线程
    ↓
阻塞等待 IO

NIO 则提供:

text
SocketChannel
ServerSocketChannel
Selector

通过:

text
非阻塞 IO
    +
IO 多路复用

让少量线程管理多个连接。

简单对比:

对比BIONIO
IO 模型阻塞 IO支持非阻塞 IO
网络 APISocket / ServerSocketSocketChannel / ServerSocketChannel
数据处理StreamChannel + Buffer
多连接处理通常一个连接一个线程Selector 管理多个 Channel
编程复杂度简单相对复杂
高并发连接线程资源压力较大更适合大量连接

两者模型对比:

text
BIO

Socket ──→ Thread
Socket ──→ Thread
Socket ──→ Thread
Socket ──→ Thread

NIO:

text
SocketChannel ─┐
SocketChannel ─┤
SocketChannel ─┼──→ Selector ──→ Thread
SocketChannel ─┘

也就是说,NIO 主要解决了 BIO 在:

大量并发网络连接下线程资源消耗过大的问题。

详细内容见:[[NIO]]


9. 小结 ​

BIO 即阻塞式 IO。

在 Java 网络编程中,主要使用:

text
ServerSocket
Socket
InputStream
OutputStream

它最核心的特点:

text
accept()
    ↓
没有连接就阻塞

read()
    ↓
没有数据就阻塞

传统 BIO 服务端通常采用:

text
一个连接
    ↓
一个线程

虽然可以通过:

text
线程池

控制线程数量,但:

线程池只能优化线程的创建和管理,并没有改变 BIO 本身的阻塞特性。

因此可以把 BIO 的核心问题记成:

text
大量连接
    ↓
大量线程
    ↓
大量线程阻塞等待 IO
    ↓
线程资源浪费
    ↓
引出 NIO

一句话理解:

BIO 是线程等 IO;NIO 则通过非阻塞 Channel 和 Selector,让一个线程可以管理多个网络连接。

Noctiflorous Garden · MIT License