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

Linux学习-TCP并发服务器构建

TCP 并发服务器与 IO 多路复用详解

一、服务器基础架构

(一)单循环服务器深度解析

单循环服务器采用最简单的工作模式,其核心流程为:

  1. 创建监听套接字并绑定端口
  2. 进入无限循环:
    • 调用accept()阻塞等待客户端连接
    • 连接建立后,在同一循环中处理该客户端的所有请求
    • 完成后关闭连接,再回到循环等待下一个连接

局限性

  • 同一时间只能处理一个客户端,其他客户端必须排队等待
  • 对于长时间连接或处理耗时的请求,会导致服务响应严重延迟
  • 仅适用于测试环境或非常简单的应用场景

(二)并发服务器设计理念

并发服务器的核心目标是打破单循环的限制,实现多客户端的并行处理。其设计需考虑:

  • 连接管理:如何高效接收和管理多个客户端连接
  • 资源分配:如何为每个连接分配适当的系统资源
  • 同步机制:当多个处理单元共享资源时的同步问题
  • 性能优化:减少上下文切换和资源消耗

二、TCP 并发服务器实现模型

(一)多进程并发模型

工作原理

主进程负责监听和接收新连接,每建立一个新连接就创建一个子进程专门处理该连接的所有交互。

完整实现流程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/wait.h>// 信号处理函数,回收僵尸进程
void handle_zombie(int sig) {while (waitpid(-1, NULL, WNOHANG) > 0);
}// 子进程处理客户端请求
void handle_client(int connfd) {char buffer[1024];ssize_t n;while ((n = read(connfd, buffer, sizeof(buffer)-1)) > 0) {buffer[n] = '\0';printf("Received: %s\n", buffer);// 简单回显服务write(connfd, buffer, n);}if (n < 0) {perror("read error");}close(connfd);exit(0);
}int main() {int listenfd, connfd;struct sockaddr_in servaddr, cliaddr;socklen_t clilen;pid_t childpid;// 注册信号处理函数,防止僵尸进程signal(SIGCHLD, handle_zombie);// 创建TCP套接字if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket error");exit(1);}// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(8080);// 绑定套接字到端口if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {perror("bind error");exit(1);}// 开始监听,最大等待队列长度为10if (listen(listenfd, 10) < 0) {perror("listen error");exit(1);}printf("Server listening on port 8080...\n");while (1) {clilen = sizeof(cliaddr);// 接受客户端连接if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {perror("accept error");continue;}// 创建子进程处理客户端if ((childpid = fork()) == 0) {// 子进程关闭监听套接字close(listenfd);// 处理客户端请求handle_client(connfd);}// 父进程关闭连接套接字close(connfd);}return 0;
}
优缺点分析

优点

  • 进程间完全隔离,一个客户端的处理崩溃不会影响其他客户端和主进程
  • 可以充分利用多CPU系统的并行处理能力

缺点

  • 进程创建和销毁的开销大,消耗系统资源多
  • 进程间通信(IPC)复杂,需要使用管道、共享内存等机制
  • 并发量受限,系统能创建的进程数量有限
适用场景
  • 客户端连接数不多,但每个连接的处理逻辑复杂
  • 对安全性和隔离性要求高的场景
  • 需要利用多个CPU核心进行计算的服务

(二)多线程并发模型

工作原理

主线程负责监听和接收新连接,每建立一个新连接就创建一个新线程专门处理该连接的交互。与多进程模型相比,线程共享进程的地址空间,资源消耗更少。

关键实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>// 线程处理函数的参数结构体
typedef struct {int connfd;struct sockaddr_in cliaddr;
} ThreadArgs;// 线程处理函数,处理客户端请求
void *handle_client(void *arg) {ThreadArgs *args = (ThreadArgs *)arg;int connfd = args->connfd;char buffer[1024];ssize_t n;// 分离线程,自动回收资源pthread_detach(pthread_self());free(args);  // 释放动态分配的参数while ((n = read(connfd, buffer, sizeof(buffer)-1)) > 0) {buffer[n] = '\0';printf("Received: %s\n", buffer);// 简单回显服务write(connfd, buffer, n);}if (n < 0) {perror("read error");}close(connfd);return NULL;
}int main() {int listenfd, connfd;struct sockaddr_in servaddr, cliaddr;socklen_t clilen;pthread_t tid;ThreadArgs *args;// 创建TCP套接字if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket error");exit(1);}// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(8080);// 绑定套接字到端口if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {perror("bind error");exit(1);}// 开始监听if (listen(listenfd, 10) < 0) {perror("listen error");exit(1);}printf("Server listening on port 8080...\n");while (1) {clilen = sizeof(cliaddr);// 接受客户端连接if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {perror("accept error");continue;}// 为线程分配参数args = malloc(sizeof(ThreadArgs));args->connfd = connfd;args->cliaddr = cliaddr;// 创建线程处理客户端if (pthread_create(&tid, NULL, handle_client, args) != 0) {perror("pthread_create error");close(connfd);free(args);}}close(listenfd);return 0;
}
优缺点分析

优点

  • 线程创建和切换的开销比进程小很多
  • 线程间共享地址空间,数据共享方便
  • 可以支持比多进程模型更高的并发量

缺点

  • 线程安全问题:多个线程共享资源时需要同步机制(互斥锁、条件变量等)
  • 一个线程崩溃可能导致整个进程崩溃
  • 受限于进程的资源限制
适用场景
  • 中等并发量的网络服务
  • 线程间需要频繁数据交换的场景
  • 对响应速度要求较高的服务

(三)线程池模型

工作原理

线程池是一种池化技术,预先创建一定数量的线程,放在"池"中等待任务。当新的客户端连接到来时,主线程将连接作为任务放入任务队列,线程池中的空闲线程会从队列中取出任务进行处理。

实现关键点
  1. 固定数量的工作线程
  2. 线程安全的任务队列
  3. 任务添加和取出的同步机制
  4. 线程池的创建、销毁和管理接口
优缺点分析

优点

  • 避免了频繁创建和销毁线程的开销
  • 控制并发线程数量,防止资源耗尽
  • 任务处理响应更快,因为线程已经预先创建

缺点

  • 实现相对复杂
  • 线程池大小需要根据应用场景调优
  • 长时间运行的任务可能导致线程池阻塞
适用场景
  • 高并发、短任务的网络服务
  • Web服务器、数据库连接池等
  • 需要限制并发线程数量的场景

三、IO多路复用模型

(一)基本概念

IO多路复用允许一个进程同时监视多个文件描述符,一旦某个描述符就绪(可读、可写或异常),能够通知程序进行相应的处理。这种模型不需要创建大量进程或线程,而是通过一个进程处理多个IO操作。

(二)select模型

工作原理

select函数允许程序监视多个文件描述符,等待其中一个或多个变为就绪状态。它使用三个文件描述符集合分别表示可读、可写和异常事件。

完整实现代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024int main() {int listenfd, connfd, maxfd;struct sockaddr_in servaddr, cliaddr;socklen_t clilen;char buffer[BUFFER_SIZE];fd_set readfds, allfds;int clientfds[MAX_CLIENTS];int i, nready, n;// 初始化客户端文件描述符数组for (i = 0; i < MAX_CLIENTS; i++) {clientfds[i] = -1;}// 创建TCP套接字if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket error");exit(1);}// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(8080);// 绑定套接字到端口if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {perror("bind error");exit(1);}// 开始监听if (listen(listenfd, 10) < 0) {perror("listen error");exit(1);}printf("Server listening on port 8080...\n");// 初始化文件描述符集合FD_ZERO(&allfds);FD_SET(listenfd, &allfds);maxfd = listenfd;while (1) {// 每次调用select前都需要重置readfdsreadfds = allfds;// 调用select等待就绪事件nready = select(maxfd + 1, &readfds, NULL, NULL, NULL);if (nready < 0) {perror("select error");continue;}// 检查监听套接字是否有新连接if (FD_ISSET(listenfd, &readfds)) {clilen = sizeof(cliaddr);if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {perror("accept error");continue;}printf("New client connected\n");// 将新连接添加到客户端数组和文件描述符集合for (i = 0; i < MAX_CLIENTS; i++) {if (clientfds[i] == -1) {clientfds[i] = connfd;FD_SET(connfd, &allfds);if (connfd > maxfd) {maxfd = connfd;}break;}}// 如果客户端数量达到上限if (i == MAX_CLIENTS) {printf("Too many clients\n");close(connfd);}// 如果没有其他就绪事件,继续循环if (--nready <= 0) {continue;}}// 检查客户端套接字的读写事件for (i = 0; i < MAX_CLIENTS; i++) {if ((connfd = clientfds[i]) == -1) {continue;}if (FD_ISSET(connfd, &readfds)) {// 读取客户端数据if ((n = read(connfd, buffer, BUFFER_SIZE - 1)) <= 0) {// 客户端关闭连接if (n < 0) {perror("read error");} else {printf("Client disconnected\n");}close(connfd);FD_CLR(connfd, &allfds);clientfds[i] = -1;} else {// 处理数据,这里简单回显buffer[n] = '\0';printf("Received: %s\n", buffer);write(connfd, buffer, n);}if (--nready <= 0) {break;}}}}close(listenfd);return 0;
}
优缺点分析

优点

  • 跨平台支持好,在Linux、Windows等系统都有实现
  • 实现相对简单,容易理解和使用

缺点

  • 每次调用select都需要将文件描述符集合从用户空间复制到内核空间
  • 文件描述符数量有限制(通常是1024)
  • 需要遍历所有文件描述符来检查就绪状态,效率随描述符数量增加而下降
http://www.xdnf.cn/news/1376569.html

相关文章:

  • 在 Windows 上部署 Go 语言开发环境
  • 数据分析编程第五步:数据准备与整理
  • JoyAgent-JDGenie开源多智能体系统详解:架构、部署与企业级应用案例
  • 5G NR学习笔记 预编码(precoding)和波束赋形(beamforming)
  • 嵌入式第三十九天(TCP多任务并发)
  • QT应用层项目20250822
  • MAX系列FPGA型号对比及低功耗特性分析
  • 【Linux 小实战】自定义 Shell 的编写
  • 把CentOS 7默认yum源改成腾讯云镜像
  • 移动端(微信等)使用 vConsole调试console
  • Web漏洞
  • Vue-24-利用Vue3的element-plus库实现树形结构数据展示
  • 一文详解 LangChain4j AiServices:自动代理实现大模型交互
  • 【datawhale组队学习】RAG技术 -TASK05 向量数据库实践(第三章3、4节)
  • 如何使用windows实现与iphone的隔空投送(AirDrop)
  • linux部署overleaf服务器
  • HarmonyOS布局实战:用声明式UI构建自适应电商卡片
  • 华为鸿蒙HarmonyOS Next基础开发教程
  • 【前端】Devtools使用
  • 毕业项目推荐:28-基于yolov8/yolov5/yolo11的电塔危险物品检测识别系统(Python+卷积神经网络)
  • 极限RCE之三字节RCE
  • Go+Gdal 完成高性能GIS数据空间分析
  • 怎么解决大模型幻觉问题
  • NSSCTF 4th WP
  • React(面试)
  • 深度讲解智能体:ReACT Agent
  • Python包发布与分发策略:从开发到生产的最佳实践(续)
  • 基于 Ultralytics YOLO11与 TrackZone 的驱动的高效区域目标跟踪方案实践
  • Effective c++ 35条款详解
  • 【测试】pytest测试环境搭建