💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
引言:《Linux网络(01):网络基础概念》我们建立了网络协议栈的分层认知,明确了传输层的定位与核心职责;《Linux 网络(02):Socket 编程 UDP》我们打通了「套接字系统调用 + 报文收发」的 UDP 全流程。本章正式进入传输层编程的另一半 ------TCP。
TCP 是传输层最典型的面向连接、可靠传输 协议,HTTP/HTTPS、SSH、FTP、数据库连接等绝大多数后端服务都构建在 TCP 之上,是后端岗网络方向第一权重考点。
1. TCP 编程预备知识
1.1 TCP 与 UDP 对比【必考核心】
考察形式:笔试选择 / 简答、一面基础问答、二面场景选型追问。
📌联动 :与《Linux 网络(02):Socket 编程 UDP》1.1 是同一张对比表 ------ 这是网络方向出现频率最高的送分题,必须背到张口就来。
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接,直接发 |
| 可靠性 | 可靠 :确认、重传、去重、有序 | 不可靠,尽力而为:可能丢包、无序、重复,协议自身不感知 |
| 数据形态 | 面向字节流,无消息边界 ➡ 粘包 / 拆包问题 | 面向报文,保留消息边界 |
| 通信模式 | 点对点(一条连接两个端点) | 一对一 / 一对多 / 多对多(支持广播、组播) |
| 流量 / 拥塞控制 | 有 | 无 |
| 首部开销 | 20 字节起 | 8 字节 |
| 典型场景 | HTTP/HTTPS、SSH、FTP、SMTP、数据库、文件传输 | DNS、DHCP、TFTP、音视频直播、实时语音、在线游戏状态同步、QUIC |
💡 一句话总结:TCP 面向连接、可靠、有序、字节流;UDP 无连接、不可靠、无序、报文。
1.2 TCP 核心函数总览【必考核心】
- 套接字系统调用头文件:#include <sys/socket.h>
| API | 作用 | TCP 用法 |
|---|---|---|
| socket(domain, type, protocol) | 创建套接字 | AF_INET, SOCK_STREAM, 0 |
| bind(fd, addr, len) | 绑定 IP + 端口 | 服务端必须显式调用 |
| listen(fd, backlog) | 将套接字置为监听状态 | TCP 专属,服务端必调 |
| accept(fd, &peer, &len) | 从全连接队列取一个连接,返回新 fd | TCP 专属,服务端循环调用 |
| connect(fd, &addr, len) | 发起连接(触发握手) | TCP 专属,客户端必调 |
| read / write | 基于连接收发数据 | 连接建立后替代 recvfrom/sendto |
| recvfrom / sendto | UDP 报文收发 | ❌ TCP 不用 |
| close(fd) | 关闭连接(触发挥手) | 服务端、客户端都用 |
- TCP 服务端四步:socket → bind → listen → accept(UDP 只有 socket + bind)
- 客户端两步:socket → connect
1.3 socket 与 bind:创建与绑定【必考核心】
socket、bind 原型与 UDP 一致,bind 坑点也与 UDP 一致。
- TCP 的 socket() 创建的是监听套接字 (必背),只负责接收连接请求,不参与数据传输
cpp
int socket(int domain, int type, int protocol);
// domain: AF_INET(IPv4) / AF_INET6(IPv6) / AF_UNIX(本地域)
// type: SOCK_DGRAM(UDP 数据报) / SOCK_STREAM(TCP 流)
// protocol: 填 0 表示按前两个参数自动推导协议
// 返回: 成功返回文件描述符(最小未用fd, 通常为3, 因0/1/2是标准输入/输出/错误);
// 失败返回 -1 并置 errno
cpp
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
// 返回 0 成功;返回-1失败并置errno
// bind失败最常见原因及errno的错误码为:
// EADDRINUSE → 端口已被其他进程占用
// EADDRNOTAVAIL → 绑定的IP不在本机网卡上(典型: 云服务器bind公网IP)
// 示例:
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 先清空整个结构体
local.sin_family = AF_INET; // 标记地址类型为 IPv4
local.sin_port = htons(8888); // 填充端口(转网络字节序)
local.sin_addr.s_addr = INADDR_ANY; // 填充 IP
bind(sockfd, (struct sockaddr*)&local, sizeof(local));
1.3.1 SO_REUSEADDR 端口复用【高频易错】
考察形式:笔试选择、场景排障题("服务器重启报 bind: Address already in use")、二面追问。
⚠️ 经典场景 :服务器被 Ctrl+C 杀掉后立刻重启,bind 报 EADDRINUSE。原因是连接断开后端口进入 TIME_WAIT 状态(约 60s),期间该端口不能被立即复用。
💡 解决:bind 前调用 setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)),允许端口在 TIME_WAIT 状态下被立即重新绑定。
SO_REUSEADDR 和 SO_REUSEPORT 分别解决什么问题?
- SO_REUSEADDR:允许端口处于 TIME_WAIT 时复用,解决服务器快速重启的 bind 失败;
- SO_REUSEPORT(Linux 3.9+):允许多个 socket 绑定同一端口,由内核做负载均衡,用于多进程 / 多线程监听同一端口。
1.4 listen 与 backlog:TCP 专属监听【必考核心】
考察形式:笔试选择、一面问答、二面深度追问(队列机制)。
cpp
int listen(int sockfd, int backlog);
// 返回 0 成功,-1 失败置 errno
- 作用:将 sockfd 置为 LISTEN 状态,此后内核开始为它接收客户端的连接请求。
- backlog:全连接队列的最大长度,一般设 5 或 10,不要设太大。
- ❌ 常见误区:以为 backlog 是 "最多能服务的客户端数量"。它只是 "已建立连接、等待 accept 取走" 的排队上限,服务能力取决于后续处理模型。
那全连接队列又是什么?(详见 1.4.1)
1.4.1 半连接队列与全连接队列【高频易错】
考察形式:二面追问、场景排障("连接建立失败 /connect 超时" 排查)。这是 2024-2026 大厂面试权重快速上升的细节考点。
- 半连接队列(SYN 队列):三次握手还没完成的连接;
- 全连接队列(accept 队列):三次握手已完成、等待应用层 accept () 取走的连接;
- 现代 Linux(2.2 之后)中 backlog 是全连接队列长度;
- 全连接队列满时,新的连接请求可能被内核直接丢弃或拒绝 ------ 客户端表现为 connect 超时或被拒。
1.5 accept:服务端获取连接【必考核心】
考察形式:笔试代码补全、一面问答("accept 返回的 fd 是什么")、场景排障。
cpp
int accept(int listen_fd, struct sockaddr* addr, socklen_t* addrlen);
- 服务端调用 accept 时若全连接队列为空,accept 阻塞等待,直到有客户端连接;
- addr 为传出参数:返回客户端的 IP + 端口;不关心可传 NULL;
- addrlen 为入出型参数 :⚠️调用前必须初始化为结构体大小,返回后为实际长度 ------《Linux 网络(02):Socket 编程 UDP》recvfrom 同款坑;
- accept() 返回值:accept() 每成功取一条连接,都会返回一个新连接服务的服务套接字 ,专门负责这条连接的数据收发 。服务套接字用完必须"单独" close,否则会造成文件描述符泄漏。accept() 失败返回 -1 并置 errno。
1.6 connect:客户端发起连接【必考核心】
考察形式:笔试代码补全、一面问答("客户端为什么不 bind")、二面追问(connect 失败场景)。
💡 connect 触发握手,accept () 只是从全连接队列里取一条已建立的连接。服务端即使不调用 accept,客户端 connect 也能成功。
cpp
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
// 返回 0 成功,-1 失败置 errno
- 参数形式与 bind 一致,但语义相反 :bind 绑自己的 地址,connect 填对方的地址;
- connect 成功即完成三次握手 (三次握手由内核完成),此后 sockfd 直接用于 read/write------不会像 accept 那样产生新 fd;
- connect 会触发内核为客户端自动分配本地临时端口 并隐式绑定 ------ 所以客户端不需要显式 bind(类比《Linux 网络(02):Socket 编程 UDP》2.4 ,UDP 中客户端首次调用 sendto 时内核自动分配本地临时端口并隐式绑定);
- connect 失败原因(面试常问):服务端没启动(ECONNREFUSED 拒绝)、IP / 端口填错、网络不通(connect 超时)、全连接队列满(被拒或超时)。
2. V1 版本:单执行流 Echo 回显服务器【实操主线】
这份代码是后面所有版本(多进程 / 多线程 / 线程池 / Reactor)的骨架,必须能默写。
2.1 服务端:socket → bind → listen → accept【必考核心】
cpp
// tcp_server.cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h> // Linux系统调用
#include <sys/types.h> // socklen_t类型
#include <sys/socket.h> // 套接字系统调用
#include <netinet/in.h> // 网络地址结构体头文件:sockaddr_in,INADDR_ANY,端口/IP类型
#include <arpa/inet.h> // 网络字节序转换函数
#define BACKLOG 5
int main(int argc, char* argv[]) {
// 依次传入程序名、端口
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " port\n" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]); // 字符串转整数,得到主机字节序端口号
// 1. socket创建监听套接字:TCP 用 SOCK_STREAM
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
// 端口复用:解决Ctrl+C后快速重启时TIME_WAIT导致bind失败
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. bind绑定:三要素填充 + 强转通用 sockaddr*
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port); // 端口转网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 整数IP转网络字节序
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
return 3;
}
// 3. listen监听:TCP 特有
if (listen(listen_sock, BACKLOG) < 0) {
std::cerr << "listen error: " << strerror(errno) << std::endl;
return 4;
}
// 4. 循环accept取连接 + 为当前连接提供服务
while (true) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 入出型参数,调用前必须初始化为结构体大小
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0) { // accept 失败不退出,continue继续取下一个连接
std::cerr << "accept error: " << strerror(errno) << std::endl;
continue;
}
// 转为主机字节序,用于打印
std::string ip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::cout << "new link: [" << ip << "]:" << cport
<< " sockfd=" << sock << std::endl;
// 为当前连接提供服务(单执行流:服务完一个才accept下一个)
char buffer[1024]; // 数据接收缓冲区
ssize_t n;
// 循环读取客户端数据
while ((n = read(sock, buffer, sizeof(buffer) - 1)) > 0) {
buffer[n] = '\0';
std::cout << ip << ":" << cport << "# " << buffer << std::endl;
write(sock, buffer, n); // 读到的数据原样回显给客户端
}
// read 返回 0 表示对端正常关闭连接
if (n == 0) std::cout << ip << ":" << cport << " close!" << std::endl;
close(sock); // ❗对端关闭连接、服务完成,单独关闭服务套接字
}
close(listen_sock);
return 0;
}
2.2 read 返回值三态【必考核心】
考察形式:笔试选择、代码补全、一面问答("怎么知道对端关闭了连接")。
📌联动 :《Linux系统(05):基础 IO》3.3 read /write 系统调用
cpp
ssize_t read(int fd, void* buf, size_t count);
- > 0:实际读到的字节数
- = 0:读到 EOF 文件末尾 ------ 对端已正常关闭连接
- < 0:出错,置 errno
⚠️ 注意区分两种 "对端消失"(面试连环追问):
- 正常关闭:read 返回 0,可感知;
- 异常掉线 (断电、断网):对端不发送 FIN,本端 read 会一直阻塞,感知不到------ 需要心跳 / 超时机制(应用层保活)。
2.3 TCP 字节流与粘包问题【高频易错】
考察形式:笔试选择("TCP 粘包问题的解决方案")、二面追问。这是 TCP 与 UDP 数据形态差异的直接后果,面试必问。
TCP 面向字节流,只负责传输连续字节,不保存消息边界,不知道哪段算一条完整消息。
- 粘包:多次 write 的数据拼在一起,被一次 read 读完。
- 拆包:一次 write 的数据被拆分,分多次 read 才读完。
因此 TCP 本身不保证 "一次发对应一次收",要想拿到完整消息,必须在应用层自己加边界规则(长度字段 / 分隔符),继续往下学就会碰到对应的协议设计。
2.4 客户端:socket → connect → 读写循环【必考核心】
cpp
// tcp_client.cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char* argv[]) {
// 依次传入程序名、IP、端口
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
// 1. 创建套接字(参数与服务端一致)
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) { std::cerr << "socket error" << std::endl; return 2; }
// 2. 填充服务端地址并connect发起连接
struct sockaddr_in server;
bzero(&local, sizeof(local));
server.sin_family = AF_INET;
server.sin_port = htons(port); // 端口转网络字节序
inet_pton(AF_INET, argv[1], &server.sin_addr); // IP:字符串→网络字节序
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
std::cerr << "connect error: " << strerror(errno) << std::endl;
return 3;
}
std::cout << "connect success" << std::endl;
// 3. 收发循环:connect 成功后直接 read/write,不再产生新 fd
std::string msg;
char buffer[1024];
while (std::getline(std::cin, msg)) {
write(sock, msg.c_str(), msg.size()); // 向服务端发消息
ssize_t n = read(sock, buffer, sizeof(buffer) - 1); // 读服务端回显
if (n > 0) {
buffer[n] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
else if (n == 0) { // read返回0代表对端正常关闭连接
std::cout << "server close!" << std::endl;
break;
}
else {
std::cerr << "read error" << std::endl;
break;
}
}
close(sock);
return 0;
}
2.5 运行结果 (CentOS7)
终端1:服务端

终端2:客户端

① 服务端:后台启动 TCP Echo 服务器,监听 8888 端口,系统返回后台进程 PID。
② 客户端:启动自研 TCP 客户端,连接本地 8888 端口,打印 connect success,三次握手完成、连接建立。
③ 服务端:检测到新连接接入,打印客户端地址 127.0.0.1:40760 及分配的服务套接字 fd。
④ 客户端:键盘输入 hello,向服务端发送该消息。
⑤ 服务端:收到客户端消息,打印客户端地址和消息内容。
⑥ 客户端:收到服务端原样回显的消息,打印 server echo# hello。
2.6 单执行流服务器的弊端【进阶拔高】
考察形式:场景排障题(现象分析)、二面追问("怎么优化")。
现象 :单执行流 TCP 服务器同一时间只能串行服务一个客户端。第一个客户端正在收发时,第二个客户端 connect 显示成功,但发的消息服务端既不打印也不回显(只是内核完成了三次握手、放进了全连接队列,应用层还没调用 accept 取走处理);等第一个客户端退出后,第二个客户端才被服务。
💡 解决:要支持并发就升级为多进程 / 多线程模型。
3. V2 版本:多进程版 TCP 服务器【进阶拔高】
考察形式:二面系统设计 / 并发模型追问、场景排障(僵尸进程)。
单执行流暴露的串行缺陷,多进程版本对策:accept 取到一个连接就 fork 一个子进程去服务。
3.1 核心思路:fork 子进程服务连接【必考核心】
- 并发逻辑:父进程只负责循环 accept 获取连接,每取到一个连接就 fork 出子进程,由子进程为客户端提供服务;父进程立刻 accept 取下一个连接。
- fork 的子进程复制父进程的文件描述符表:fork 后子进程的 fd 表是父进程 fd 表的拷贝,父进程 accept 的服务套接字 fd,子进程可以直接用来收发(《Linux系统(04):进程控制》1.1 fork 函数初识)。
3.2 多进程服务器产生****僵尸进程【高频易错】
考察形式:笔试选择、场景排障("进程表里出现很多僵尸进程")、二面追问。
多进程服务器不处理子进程退出 ➡ 僵尸进程堆积 ➡ 内存泄漏,这是面试官最爱挖的坑。
📌 联动 :《Linux系统(03):进程概念》4.3 僵尸进程
这里父进程循环 accept 获取连接不能阻塞,所以❌"父进程调用 wait() 或 waitpid() 主动回收僵尸进程" 。
捕捉 SIGCHLD
📌 联动 :《Linux系统(08):进程信号》1.3 信号的三种处理方式 signal 函数
子进程退出时向父进程发 SIGCHLD 信号,
- 方式一:忽略后由内核直接回收子进程(简单、推荐)
cpp
signal(SIGCHLD, SIG_IGN);
- 方式二:自定义信号处理函数内部循环调用 waitpid(-1, &st, WNOHANG) 回收(必须循环waitpid,多个子进程同时退出信号会合并递送,只waitpid一次会漏僵尸)。
孙子进程方案
- 爷爷(服务端主进程) accept 连接
- 爷爷 fork 出爸爸,爸爸再 fork 出孙子
- 爷爷 waitpid 回收爸爸(爸爸 fork 完就立即退出,waitpid 立即返回,不阻塞爷爷 accept)
- 孙子服务连接,后续孙子变孤儿进程,被 init 领养,退出后由 init 回收,爷爷无需等待孙子。
3.3 父子进程的 fd 关闭规则【必考核心】
考察形式:手撕代码纠错、二面追问("为什么每个版本都要 close 对应的 fd")。
| 进程 | 该关闭的 fd | 原因 |
|---|---|---|
| 爷爷(父) | accept 返回的服务套接字 fd | 爷爷只负责 accept,不 close 会随连接增多 fd 泄漏 |
| 爸爸 / 孙子(子) | 继承下来的监听套接字 fd | 子进程不需要监听;防止误操作污染监听套接字 |
⚠️ fork 的子进程复制父进程的文件描述符表,一个进程一份文件描述符表,所以父子 fd 表各自独立,谁不用这个 fd 就自己 close(fd),一方关闭不影响对方。
3.4 多进程 TCP 服务端代码
fork 创建子进程、SIGCHLD 信号回收僵尸进程、套接字描述符继承与关闭、阻塞 accept 接口。
cpp
// tcp_server_fork.cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#define BACKLOG 5
// SIGCHLD 信号处理:回收子进程,避免僵尸进程【作业核心考点】
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " port\n" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
// 1. 创建监听套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
// 端口复用:解决重启时 TIME_WAIT 导致的 bind 失败
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 注册信号处理函数
signal(SIGCHLD, sigchld_handler);
// 2. 绑定地址
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
return 3;
}
// 3. 开始监听
if (listen(listen_sock, BACKLOG) < 0) {
std::cerr << "listen error: " << strerror(errno) << std::endl;
return 4;
}
// 4. 循环 accept + fork 子进程处理连接【作业核心考点】
while (true) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0) {
if (errno == EINTR) continue; // 信号中断,重试
std::cerr << "accept error: " << strerror(errno) << std::endl;
continue;
}
std::string ip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::cout << "new link: [" << ip << "]:" << cport
<< " sockfd=" << sock << std::endl;
pid_t pid = fork();
if (pid == 0) {
// 子进程:关闭监听套接字(无需监听)【套接字描述符考点】
close(listen_sock);
// 处理客户端请求:读取+打印+回显
char buffer[1024];
ssize_t n;
while ((n = read(sock, buffer, sizeof(buffer) - 1)) > 0) {
buffer[n] = '\0';
std::cout << ip << ":" << cport << "# " << buffer << std::endl;
write(sock, buffer, n); // 回复响应
}
if (n == 0) std::cout << ip << ":" << cport << " close!" << std::endl;
close(sock);
exit(0);
} else if (pid > 0) {
// 父进程:关闭已连接套接字(交给子进程管理)【套接字描述符考点】
close(sock);
} else {
std::cerr << "fork error: " << strerror(errno) << std::endl;
close(sock);
}
}
close(listen_sock);
return 0;
}
4. V3 版本:多线程版 TCP 服务器【进阶拔高】
考察形式:二面并发模型追问、手撕代码(线程参数传递)、场景排障(编译不过 / 参数传错)。
多进程方案解决了并发,但它的代价在哪?------创建线程的成本远低于进程(线程共享进程地址空间、页表等资源),多执行流服务器优先用多线程。
4.1 线程共享文件描述符表【必考核心】
- 文件描述符表属于进程 ,进程内所有线程看到同一张 fd 表;
- 主线程 accept 上来的 fd,服务线程可直接访问 ------ 但线程不知道 "该服务哪个 fd",必须把 fd + 对端地址显式传给线程(参数传递见 4.2);
- ⚠️ 与多进程的本质区别:进程 fork 是 "拷贝"fd 表(各自独立 close),线程是 "共享"fd 表(谁 close 都影响所有人)------ 所以服务 fd 只能由服务线程 close,监听 fd 谁都不能乱关。
共享 fd 表省事了,但第一个难题随之而来:参数怎么安全地传进线程。
4.2 线程参数传递 ThreadData【高频易错】
考察形式:手撕代码、笔试判断(为什么 new / 为什么 static)、二面追问(内存泄漏)。
pthread_create 的例程只接收一个 void* 参数,而服务需要 fd + IP + 端口三个数据 → 封装参数结构体:
cpp
struct ThreadData {
int _sockfd;
InetAddr _addr; // 封装 sockaddr_in,提供 Ip()/Port()/PrintDebug()
ThreadData(int fd, const struct sockaddr_in& addr) : _sockfd(fd), _addr(addr) {}
};
⚠️ 两大高频坑点:
- 参数必须 new 到堆上:若在栈上定义并取地址传给线程,函数返回后栈空间被复用,线程再访问就是脏数据 / 野指针 ------ 经典并发 bug;
- 线程例程必须是 static 成员函数 :pthread_create 要求 C 风格函数指针
void*(*)(void*),普通成员函数隐含 this 指针,签名不匹配;static 成员函数没有 this,符合要求。例程内记得delete堆参数,否则内存泄漏。
4.3 线程退出回收:pthread_detach【必考核心】
- 线程默认是 joinable 的,退出后资源不自动回收,主线程需 pthread_join(类似进程的 wait);
- 主线程只负责 accept,不想逐个 join → 在例程开头调用
pthread_detach(pthread_self()),线程退出时系统自动回收其资源。
4.4 多线程 TCP 服务端代码
cpp
// tcp_server_pthread.cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define BACKLOG 5
// 线程处理函数:处理单个客户端连接【作业核心考点】
void* client_handler(void* arg) {
int sock = *(int*)arg;
delete (int*)arg; // 释放动态参数内存
char buffer[1024];
ssize_t n;
while ((n = read(sock, buffer, sizeof(buffer) - 1)) > 0) {
buffer[n] = '\0';
std::cout << "client[" << sock << "]# " << buffer << std::endl;
write(sock, buffer, n); // 回复响应
}
if (n == 0) std::cout << "client[" << sock << "] close!" << std::endl;
close(sock);
return NULL;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " port\n" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
// 1. 创建监听套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
// 端口复用
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定地址
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) {
std::cerr << "bind error: " << strerror(errno) << std::endl;
return 3;
}
// 3. 开始监听
if (listen(listen_sock, BACKLOG) < 0) {
std::cerr << "listen error: " << strerror(errno) << std::endl;
return 4;
}
// 4. 循环 accept + 创建线程处理连接【作业核心考点】
while (true) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0) {
std::cerr << "accept error: " << strerror(errno) << std::endl;
continue;
}
std::string ip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::cout << "new link: [" << ip << "]:" << cport
<< " sockfd=" << sock << std::endl;
// 动态传参,避免栈变量竞争
int* p_sock = new int(sock);
pthread_t tid;
// 创建线程
if (pthread_create(&tid, NULL, client_handler, p_sock) != 0) {
std::cerr << "pthread_create error" << std::endl;
close(sock);
delete p_sock;
continue;
}
// 分离线程:主线程无需等待,结束自动回收资源【作业核心考点】
pthread_detach(tid);
}
close(listen_sock);
return 0;
}
4.5 演进方向:线程池【进阶拔高】
考察形式:二面追问("每连接一线程有什么问题?")。
- 每连接建线程的问题:连接多时频繁创建 / 销毁线程开销大;线程数过多导致 CPU 调度切换成本飙升、响应变慢;
- 解决:预创建一批线程 + 任务队列(生产者消费者模型),连接到来封装成任务塞队列,空闲线程取任务执行,线程不随连接创建销毁;
- 📌 联动:线程池的完整实现(互斥锁 + 条件变量 + 任务队列)已在《11 - 线程池实现》章节亲手写过,本章只是把「连接处理」封装成任务塞进池子,代码可复用。
5. V3-1 版本:多线程远程命令执行【进阶拔高】
考察形式:项目亮点 / 综合能力展示、安全问追问。
V3-1 把多线程服务器的业务替换为 "远程执行命令并回传结果",是 V1~V3 知识的综合应用,重点考察业务与网络解耦 + 安全边界意识。
核心业务浓缩成一个 Command 类:收命令 → 校验 → 执行 → 回传。
5.1 Command 类:白名单校验 + popen 执行【进阶拔高】
cpp
class Command {
public:
Command(int sockfd) : _sockfd(sockfd) {
// 只允许执行白名单内的少量命令
_safe.insert("ls"); _safe.insert("pwd");
_safe.insert("ls -l"); _safe.insert("whoami");
// 业务可根据需要增删
}
std::string Execute(const std::string& cmd) {
if (!IsSafe(cmd)) return "unsafe"; // ⚠️ 安全校验必须在执行前
FILE* fp = popen(cmd.c_str(), "r"); // popen:执行命令并捕获输出
if (fp == nullptr) return "";
char buf[1024]; std::string result;
while (fgets(buf, sizeof(buf), fp)) result += buf;
pclose(fp);
return result;
}
private:
bool IsSafe(const std::string& cmd) { return _safe.count(cmd) > 0; }
std::set<std::string> _safe;
int _sockfd;
};
⚠️ 面试价值点:
- 命令注入 :直接执行客户端传来的任意字符串 = 高危漏洞(
ls; rm -rf /)。白名单校验是面试官最想听到的安全意识; - popen 与 system 的区别(追问):popen 可捕获命令输出,system 只返回退出状态;
- 无结果命令(如 touch)要回 "done" 兜底,防止客户端空等。