一、什么是阻塞 I/O?
所谓阻塞 I/O(Blocking I/O):
当线程调用 recv() / accept() / send() 时,如果条件不满足,线程会被挂起(阻塞),直到操作完成才返回。
简单说就是:
没数据 → 等
有数据 → 拷贝 → 返回
线程在等待期间什么都不能做。
1.内核层到底发生了什么?
我们以 recv() 为例。当你调用:
recv(sock, buf, 1024, 0);
内核内部流程:
- 进入内核态
️2. 检查 socket 接收缓冲区
️3. 如果没有数据
→ 线程进入等待队列
→ CPU切走
️3. 网络数据到达
️4. 网卡中断
️5. 数据进入内核缓冲区
️6. 唤醒等待线程
️7. 内核把数据拷贝到用户缓冲区
️8. 返回
重点:
- 线程被挂起
- CPU 可以调度其他线程
- 不是忙等
2.阻塞 I/O 时序图
用户线程
│
│ recv()
▼
内核
│
│ 没数据
▼
线程睡眠(阻塞)
│
│ 数据到达
▼
唤醒线程
│
│ 拷贝数据
▼
返回
3.从TCP发送数据的流程理解阻塞 I/O

以两个应用程序通讯为例,来了解一下当“A”向"B" 发送一条消息,流程如下:
第一步:应用A把消息发送到 TCP发送缓冲区。
第二步: TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区。
第三步:B再从TCP接收缓冲区去读取属于自己的数据。
根据上图我们基本上了解消息发送要经过 应用A、应用A对应服务器的TCP发送缓冲区、经过网络传输后消息发送到了应用B对应服务器TCP接收缓冲区、然后最终B应用读取到消息。
应用之间发送消息是间断性的,也就是说在上图中TCP缓冲区还没有接收到属于应用B该读取的消息时,那么此时应用B向TCP缓冲区发起读取申请,TCP接收缓冲区是应该马上告诉应用B, 现在没有你的数据,还是说让应用B在这里等着,直到有数据再把数据交给应用B。
把这个问题应用到第一个步骤也是一样,应用A在向TCP发送缓冲区发送数据时,如果TCP发送缓冲区已经满了,那么是告诉应用A现在没空间了,还是让应用A等待着,等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。
所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。用术语描述就是在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO。
流程:
- 应用进程向内核发起recfrom读取数据。
- 准备数据报(应用进程阻塞)。
- 将数据从内核负责到应用空间。
- 复制完成后,返回成功提示。

4.Windows 阻塞 I/O 服务端完整代码
服务端代码:
#include <winsock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1️⃣ 创建socket
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.s_addr = INADDR_ANY;
// 2️⃣ 绑定
bind(listenSock, (sockaddr*)&serverAddr, sizeof(serverAddr));
// 3️⃣ 监听
listen(listenSock, 5);
std::cout << "Server listening..." << std::endl;
// 4️⃣ 阻塞等待客户端连接
SOCKET clientSock = accept(listenSock, NULL, NULL);
std::cout << "Client connected!" << std::endl;
char buffer[1024];
while (true)
{
// 5️⃣ 阻塞等待数据
int ret = recv(clientSock, buffer, sizeof(buffer), 0);
if (ret <= 0)
{
std::cout << "Client disconnected." << std::endl;
break;
}
buffer[ret] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 6️⃣ 阻塞发送
send(clientSock, buffer, ret, 0);
}
closesocket(clientSock);
closesocket(listenSock);
WSACleanup();
return 0;
}
客户端代码:
#include <winsock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 阻塞连接
connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));
char sendBuf[1024] = "Hello Server";
// 阻塞发送
send(sock, sendBuf, strlen(sendBuf), 0);
char recvBuf[1024];
int ret = recv(sock, recvBuf, sizeof(recvBuf), 0);
recvBuf[ret] = '\0';
std::cout << "Server says: " << recvBuf << std::endl;
closesocket(sock);
WSACleanup();
return 0;
}
二、阻塞 I/O + 多线程模型
核心思想:
每个客户端连接 → 创建一个线程 → 在线程中使用阻塞 I/O
结构图:
主线程
│
│ accept()
▼
创建新线程
│
▼
线程1 —— recv()
线程2 —— recv()
线程3 —— recv()
每个线程:
while(true)
{
recv();
send();
}
执行流程:
主线程
1. listen
2. accept (阻塞)
3.️ 创建线程
️4. 回到 accept
工作线程
️1. recv (阻塞)
️2. 处理数据
️3. send (阻塞)
️4. 循环
服务端代码:
#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <thread>
#pragma comment(lib, "ws2_32.lib")
void ClientHandler(SOCKET clientSock)
{
char buffer[1024];
while (true)
{
int ret = recv(clientSock, buffer, sizeof(buffer), 0);
if (ret <= 0)
{
std::cout << "Client disconnected." << std::endl;
break;
}
buffer[ret] = '\0';
std::cout << "Received: " << buffer << std::endl;
send(clientSock, buffer, ret, 0);
}
closesocket(clientSock);
}
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
serverAddr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&serverAddr, sizeof(serverAddr));
listen(listenSock, 10);
std::cout << "Server listening..." << std::endl;
while (true)
{
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET)
{
std::cout << "New client connected." << std::endl;
std::thread t(ClientHandler, clientSock);
t.detach(); // 分离线程
}
}
closesocket(listenSock);
WSACleanup();
return 0;
}
为什么性能会崩?
假设:
2000 个连接
就意味着:
2000 个线程
问题来了:
内存爆炸
2000 × 1MB = 2GB 栈空间
CPU 上下文切换
线程多了:
频繁 context switch
CPU 大量时间花在线程调度。
三、和阻塞单线程对比

多线程阻塞比单线程强很多,但不够优雅。
常见改进:线程池 + 阻塞 I/O
可以改成:
主线程 accept
↓
把 socket 放入任务队列
↓
固定数量线程处理
但注意:阻塞 I/O 在线程池里意义不大;因为 recv() 会卡住线程。它是用线程数量换并发能力;但代价是内存和 CPU 调度开销。
326



被折叠的 条评论
为什么被折叠?



