当前位置: 首页 > news >正文

【网络编程】一请求一线程

一、前言

使用 C 语言实现多线程 TCP 服务器,通过调用 pthread_create() 为每个客户端连接创建一个独立线程,完成回显服务(Echo Server)。通信模型采用 一请求一线程(One-Connection-One-Thread) 的方式。

二、TCP套接字流程概览

TCP 是面向连接的协议,在客户端和服务端之间建立可靠的双向通信。基本通信流程如下:

服务端流程

  1. 创建套接字 socket()
  2. 绑定地址和端口 bind()
  3. 监听连接请求 listen()
  4. 接收连接 accept()
  5. 收发数据 recv()/send()
  6. 关闭连接 close()

客户端流程

  1. 创建 socket;
  2. 连接服务器 connect()
  3. 数据交互;
  4. 关闭连接。

三、一请求一线程模型

3.1 流程

使用 pthread 创建线程,具体策略:

  • 主线程执行 accept()
  • 每接收一个客户端连接,就创建一个新线程;
  • 线程内持续使用 recv() 接收数据;
  • 回显数据给客户端,再次循环;
  • 客户端断开连接时,线程关闭该 fd 并结束。

3.2 优缺点

优点:

  1. 结构简单、逻辑清晰
  • 每来一个客户端就 accept(),然后创建一个新线程去处理;
  • 每个线程只管自己的连接,代码非常直观;
  • 不用考虑连接复用、事件回调;
  • 哪个客户端出了问题,对应线程查日志就行,不会影响其他线程。
  • recv() 阻塞接收数据,不用处理 EAGAIN 等非阻塞复杂情况;
  1. 适合连接不多的项目

缺点(不适合高并发)

  1. 线程多 → 系统资源占用高
  • 每个线程都占内存(默认栈大小通常是 1MB);
  • 线程多了容易吃光内存或系统线程数达到上限。
  1. 线程切换开销大
  • 系统频繁在线程之间切换,会占用 CPU;
  • 实际跑业务的时间变少,性能变差。
  1. 没有线程复用,效率低
  • 每个客户端都创建线程,用完就销毁,系统一直在创建+销毁,浪费资源;
  • 没有线程池这种“复用线程”的优化。
  1. 线程退出不处理可能会资源泄漏
  • 如果不写 pthread_detach()pthread_join()
  • 线程退出后资源不会被系统清理 → 僵尸线程越来越多
  1. 不适合高并发(上千连接)
  • 比如聊天室、游戏服务器、网关代理等,一旦连接多了就会卡或崩。

四、完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>// 客户端处理线程函数:收发数据
void *client_thread(void *arg) {int clientfd = *(int *)arg;  // 获取客户端连接的 socketwhile (1) {char buffer[1024] = {0};int count = recv(clientfd, buffer, sizeof(buffer), 0);if (count == -1) {perror("recv");// return -1;} else if (count == 0) {printf("Client disconnected\n");break;} send(clientfd, buffer, count, 0);printf("Received %d bytes: %s\n", count, buffer);}close(clientfd);
}int main() {// 1. 创建 socket(监听套接字)int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 2. 配置服务器地址结构struct sockaddr_in serveraddr;memset(&serveraddr, 0, sizeof(struct sockaddr_in));serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(8888);  // 端口号serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定到所有可用的接口// 3. 绑定地址和端口int ret = bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));if (ret == -1) {perror("bind");return -1;}// 4. 开始监听(最大等待队列长度为10)ret = listen(sockfd, 10);if (ret == -1) {perror("listen");return -1;}// 5. 循环接收客户端连接while (1) {struct sockaddr_in clientaddr;socklen_t len = sizeof(clientaddr);int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);if (clientfd == -1) {perror("accept");return -1;}// 6. 创建线程处理客户端连接pthread_t thid;pthread_create(&thid, NULL, client_thread, &clientfd);}return 0;
}

五、要点总结

1. IO、fd、socket

  • IO(Input/Output):输入输出的统称,程序通过 IO 与外部世界(比如文件、网络)进行数据交互。

    • 在网络编程中,IO 通常指的是“网络数据的读写操作”
  • fd(file descriptor,文件描述符)

    • 是一个非负整数
    • 操作系统用于标识打开的文件、网络连接等资源
    • 所有IO操作(包括网络IO)都是基于fd进行的。
  • socket 是一种特殊的文件描述符,用于网络通信。

    • 本质上:socket = 一种用于网络通信的 fd
  • 在口语中,fd / IO / socket 往往会混着说,语境不同但常指同一件事

2. 为什么要清零?

memset(&serveraddr, 0, sizeof(struct sockaddr_in));
struct sockaddr_in serveraddr = {0};
  • 避免脏数据:若未初始化,结构体可能包含随机值,导致网络编程中出现不可预期的错误(如连接失败、绑定错误等)。

3. 调用 accept() 函数时,必须通过指针传递 len 变量

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);
  • 传入 &len:让 accept() 知道客户端地址缓冲区的初始大小(即 clientaddr 的大小);
  • 接受返回值accept() 可能会将 len 修改为实际填充的地址长度;
  • 这种设计可以兼容 IPv4 和 IPv6 等不同地址结构;
  • 防止地址溢出或截断,确保程序安全。
  • 这种设计允许 accept() 适应不同类型的地址结构(如 IPv4、IPv6),如果客户端使用 IPv6 连接,而服务器分配的是 IPv4 结构,accept() 会将 len 设置为实际使用的长度(可能小于或大于初始值),避免缓冲区溢出。

4. TCP三次握手建立连接不需要代码显式参与

  • TCP三次握手(SYN→SYN+ACK→ACK)由操作系统内核的TCP/IP协议栈自动完成,应用层代码只需通过套接字API进行抽象操作。
  • 服务器通过listen()准备接收连接,accept()阻塞等待握手完成;客户端通过connect()触发握手。握手成功后,accept()返回新的套接字描述符(服务器)或connect()返回0(客户端)。
  • 应用层无需直接编写处理SYN、SYN+ACK、ACK包的代码。

5. 出现大量TIME_WAIT

TIME_WAIT 是 TCP 连接正常关闭后,客户端/服务器端保持连接一段时间(默认 60 秒以上)来确保数据完整关闭的状态。

  • TCP 四次挥手的最后一步,主动关闭连接的一方进入 TIME_WAIT
  • 它会持续一段时间(通常是 2 倍的最大报文生存时间,Linux默认 60 秒或更长)。
  • 它的作用是:
    • 确保最后一个 ACK 能被对方收到;
    • 防止旧连接的数据混入新连接(连接复用时端口相同)。

常见场景:

  • HTTP 请求频繁(短连接);
  • 服务器不断接受短连接(如聊天、爬虫);
  • 自己写的 socket 客户端/服务端每次 connect -> close 都触发。

大量出现:

  • 端口资源会被占满(端口号 + TIME_WAIT = 不可复用);
  • 内核需要维护大量连接状态,影响性能;
  • 新连接会被拒绝或超时。

6. 出现CLOSE_WAIT

对方关闭连接(发送 FIN)后你收到了,但你没有关闭自己的 socket,就会进入 CLOSE_WAIT 状态

  • 表示自己接收到了对方断开连接的请求;
  • 但程序未及时调用 close() 关闭该 socket
  • 连接会卡在 CLOSE_WAIT 状态不释放资源。

7. fd 和 TCP 连接的回收

  1. 什么时候 fd 会被回收?
    • 当调用 close(fd) 的时候,fd 就被“归还”给系统,可以被下一个 socket 复用。
  2. TCP 连接什么时候断?
    • TCP 是网络层的事,fd 是操作系统的事,两者回收机制不同步
    • close(fd) 只是关闭了 fd,但底层的 TCP 连接可能仍处于 TIME_WAIT 状态。
  3. 分层解释
概念属于哪一层回收时机
fd文件系统调用 close() 后立即回收
TCP连接(状态)网络协议栈(内核)等待状态机完成(如TIME_WAIT)

8. 命令 netstat -anop | grep 8888

  • 用于查看当前系统上所有连接的状态;
  • 过滤出与端口 8888 有关的连接;
  • 可以看到每个连接的状态(如 ESTABLISHED、TIME_WAIT、CLOSE_WAIT);
  • -o 还可以显示每个连接对应的 PID,方便定位问题进程。

9. fd 回收机制(延伸理解)

  • **fd 是 int 类型,通常从 3 开始分配(0=stdin,1=stdout,2=stderr);
  • 每次 socket()open() 等调用都会分配新的 fd;
  • close(fd) 后,fd 会被系统回收,并在后续优先重用(越早释放越早被重用);
  • 如果短时间内连接大量断开又快速建立,容易出现 fd 被重复复用(比如第 4 个客户端连接得到的 fd 是前一个断开的 fd);
  • 底层 TCP 状态仍处于 TIME_WAIT,但 fd 已换人使用,这就是 fd 和 TCP 的不同步表现。

10. 查看和修改系统最大 fd 限制

ulimit -n
  • 表示系统允许的最大打开文件描述符数(fd 数)
  • 默认可能是 1024,可以通过配置提升(对高并发服务);
  • 太小会导致连接建立失败、accept 报错等问题。
http://www.lryc.cn/news/612664.html

相关文章:

  • 云原生安全挑战与治理策略:从架构思维到落地实践
  • PyTorch + PaddlePaddle 语音识别
  • 从BaseMapper到LambdaWrapper:MyBatis-Plus的封神之路
  • day44 力扣1143.最长公共子序列 力扣1035.不相交的线 力扣53. 最大子序和 力扣392.判断子序列
  • WEB开发-第二十七天(PHP篇)
  • 笔试——Day31
  • Linux(17)——Linux进程信号(下)
  • 【42】【OpenCV C++】 计算图像某一列像素方差 或 某一行像素的方差;
  • uniapp vue3中使用pinia 和 pinia持久化(没有使用ts)
  • SQLite 创建表
  • VUE+SPRINGBOOT从0-1打造前后端-前后台系统-文章列表
  • [失败记录] 使用HBuilderX创建的uniapp vue3项目添加tailwindcss3的完整过程
  • 《深入浅出 Django ORM:设计理念与惰性查询实现详解》
  • Django 表单:深度解析与最佳实践
  • 上门家教 app 用户端系统模块设计
  • 解锁高效开发:AWS 前端 Web 与移动应用解决方案详解
  • 深度解析:打造一个文件、可持续的Python网络爬虫——以澳洲房地产网站为例
  • uni-app vue3 小程序接入 aliyun-rtc-wx-sdk
  • 深拷贝之 structuredClone ()
  • JavaSE---异常的经典面试题
  • SUNO音乐歌曲生成平台的关键字指令
  • 内网穿透原理和部署教程
  • [激光原理与应用-171]:测量仪器 - 能量型 - 激光能量计(单脉冲能量测量)
  • YOLOv8面试知识
  • Spring事务失效场景?
  • 【基础知识】springboot+vue 基础框架搭建(更新中)
  • 下载 | Windows Server 2016最新原版ISO映像!(集成7月更新、标准版、数据中心版、14393.8246)
  • MacOS Docker 安装指南
  • 进程、网络通信方法
  • 在Linux下访问MS SQL Server数据库