Linux网络(03):Socket 编程 TCP

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!


引言:《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

⚠️ 注意区分两种 "对端消失"(面试连环追问):

  1. 正常关闭:read 返回 0,可感知;
  2. 异常掉线 (断电、断网):对端不发送 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一次会漏僵尸)。

孙子进程方案

  1. 爷爷(服务端主进程) accept 连接
  2. 爷爷 fork 出爸爸,爸爸再 fork 出孙子
  3. 爷爷 waitpid 回收爸爸(爸爸 fork 完就立即退出,waitpid 立即返回,不阻塞爷爷 accept)
  4. 孙子服务连接,后续孙子变孤儿进程,被 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) {}
};

⚠️ 两大高频坑点:

  1. 参数必须 new 到堆上:若在栈上定义并取地址传给线程,函数返回后栈空间被复用,线程再访问就是脏数据 / 野指针 ------ 经典并发 bug;
  2. 线程例程必须是 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" 兜底,防止客户端空等。
相关推荐
广州灵眸科技有限公司2 小时前
单芯片同时跑 Linux 与 RT-Thread:RV1126B的AMP异构方案完整拆解
linux·运维·服务器
小雪崩2 小时前
嵌入式学习:Linux驱动开发总结
linux·驱动开发·学习
Lynne3092 小时前
TSN 在变电站自动化通信网络升级中的应用价值与技术挑战
服务器·网络·数据库
C++ 老炮儿的技术栈2 小时前
std::atomic 是什么
linux·c语言·网络·c++·visual studio
2401_862880823 小时前
input 子系统框架 、platform 驱动框架
linux·驱动开发·嵌入式硬件
Liuqy-053 小时前
线程的互斥与同步机制——互斥锁、条件变量、无名信号量
linux·开发语言·线程·进程·条件变量·同步·互斥
十年磨一剑~3 小时前
LVDS 背光控制问题排查与 DTS 修正文档
linux·ubuntu
有毒的教程3 小时前
Linux底层存储路由配置
linux·运维·网络