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

Linux 软件编程(十二)网络编程:TCP 并发服务器构建与 IO 多路复用

在网络编程中,服务器需要应对多个客户端的连接与请求,单循环服务器因同一时刻仅能处理一个客户端任务,无法满足高并发场景。而 TCP 并发服务器可实现多客户端同时处理 。

一、TCP 并发服务器

(一)单循环与并发服务器区别

  • 单循环服务器:同一时刻仅能处理一个客户端任务,处理完当前客户端才能响应下一个,效率低。
  • 并发服务器:可同时处理多个客户端任务,提升服务端资源利用率与响应能力。

(二)TCP 连接特性

TCP 基于 “三次握手” 建立 一对一连接 ,可靠传输数据,但需高效并发模型支撑多客户端交互。

二、TCP 服务端并发模型

(一)多进程模型

  1. 原理:服务端通过 fork() 创建子进程,父进程负责监听新连接,子进程处理客户端交互。
  2. 特点
    • 资源开销大:进程有独立地址空间,创建、切换成本高。
    • 安全性高:进程间资源隔离,一个进程异常一般不影响其他进程。
  3. 代码示例
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;void hander(int handnum)
{wait(NULL);
}int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;}seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}int main(int argc, char const *argv[])
{signal(SIGCHLD, hander);int sockfd = init_tcp_recv();struct sockaddr_in cliaddr;socklen_t lenaddr = sizeof(cliaddr);while(1){int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &lenaddr);if(connfd < 0){perror("accept error");return -1;}pid_t pid = fork();if(pid > 0){}else if(pid == 0){char buff[1024] = {0};while(1){memset(buff, 0, sizeof(buff));int ret = recv(connfd, buff, sizeof(buff), 0);if(ret < 0){perror("error recv");break;}else if(0 == ret){printf("[%s:%d]: client off\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));close(connfd);break;}printf("[%s:%d]:%s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);strcat(buff, "---->ok");int cnt = send(connfd, buff, strlen(buff), 0);if(cnt < 0){perror("send error");close(connfd);break;}}}}close(sockfd);return 0;}

(二)多线程模型

  1. 原理:借助 pthread_create() 创建线程,主线程负责 accept 新连接,子线程处理客户端数据收发。
  2. 特点
    • 资源开销小:线程共享进程地址空间,创建、切换成本低于进程,相同资源环境下并发量更高。
    • 需注意同步:线程共享资源,如需客户端地址信息等,需用锁机制(如互斥锁)避免竞争,否则易引发数据混乱,或者创建结构体将客户端信息和通信套接字放在里面,然后线程传参时第四个参数传入结构体的地址,然后进行访问内容。
  3. 伪代码示例
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include<pthread.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;} //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}// struct sockaddr_in cliaddr;
// socklen_t lenaddr = sizeof(cliaddr);void *recv_senf(void *arg)
{int connfd = *((int *)arg);char buff[1024] = {0};while(1){memset(buff, 0, sizeof(buff));int ret = recv(connfd, buff, sizeof(buff), 0);if(ret < 0){perror("error recv");break;}else if(0 == ret){printf("client off\n");close(connfd);break;}printf("%s\n", buff);strcat(buff, "---->ok");int cnt = send(connfd, buff, strlen(buff), 0);if(cnt < 0){perror("send error");close(connfd);break;} }return NULL;
}int main(int argc, char const *argv[])
{int sockfd = init_tcp_recv();if(sockfd < 0){return -1;}pthread_t tid;while(1){int connfd = accept(sockfd, NULL, NULL);if(connfd < 0){perror("accept error");return -1;}pthread_create(&tid, NULL, recv_senf, &connfd);pthread_detach(tid);}close(sockfd);return 0;}

(三)线程池模型

  1. 背景:多线程 / 多进程模型中,频繁创建、销毁线程 / 进程会产生大量时间消耗。线程池基于 生产者 - 消费者模型 与 任务队列 ,预先创建一定数量线程,复用线程处理任务,减少资源开销。
  2. 原理
    • 生产者(主线程):负责 accept 客户端连接,将任务(如处理客户端请求)放入任务队列。
    • 消费者(线程池中的次线程):从任务队列取出任务并执行,执行完可继续获取新任务,无需频繁创建销毁。
  3. 模型示意图
    主线程作为生产者生产任务(如客户端连接处理任务),放入任务队列;多个次线程作为消费者,从队列取任务执行,实现任务高效复用与并发处理。

(四)IO 多路复用模型

  1. 核心思想:让一个进程 / 线程 复用多个文件描述符(fd)的读写操作 ,不创建新进程 / 线程,通过一个进程监测、处理多个文件(如 socket 连接)的读写事件。
  2. 应用场景:需同时监听多个 fd (如 stdinconnfd 等),避免阻塞 IO 导致程序 “卡主”,典型函数有 selectpollepoll 。
(1)select 函数
  • 函数与操作步骤
    • 创建文件描述符集合:用 fd_set 类型,通过 FD_ZERO 清空集合,FD_SET 添加关注的 fd 。
    • 传递集合给内核监测:调用 select 函数,内核监测集合中 fd 的事件(读、写、异常等),监测期间进程阻塞。
    • 处理事件:内核监测到事件后,select 解除阻塞,通过 FD_ISSET 判断具体 fd 事件,执行对应读写操作。
  • 关键函数与 select 原型
  • void FD_CLR(int fd, fd_set *set); 
    int  FD_ISSET(int fd, fd_set *set); 
    void FD_SET(int fd, fd_set *set); 
    void FD_ZERO(fd_set *set); 

    int select(int nfds, fd_set *readfds, fd_set *writefds,
    fd_set *exceptfds, struct timeval *timeout);
    功能:传递文件描述符结合表给内核并等待获取事件结果
    参数:
    nfds : 关注的最大文件描述符+1
    readfds:读事件的文件描述符集合
    writefds:写事件的文件描述符集合
    exceptfds:其他事件的文件描述符集合
    timeout:设置select监测时的超时时间
    NULL : 不设置超时时间(select一直阻塞等待)

            返回值:
    成功:返回内核监测的到达事件的个数
    失败:-1
    0 : 超时时间到达,但没有事件发生,则返回

  • 伪代码示例(结合 TCP 服务端)
#include <sys/select.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include<string.h>
#include <stdio.h>
#include <sys/types.h>          /* See NOTES */#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include<pthread.h>#define SER_PORT 50000
#define SER_IP "192.168.0.149"struct sockaddr_in seraddr;int init_tcp_recv()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("sockfd error");return -1;} //允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1;}ret =listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}int main(int argc, char const *argv[])
{int sockfd = init_tcp_recv();if(sockfd < 0){return -1;}fd_set rdfds;fd_set rdfdstem;FD_ZERO(&rdfds);FD_ZERO(&rdfdstem);FD_SET(sockfd, &rdfds);int maxfd = sockfd; char buff[1024] = {0};while(1){rdfdstem = rdfds;int ret = select(maxfd + 1, &rdfds, NULL, NULL, NULL);if(ret < 0){perror("select error");return -1;}if(FD_ISSET(sockfd, &rdfdstem)){int connfd = accept(sockfd, NULL, NULL);if(connfd < 0){perror("accept error");return -1;}FD_SET(connfd, &rdfds);maxfd = maxfd > connfd ? maxfd : connfd;}for(int i = sockfd + 1;i < maxfd + 1;++i){if(FD_ISSET(i, &rdfdstem)){memset(buff, 0, sizeof(buff));ssize_t cnt  = recv(i, buff, sizeof(buff), 0);if(cnt < 0){perror("recv error");FD_CLR(i, &rdfds);close(i);continue;}else if(cnt == 0){FD_CLR(i, &rdfds);close(i);continue;}printf("%s\n", buff);strcat(buff, "---->ok");cnt = send(i, buff, strlen(buff), 0);if(cnt < 0){perror("send error");FD_CLR(i, &rdfds);close(i);continue;}}}         }close(sockfd);return 0;
}

  • 特点
    • 需维护 fd 集合,每次 select 后需重新添加 fd(因内核会修改集合)。
    • fd 数量受限(受系统默认限制,如 1024 ),高并发场景可能不够用。
(2)poll/epoll 
  • poll:与 select 类似,通过 struct pollfd 数组传递监测的 fd 及事件,解决 select 中 fd 数量受限问题,但本质仍需遍历 fd 判断事件,高并发下效率一般。
  • epoll:Linux 下高效的 IO 多路复用机制,通过 红黑树 维护监测的 fd回调机制 通知事件,无需遍历所有 fd ,高并发场景(如大量客户端连接)效率远高于 select/poll 。

三、总结

TCP 并发服务器构建有多种模式:

  • 多进程模型资源开销大但安全;
  • 多线程模型轻量但需注意同步;
  • 线程池模型优化线程资源管理,适配高频任务场景;
  • IO 多路复用(select/poll/epoll )让单进程实现多 fd 监测,高效处理并发连接。
http://www.xdnf.cn/news/1375345.html

相关文章:

  • PPT处理控件Aspose.Slides教程:在.NET中开发SVG到EMF的转换器
  • 爬虫基础学习 - Xpath
  • 设计模式与设计原则简介——及其设计模式学习方法
  • 优选算法-常见位运算总结
  • uniapp中 ios端 scroll-view 组件内部子元素z-index失效问题
  • 基于 Node.js 的淘宝 API 接口开发:快速构建异步数据采集服务
  • 汽车电气系统的发展演进为测试带来了哪些影响?
  • DeFi协议Lombard能突破比特币生态原生叙事困境吗?
  • 图表可视化地理趋势-Telerik WPF Chart
  • 【Day 35】Linux-主从复制的维护
  • (LeetCode 面试经典 150 题 ) 637. 二叉树的层平均值(深度优先搜索dfs)
  • 亚马逊广告关键词排名提升的五大核心策略解析
  • java简单ssm(spring+springmvc+mybatis)框架结构demo
  • 大模型重构建筑“能耗基因“:企业如何用物联中台打响能源革命?
  • 手写MyBatis第36弹:MyBatis执行流程中SQL命令类型解析
  • 登录业务——密码重置与强制修改初始密码实现思路
  • 【微信小程序】分别解决H5的跨域代理问题 和小程序正常不需要代理问题
  • Coze用户账号设置修改用户名-后端源码
  • map|math
  • 腾讯位置商业授权微信小程序路线规划
  • 【开源工具】基于Flask与Socket.IO的跨平台屏幕监控系统实战(附完整源码)
  • 前端性能优化:从指标监控到全链路落地(2024最新实战指南)
  • 论文阅读:Gorilla: Large Language Model Connected with Massive APIs
  • 深度学习入门:神经网络基础知识
  • lesson47:Linux常用软件使用指南:远程连接、远程拷贝、Vim与Nginx
  • VESA时序检测模块设计verilog实现
  • Ubuntu 24 Server 如何设置无线网络
  • imx6ull-驱动开发篇45——Linux 下 SPI 驱动框架简介
  • d435i相机读取镜头内参和相对之间的外参
  • 艾体宝新闻 | 98%好评率!KnowBe4 连续5年蝉联第一,现开放免费钓鱼测试等你解锁