TCP(2)
TCP
多进程版本
这段代码实现了一个 多进程版本的 TCP Echo 服务器,主要功能是接收客户端连接,并使用 父子进程模型 处理多个客户端请求。以下是详细解析:
1. 核心功能
(1) 初始化服务器 (Init())
-
创建监听 Socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4 + TCP- 失败时记录
FATAL日志并退出。
- 失败时记录
-
绑定端口 (
bind())InetAddr local(_port); // 封装 sockaddr_in bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());- 默认绑定
0.0.0.0(所有网卡)。
- 默认绑定
-
监听连接 (
listen())listen(_listensockfd, backlog); // backlog=8(等待连接队列长度)
(2) 处理客户端请求 (Service())
-
读取客户端数据:
ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);- 成功时回显
"echo#"+数据。 n=0:客户端断开连接。n<0:读取异常,关闭连接。
- 成功时回显
-
回显数据:
write(sockfd, echo_string.c_str(), echo_string.size());
(3) 启动服务器 (Run())
-
接受客户端连接 (
accept())int sockfd = accept(_listensockfd, CONV(peer), &len);- 失败时记录
WARNING并继续循环。
- 失败时记录
-
创建子进程处理连接
pid_t id = fork(); if (id == 0) { // 子进程close(_listensockfd); // 关闭不需要的监听socketif (fork() > 0) exit(OK); // 孙子进程脱离父进程Service(sockfd, addr); // 孙子进程处理请求exit(OK); } else { // 父进程close(sockfd); // 父进程关闭客户端socketwaitpid(id, nullptr, 0); // 等待子进程结束(避免僵尸进程) }- 双
fork()技巧:避免僵尸进程,孙子进程由init托管。
- 双
2. 多进程模型解析
(1) 进程关系
父进程(主循环)├─ 子进程(立即退出)└─ 孙子进程(实际处理请求,由 init 回收)
- 目的:避免僵尸进程,无需显式
wait()。
(2) 文件描述符管理
| 进程 | 关闭的 fd | 保留的 fd |
|---|---|---|
| 父进程 | sockfd(客户端) | _listensockfd |
| 子进程 | _listensockfd | sockfd |
| 孙子进程 | 无 | sockfd |
3. 关键优化点
(1) 避免僵尸进程
- 子进程立即退出,孙子进程由
init回收:if (fork() > 0) exit(OK); // 子进程退出,孙子进程成为孤儿进程
(2) 资源释放
- 父进程:关闭
sockfd(避免泄漏)。 - 子进程:关闭
_listensockfd(不需要监听)。
(3) 错误处理
- 所有系统调用检查返回值,记录日志。
4. 潜在问题
(1) 缓冲区溢出风险
read(sockfd, buffer, sizeof(buffer)-1); // 正确
buffer[n] = '\0'; // 应添加此句确保字符串终止
- 当前代码
buffer[0]=0会清空数据,应改为buffer[n]='\0'。
(2) 高并发限制
- 多进程模型 资源消耗大,适合低并发场景。
- 改进方案:
- 线程池(
pthread+ 任务队列)。 - IO 多路复用(
epoll)。
- 线程池(
(3) 端口复用
- 服务器崩溃后可能无法立即重启(
TIME_WAIT状态)。 - 解决方案:
int opt = 1; setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
5. 改进建议
(1) 增加 SO_REUSEADDR
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
(2) 修复 read()/write() 错误处理
if (write(sockfd, echo_string.c_str(), echo_string.size()) < 0) {LOG(LogLevel::WARNING) << "write error";break;
}
(3) 支持优雅退出
void Stop() { _isrunning = false; } // 外部调用可停止服务器
总结
- 功能:多进程 TCP Echo 服务器,支持并发客户端。
- 优点:
- 避免僵尸进程(双
fork技巧)。 - 父子进程正确管理文件描述符。
- 避免僵尸进程(双
- 缺点:
- 进程创建开销大,不适合高并发。
- 缓冲区处理不够安全。
- 适用场景:学习多进程网络编程,低并发 Echo 服务。
多线程版本
这段代码实际上是一个 多线程版本 的 TCP 服务器(使用了线程池),但注释中保留了 多进程版本 的实现(被注释掉的 fork() 部分)。我来解释 多进程版本 的设计含义和实现逻辑:
1. 多进程版本的核心思想
多进程服务器通过 fork() 创建子进程来处理客户端连接,每个客户端由一个独立的进程服务。
优点:
- 进程间隔离性强,一个进程崩溃不会影响其他进程
- 适合 CPU 密集型任务(Python 的 GIL 限制就是典型例子)
- 编程模型相对简单(相比多线程)
缺点:
- 进程创建/销毁开销大(比线程重)
- 进程间通信(IPC)较复杂(需要管道、共享内存等)
- 大量并发时系统资源消耗大
2. 代码中的多进程实现(注释部分)
pid_t id = fork();
if (id < 0) {LOG(LogLevel::FATAL) << "fork error";exit(FORK_ERR);
}
else if (id == 0) { // 子进程close(_listensockfd); // 子进程不需要监听socketif (fork() > 0) exit(OK); // 孙子进程脱离父进程Service(sockfd, addr); // 孙子进程处理请求exit(OK);
}
else { // 父进程close(sockfd); // 父进程不需要客户端socketpid_t rid = waitpid(id, nullptr, 0); // 等待子进程结束(void)rid;
}
关键点解析
-
双
fork()技巧- 第一次
fork()创建子进程,子进程再fork()创建孙子进程后立即退出。 - 目的:让孙子进程被
init进程(PID=1)接管,避免僵尸进程。
- 第一次
-
文件描述符管理
进程 关闭的 fd 保留的 fd 父进程 sockfd(客户端)_listensockfd子进程 _listensockfdsockfd孙子进程 无 sockfd -
避免僵尸进程
- 父进程通过
waitpid()回收子进程(第一次fork()的进程)。 - 孙子进程由
init自动回收,无需处理。
- 父进程通过
3. 多进程 vs 多线程(代码对比)
| 特性 | 多进程版本 | 当前多线程版本 |
|---|---|---|
| 并发模型 | 每个客户端一个进程 | 线程池处理多个客户端 |
| 资源隔离性 | 高(进程间内存隔离) | 低(线程共享内存) |
| 上下文切换开销 | 较大 | 较小 |
| 编程复杂度 | 中等(需处理 fork() 和 IPC) | 较低(线程间共享数据方便) |
| 适用场景 | CPU 密集型任务 | I/O 密集型任务 |
4. 如何恢复多进程版本?
如果想恢复多进程实现,只需:
- 取消注释
fork()部分的代码。 - 注释掉线程池相关的代码:
// ThreadPool<task_t>::GetInstance()->Enqueue([this,&sockfd,&addr](){ // this->Service(sockfd,addr); // });
5. 多进程版本的改进建议
-
信号处理
取消注释signal(SIGCHLD, SIG_IGN),避免僵尸进程:signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号 -
端口复用
添加SO_REUSEADDR选项,避免TIME_WAIT状态:int opt = 1; setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); -
进程池优化
类似线程池,可以预创建进程池(但实现复杂,通常直接用多线程)。
总结
- 多进程版本 通过
fork()为每个客户端创建独立进程,适合 CPU 密集型 任务。 - 双
fork()技巧 用于避免僵尸进程。 - 当前代码实际是 多线程+线程池 实现,更适合 高并发 I/O 密集型 场景。
- 两种模式各有优劣,选择取决于具体需求。
