BIO
字数: 0 字 时长: 0 分钟
BIO 通常指 Blocking I/O(阻塞式 IO)。
在 Java 网络编程中,BIO 主要使用:
ServerSocket
Socket
InputStream
OutputStream进行网络通信。
它最大的特点就是:
当线程执行 IO 操作时,如果数据还没有准备好,线程会一直阻塞等待。
例如:
Socket socket = serverSocket.accept();如果没有客户端连接:
当前线程
↓
阻塞在 accept()
↓
等待客户端连接读取数据也是一样:
int len = inputStream.read(buffer);如果客户端还没有发送数据:
当前线程
↓
阻塞在 read()
↓
等待数据到达1. BIO 的基本模型
传统 BIO 网络服务器通常采用:
一个连接
↓
一个线程大致模型:
客户端 1 ──→ Socket ──→ 线程 1
客户端 2 ──→ Socket ──→ 线程 2
客户端 3 ──→ Socket ──→ 线程 3
客户端 4 ──→ Socket ──→ 线程 4服务器通过:
ServerSocket监听客户端连接。
每当有新的客户端连接:
Socket socket = serverSocket.accept();服务器获得一个:
Socket然后通过 Socket 的:
InputStream
OutputStream与客户端进行数据通信。
整体流程:
ServerSocket
↓
accept()
↓
Socket
↓
InputStream / OutputStream
↓
读写数据2. BIO 服务端
来看一个最基础的 BIO 服务端:
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();
}
}
}这里存在两个比较明显的阻塞点。
第一个:
serverSocket.accept();没有客户端连接时:
线程阻塞
↓
等待连接第二个:
in.read(buffer);客户端没有发送数据时:
线程阻塞
↓
等待数据这就是 BIO 的典型特点。
3. BIO 客户端
客户端使用:
Socket连接服务器:
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)
);
}
}
}整个通信过程:
客户端
↓
Socket
↓
OutputStream
↓
网络
↓
ServerSocket / Socket
↓
InputStream
↓
服务端4. 一个连接一个线程
如果只有一个线程处理所有客户端:
客户端 A
↓
连接成功
↓
read() 阻塞
↓
客户端 B 无法及时处理所以传统 BIO 服务端通常会采用:
一个客户端连接
↓
创建一个线程
↓
线程负责这个 Socket例如:
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> {
handle(socket);
}).start();
}这样多个客户端就可以同时处理。
模型变成:
ServerSocket
│
┌─────────────┼─────────────┐
│ │ │
Socket Socket Socket
│ │ │
Thread Thread Thread但是这种模型也带来了新的问题。
假设同时有:
10000 个连接理论上可能需要维护大量线程。
大量线程会带来:
线程创建和销毁成本
线程栈内存占用
线程上下文切换
CPU 调度压力更关键的问题是:
很多线程大部分时间可能什么都没干,只是在阻塞等待网络数据。
例如:
线程 1 → read() → 等待
线程 2 → read() → 等待
线程 3 → read() → 等待
线程 4 → read() → 等待
...这就是 BIO 在大量长连接场景下的主要问题。
5. 使用线程池优化 BIO
当然,我们也不一定要:
一个连接创建一个新线程可以使用线程池控制线程数量。
例如:
ExecutorService executor = Executors.newFixedThreadPool(10);
while (true) {
Socket socket = serverSocket.accept();
executor.execute(() -> {
handle(socket);
});
}模型变成:
多个 Socket
↓
线程池
↓
固定数量线程相比不断创建线程:
new Thread()线程池可以减少:
线程频繁创建
线程频繁销毁同时限制服务器中的线程数量。
这种模型有时也被称为:
伪异步 IO但是它没有改变 BIO 的本质。
线程执行:
socket.getInputStream().read();时:
仍然会发生阻塞。
因此:
线程池
↓
解决线程无限创建问题
但没有解决
↓
IO 本身的阻塞问题6. BIO 的主要问题
BIO 最大的问题并不是:
它不能工作而是大量网络连接情况下:
连接数增加
↓
需要更多线程
↓
大量线程阻塞等待 IO
↓
内存占用增加
↓
上下文切换增加
↓
系统性能下降特别是这种场景:
连接很多
+
真正发送数据的连接很少例如:
10000 个客户端保持连接
↓
只有几十个客户端正在发送数据BIO 仍然可能需要大量线程维护这些连接。
而大量线程实际上都在:
read()
↓
阻塞等待线程资源利用率并不高。
7. BIO 适合什么场景
BIO 并不是完全没有价值。
如果系统:
连接数量较少
通信逻辑简单
连接生命周期较短
对并发要求不高BIO 的代码反而非常直观。
它最大的优点就是:
编程模型简单,代码容易理解。
例如:
accept()
↓
read()
↓
处理业务
↓
write()整个程序基本就是按照正常的代码执行顺序运行。
因此 BIO 的特点可以概括为:
优点
↓
简单
直观
容易编写
缺点
↓
线程容易阻塞
大量连接需要大量线程
高并发场景资源消耗较大8. BIO 与 NIO
BIO 的典型模型:
一个连接
↓
一个线程
↓
阻塞等待 IONIO 则提供:
SocketChannel
ServerSocketChannel
Selector通过:
非阻塞 IO
+
IO 多路复用让少量线程管理多个连接。
简单对比:
| 对比 | BIO | NIO |
|---|---|---|
| IO 模型 | 阻塞 IO | 支持非阻塞 IO |
| 网络 API | Socket / ServerSocket | SocketChannel / ServerSocketChannel |
| 数据处理 | Stream | Channel + Buffer |
| 多连接处理 | 通常一个连接一个线程 | Selector 管理多个 Channel |
| 编程复杂度 | 简单 | 相对复杂 |
| 高并发连接 | 线程资源压力较大 | 更适合大量连接 |
两者模型对比:
BIO
Socket ──→ Thread
Socket ──→ Thread
Socket ──→ Thread
Socket ──→ ThreadNIO:
SocketChannel ─┐
SocketChannel ─┤
SocketChannel ─┼──→ Selector ──→ Thread
SocketChannel ─┘也就是说,NIO 主要解决了 BIO 在:
大量并发网络连接下线程资源消耗过大的问题。
详细内容见:[[NIO]]
9. 小结
BIO 即阻塞式 IO。
在 Java 网络编程中,主要使用:
ServerSocket
Socket
InputStream
OutputStream它最核心的特点:
accept()
↓
没有连接就阻塞
read()
↓
没有数据就阻塞传统 BIO 服务端通常采用:
一个连接
↓
一个线程虽然可以通过:
线程池控制线程数量,但:
线程池只能优化线程的创建和管理,并没有改变 BIO 本身的阻塞特性。
因此可以把 BIO 的核心问题记成:
大量连接
↓
大量线程
↓
大量线程阻塞等待 IO
↓
线程资源浪费
↓
引出 NIO一句话理解:
BIO 是线程等 IO;NIO 则通过非阻塞 Channel 和 Selector,让一个线程可以管理多个网络连接。