📌 核心结论(TL;DR):
单进程阻塞 :单进程 TCP 服务器卡在
read时,无法accept新连接。多进程方案 (
fork):主进程接客,子进程服务。优点是隔离性好,缺点是资源开销极大。多线程方案 (
pthread) :主线程accept,子线程read/write。开销小,并发量高。多线程两大坑 :必须调用
pthread_detach回收线程资源(防僵尸线程);主线程必须close(conn_fd)防止文件描述符泄漏。
一、 为什么需要并发服务器?
最初的 EchoServer,一旦 accept 成功,主线程就陷入了 while(read) 阻塞,此时其他客户端根本无法建立连接。
二、 多线程并发模型设计
采用经典的 主从模式:
-
主线程(Main Thread) :死循环执行
accept,负责"拉客"。 -
工作线程(Worker Thread) :每来一个客户端,主线程就创建一个新线程,并把
connfd交给它。工作线程负责read/write和关闭连接。
三、 代码实战与核心避坑
(插入 thread_worker 函数的核心代码和主线程创建线程的代码)
cpp
// ... 省略部分结构体定义
while (1) {
ThreadArg* arg = (ThreadArg*)malloc(sizeof(ThreadArg));
socklen_t len = sizeof(arg->client_addr);
arg->conn_fd = accept(listen_fd, (struct sockaddr*)&(arg->client_addr), &len);
pthread_t tid;
pthread_create(&tid, NULL, thread_worker, (void*)arg);
// 🚨 坑点 1:分离线程
pthread_detach(tid);
// 🚨 坑点 2:关闭主线程的 conn_fd
close(arg->conn_fd);
}
深度解析:
-
为什么要
pthread_detach?如果不分离,线程结束后资源不回收,系统会报pthread_create failed: Resource temporarily unavailable。 -
为什么主线程必须
close(conn_fd)?因为accept返回的文件描述符是主线程拥有的,传给子线程后,如果不关闭主线程的,主线程的文件描述符表会无限增长,导致Too many open files。
四、 进阶预告
多线程虽然好,但如果客户端达到上万个,创建上万个线程依然会压垮 CPU。
自我实战操作
多线程版服务端 tcp_server_thread.c
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
typedef struct {
int conn_fd;
struct sockaddr_in client_addr;
} ThreadArg;
// 子线程干活的函数
void* thread_worker(void* arg) {
ThreadArg* t_arg = (ThreadArg*)arg;
int conn_fd = t_arg->conn_fd;
char buffer[1024];
printf("新线程开始服务客户端: %d\n", conn_fd);
while (1) {
int n = read(conn_fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
write(conn_fd, buffer, n); // 回显
} else if (n == 0) {
printf("客户端 %d 断开\n", conn_fd);
break;
} else break;
}
close(conn_fd); // 子线程负责关闭自己通信的 FD
free(t_arg);
return NULL;
}
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_fd, 10);
printf("多线程 TCP 服务器启动...\n");
while (1) {
ThreadArg* arg = (ThreadArg*)malloc(sizeof(ThreadArg));
socklen_t len = sizeof(arg->client_addr);
// 主线程只负责接客
arg->conn_fd = accept(listen_fd, (struct sockaddr*)&(arg->client_addr), &len);
// 创建子线程去处理通信
pthread_t tid;
pthread_create(&tid, NULL, thread_worker, (void*)arg);
// 🚨 核心坑点 1:一定要分离线程!
pthread_detach(tid);
// 🚨 核心坑点 2:主线程必须关掉 conn_fd!因为已经复制给子线程了。
// 如果不关,主线程的 FD 会越来越多,最后爆满。
close(arg->conn_fd);
}
return 0;
}