第一部分:概念与流程解释
1. 什么是"单线程,阻塞式 I/O"?
- 单线程 :就像餐厅里只有一个服务员。他一次只能接待一桌客人。如果这桌客人点菜慢,或者一直在吃饭,服务员就得干等着,不能去招呼下一桌。
- 阻塞式 (Blocking):意味着"死等"。
- 当程序运行到
accept(等待连接)时,如果没有人来连,程序就卡在那里不动,直到有人来。 - 当程序运行到
recv(接收数据)时,如果对方没发数据,程序也卡在那里不动。
2. Socket API 流程详解 (打电话的比喻)
Socket 网络编程的流程,几乎完美对应生活中座机电话的流程:
socket()------ 买个电话机
- 含义:向操作系统申请一个"套接字"资源。
- 代码意图:创建一个文件描述符(fd),告诉系统我要开始网络通信了。
bind()------ 插上电话线(绑定号码)
- 含义 :把电话机和你的电话号码(IP地址 + 端口号)绑定在一起。
- 代码意图 :告诉系统,如果有人打
8080这个端口,就转给我这个程序。
listen()------ 接通电源,准备接听
- 含义:把电话机状态设置为"可接听"。
- 代码意图:告诉系统,我准备好了,如果有连接进来,先帮我放在"等待队列"里排队(backlog)。
accept()------ 电话响了,拿起听筒
- 含义:从等待队列里取出一个连接,正式建立通话。
- 关键点 :这是一个阻塞 操作。如果没有人打进来,代码就停在这行不动。一旦有人打进来,它会返回一个新的 socket(专门用于和这个人通话),原来的 socket 继续监听别人的电话。
read()/recv()------ 听对方说话
- 接收浏览器发过来的 HTTP 请求(一堆字符串)。
write()/send()------ 回复对方
- 发送 HTTP 响应(HTML 页面)。
close()------ 挂断电话
- 结束这次通话,释放资源。
第二部分:极简乞丐版 WebServer 代码
这是一个在 Linux/Mac 环境下可运行的 C++ 代码(Windows 下 socket API 略有不同,建议在 Linux 虚拟机或 WSL 下运行)。
文件名建议保存为:simple_server.cpp
cpp
#include <iostream>
#include <cstring> // for memset
#include <sys/socket.h> // 核心 socket API
#include <netinet/in.h> // 包含 AF_INET, sockaddr_in 等
#include <unistd.h> // 包含 close, read, write
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
// 1. 创建 Socket (买电话机)
// AF_INET: 使用 IPv4
// SOCK_STREAM: 使用 TCP 协议
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 0) {
perror("Socket creation failed");
return -1;
}
// 2. 绑定 IP 和 端口 (插电话线)
struct sockaddr_in address;
int addrlen = sizeof(address);
// 初始化结构体
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听本机所有 IP
address.sin_port = htons(PORT); // 端口号转为网络字节序
// 执行 bind
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
return -1;
}
// 3. 监听 (打开铃声)
// 3 代表等待队列的最大长度
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
return -1;
}
std::cout << "Server is listening on port " << PORT << "..." << std::endl;
while (true) {
// 4. 接受连接 (拿起听筒) - 阻塞点
// accept 会返回一个新的 socket (new_socket) 用于专门和这个客户端通信
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("Accept failed");
continue;
}
// 5. 读取数据 (听对方说啥)
char buffer[BUFFER_SIZE] = {0};
read(new_socket, buffer, BUFFER_SIZE);
// 打印请求内容(调试用)
// std::cout << "Received Request:\n" << buffer << std::endl;
// 简单的字符串查找判断 GET
if (strstr(buffer, "GET") != NULL) {
// 6. 构造响应 (硬编码 HTML)
// HTTP 响应格式必须严格遵守:状态行 + 响应头 + 空行 + 响应体
const char *html_content = "<html><body><h1>Hello, World!</h1><p>This is a C++ WebServer.</p></body></html>";
std::string response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: text/html\r\n";
response += "Content-Length: " + std::to_string(strlen(html_content)) + "\r\n";
response += "\r\n"; // 必须有的空行,分隔头和体
response += html_content;
// 7. 发送响应 (说话)
send(new_socket, response.c_str(), response.length(), 0);
std::cout << "Response sent to client." << std::endl;
}
// 8. 关闭连接 (挂断)
// 在 HTTP/1.0 中,发完就关。
close(new_socket);
}
// 实际运行中这里不可达,但逻辑上需要关闭监听 socket
close(server_fd);
return 0;
}
第三部分:如何运行和测试
请确保你在 Linux 环境(Ubuntu/CentOS)或者 MacOS 下。
1. 编译代码
打开终端,进入代码所在目录,输入:
bash
g++ simple_server.cpp -o server
如果没有报错,会生成一个名为 server 的可执行文件。
2. 运行服务器
输入:
bash
./server
终端会显示:Server is listening on port 8080...
此时,程序阻塞 在了 accept 函数,正在等待连接。
3. 测试访问
你可以用两种方式测试:
- 方式 A(浏览器):
打开浏览器(Chrome/Edge),在地址栏输入:http://localhost:8080
你将看到页面显示大号的 Hello, World!。 - 方式 B(命令行 curl):
打开另一个终端窗口,输入:
bash
curl -v http://localhost:8080
你会看到详细的 HTTP 握手过程和返回的 HTML。
第四部分:这个版本的"缺陷"在哪?(为了下一阶段学习)
你在运行这个代码时,如果手速够快,或者用脚本同时发 10 个请求,你会发现:
它一次只能服务一个人。
代码逻辑是:
accept -> read -> send -> close -> 回到 accept。
如果我在 read 之后加一个 sleep(10)(模拟处理业务很慢),那么在这 10 秒钟内,其他任何人访问这个网页,浏览器都会在那转圈圈(等待连接),因为主线程还在睡觉,没空回到 accept 去接新的电话。
这就是"阻塞式 I/O + 单线程"的致命弱点。
为了解决这个问题,下一阶段(进阶版)通常会引入:
- IO 多路复用 (Epoll):不用傻等一个电话,而是雇一个接线员(内核),哪个电话响了通知我处理哪个。
- 线程池:来了任务扔给线程池去做,主线程立刻回去接下一个电话。