TCP 服务器如何支持高并发?单进程、多进程、多线程模型详解

TCP 服务器如何支持高并发?单进程、多进程、多线程模型详解

 在上一篇博客中,我们基于 UDP 实现了一个简单的群聊模型。

今天,我们正式进入 TCP 网络编程,实现一个最经典的功能 ——

🧾 服务器回显(Echo Server)

就是我们发送的消息,服务器不做处理,直接给我们返回即可。

一、TCP 服务器整体流程

一个最基础的 TCP 服务器,需要经历以下步骤:

socket() bind() listen() accept() read()/write() close()

流程图可以理解为:

创建套接字 → 绑定端口 → 开始监听 → 等待客户端连接 → 收发数据 → 关闭连接

我们都知道TCP是连接的,可靠的传输层协议,所以每一个客户端在访问服务器的时候都会建立连接(也就是我们课本上说的三次握手),在客户端没有申请建立连接的时候,服务器要始终保持这监听状态(调用系统调用接口listen)(因为用户可是一天24小时内任意时间都有可能对服务器进行访问,所以服务器必须始终保持这监听状态,这就好比我们半夜不睡觉,就是刷抖音短视频,我们可从来没有打不开抖音的时候,这就是因为服务器保持着监听状态,即使你半夜进行访问,也可以与你进行连接,不会妨碍你半夜刷抖音),一旦客户端进行申请连接(调用系统调用接口connect),服务器就得与客户端进行连接(调用系统调用接口accept),然后与客户端进行通信,通信结束后进行关闭,这就是一个TCP服务器整体的简单流程。

为什么有 listenfd 还要 accept 生成新的 sockfd?

现在我们先来回答一个关于TCP网络编程中的一个常见的问题就是,为什么我们通过socket已经创建了一个listenfd_,为什么还要系统调用accept再创建一个sockfd呢?

我们用生活中一个简单的例子进行举例,相信大家都和自己的好朋友去过万达商场吧,万达商场的最上面两层都是美食,等你们玩累了进行品尝,而我们经常会看到有一些门店会派一些人拿着店里的传单或者食物,吸引顾客来它们餐厅进行吃饭,现在我们假设这个人叫做张三,这个张三负责的就是进行拉客的工作,现在张三凭借着自己的三寸不烂之舌吸引了一位顾客,将这个顾客带到了它们店里,然后赶紧招呼店里的员工,比如李四,来了两位客人,你来接待以下,然后李四就带着这两位顾客找了一个座位,然后给这两位顾客倒水点菜等等服务,这个时候张三则继续出去外面进行拉客的活,吸引到新的顾客之后,再将其带到店里,再叫王五出来接待贵客,然后王五就像李四一样给顾客提供服务,然后张三又继续出去接客,以此类推。

这就是为什么我们通过socket已经创建了一个listenfd_,为什么还要系统调用accept再创建一个sockfd,这就是因为listenfd_就是上面的张三,而sockfd就是上面的李四,王五等等服务员,我们的TCP是基于连接的协议,所以我们就需要像张三一样的人一直进行监听,一旦有人进行访问的时候,我们就需要再安排一个人进行服务,然后张三继续监听,等到又有新人来的时候,再安排一个人进行服务,这样我们的服务器就可以对每一个客户端保持连接,同时让所有的客户端都可以享受到服务器的服务。

这就是 TCP 的设计思想

  • listenfd:专门负责接收新连接
  • sockfd:专门负责和客户端通信

单进程版本

TCP服务端

class Server { public: Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { } void init() { listenfd_ = socket(AF_INET, SOCK_STREAM, 0); if (listenfd_ < 0) { printf("listenfd_ create error!!!\n"); exit(1); } printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_); struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port_); inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr)); if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { printf("bind fail !!!\n"); exit(2); } printf("bind successful!!!\n"); if (listen(listenfd_, 10) < 0) { printf("listen fail!!!\n"); exit(3); } printf("listen successful!!!\n"); } void service(int sockfd, std::string client_ip, uint16_t client_port) { while (1) { char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("client say:%s\n", buffer); std::string info = "server say:"; info += buffer; write(sockfd, info.c_str(), info.size()); } else if (s == 0) { printf("client quit !!!\n"); break; } else { printf("read error !!!\n"); break; } } } void start() { while (1) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len); if (sockfd < 0) { printf("accept fail!!!\n"); } printf("get a new link !!!, sockfd : %d\n", sockfd); uint16_t client_port = ntohs(client.sin_port); char client_ip[16]; inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip); service(sockfd, client_ip, client_port); close(sockfd); } } ~Server() { close(listenfd_); } private: int listenfd_; uint16_t server_port_; std::string server_ip_; }; int main() { Server srv = Server(8080, "0.0.0.0"); srv.init(); srv.start(); return 0; }

TCP客户端

int main(int argc, char *argv[]) { if (argc != 3) { printf("\n\t Usage: %s serverip serverport!\n"); exit(1); } uint16_t server_port = atoi(argv[2]); std::string server_ip = argv[1]; int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { printf("sockfd fail !!!\n"); exit(2); } printf("sock successful !!!, sockfd : %d\n", sockfd); struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port); inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); if (connect(sockfd, (struct sockaddr *)&server, sizeof server) < 0) { printf("connect fail!!!\n"); } while (1) { std::string message; std::getline(std::cin, message); write(sockfd, message.c_str(), message.size()); char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("%s\n", buffer); } else if (s == 0) { printf("server close\n"); break; } } close(sockfd); return 0; }

这样一个简单的单进程版本的基于TCP的socket网络编程就成功实现了。但是现在有如下的问题:

由于我们的服务器现在是单进程的,所以当一个客户端连接到服务器之后,会调用service进行服务,而我们的service是循环执行,只有当客户端退出的时候,service才能退出,这就导致我们无法继续监听,所以从上图来看到,一个客户端访问的时候,另一个客户端访问的时候是没有反应的,只有当一个客户端退出之后,这个客户端才得以访问,作为一个服务器,这样的问题是不可以存在的,所以现在我们改为对进程版本的TCP服务端,必须保证一个客户端访问的同时,另一个客户端也可以进行访问。

多进程版本 TCP 服务器

双 fork

class Server { public: Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { } void init() { listenfd_ = socket(AF_INET, SOCK_STREAM, 0); if (listenfd_ < 0) { printf("listenfd_ create error!!!\n"); exit(1); } printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_); struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port_); inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr)); if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { printf("bind fail !!!\n"); exit(2); } printf("bind successful!!!\n"); if (listen(listenfd_, 10) < 0) { printf("listen fail!!!\n"); exit(3); } printf("listen successful!!!\n"); } void service(int sockfd, std::string client_ip, uint16_t client_port) { while (1) { char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("client say:%s\n", buffer); std::string info = "server say:"; info += buffer; write(sockfd, info.c_str(), info.size()); } else if (s == 0) { printf("client quit !!!\n"); break; } else { printf("read error !!!\n"); break; } } } void start() { while (1) { struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len); if (sockfd < 0) { printf("accept fail!!!\n"); } printf("get a new link !!!, sockfd : %d\n", sockfd); uint16_t client_port = ntohs(client.sin_port); char client_ip[16]; inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip); //单进程版本 // service(sockfd, client_ip, client_port); // close(sockfd); //多进程版本 pid_t id = fork(); if (id == 0) { close(listenfd_); if (fork() > 0) { exit(0); } service(sockfd, client_ip, client_port); close(sockfd); exit(0); } close(sockfd); pid_t ret = waitpid(id, nullptr, 0); } } ~Server() { close(listenfd_); } private: int listenfd_; uint16_t server_port_; std::string server_ip_; };

通过这样的方式就可以很好的保证多个客户端都可以享受到服务器的服务

有很多人可能看不懂这段代码,现在我来进行解释以下,首先我们创建子进程之后,父进程必须关掉sockfd(也就是李四对应的文件描述符),这是因为随着客户端的访问人数增多,如果我们不关闭,就会导致文件描述符一直增多,并且我们将我们的任务交给子进程处理之后,父进程就不需要再继续占用这个文件描述符了,全权交给子进程就可以了,这样就可以缓解父进程文件描述符一直增多的问题。

而在我们的子进程中,我为什么要继续创建子进程呢?这是因为我们创建完子进程之后,父进程需要对子进程的退出状态进行等待回收,这就会导致我们的父进程被阻塞,影响我们与下一个客户端的连接,因此我们可以直接在子进程中再创建子进程(孙子进程),然后让子进程直接退出,这样父进程就可以直接获取到退出结果,也就不需要继续等待,而对于孙子进程,当它的父进程(也就是子进程)退出之后,孙子进程就会被托孤,交给我们的操作系统进行回收,所以我们不必担心孙子进程会成为僵尸进程。这样就实现了一个多进程版本的TCP服务端。

简而言之就是如下:

1️⃣ 父进程必须关闭 sockfd

否则:

  • 文件描述符会不断增加
  • 造成资源泄漏

因为任务已经交给子进程,父进程没必要再持有。


2️⃣ 双 fork 防止僵尸进程

父进程 └── 子进程 └── 孙子进程

当子进程退出:

  • 孙子进程变成孤儿进程
  • 由 init 接管
  • 自动回收

忽略 SIGCHLD信号

我们还可以不用父进程进行等待,利用我们在信号那里学到的东西,通过对SIGCHLD信号进行忽略,这样子进程在退出之后,也不会出现僵尸进程,同时父进程还可以继续监听。

class Server { public: Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { } void init() { listenfd_ = socket(AF_INET, SOCK_STREAM, 0); if (listenfd_ < 0) { printf("listenfd_ create error!!!\n"); exit(1); } printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_); struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port_); inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr)); if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { printf("bind fail !!!\n"); exit(2); } printf("bind successful!!!\n"); if (listen(listenfd_, 10) < 0) { printf("listen fail!!!\n"); exit(3); } printf("listen successful!!!\n"); } void service(int sockfd, std::string client_ip, uint16_t client_port) { while (1) { char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("client say:%s\n", buffer); std::string info = "server say:"; info += buffer; write(sockfd, info.c_str(), info.size()); } else if (s == 0) { printf("client quit !!!\n"); break; } else { printf("read error !!!\n"); break; } } } void start() { while (1) { signal(SIGCHLD, SIG_IGN); struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len); if (sockfd < 0) { printf("accept fail!!!\n"); } printf("get a new link !!!, sockfd : %d\n", sockfd); uint16_t client_port = ntohs(client.sin_port); char client_ip[16]; inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip); // 单进程版本 // service(sockfd, client_ip, client_port); // close(sockfd); // 多进程版本 // pid_t id = fork(); // if (id == 0) // { // close(listenfd_); // if (fork() > 0) // { // exit(0); // } // service(sockfd, client_ip, client_port); // close(sockfd); // exit(0); // } // close(sockfd); // pid_t ret = waitpid(id, nullptr, 0); // 信号 pid_t id = fork(); if (id == 0) { close(listenfd_); service(sockfd, client_ip, client_port); close(sockfd); exit(0); } close(sockfd); } } ~Server() { close(listenfd_); } private: int listenfd_; uint16_t server_port_; std::string server_ip_; };
signal(SIGCHLD, SIG_IGN);

让内核自动回收子进程。

多线程版本 TCP 服务器

但是当客户越来越多时,我们通过创建多线程的方式实在很消耗资源,所以我们还可以通过多线程的方式。

class Server; struct ThreadData { ThreadData(int sockfd, int16_t client_port, std::string client_ip, Server *srv) : sockfd_(sockfd), client_port_(client_port), client_ip_(client_ip), srv_(srv) { } int sockfd_; int16_t client_port_; std::string client_ip_; Server *srv_; }; class Server { public: Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { } void init() { listenfd_ = socket(AF_INET, SOCK_STREAM, 0); if (listenfd_ < 0) { printf("listenfd_ create error!!!\n"); exit(1); } printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_); struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port_); inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr)); if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { printf("bind fail !!!\n"); exit(2); } printf("bind successful!!!\n"); if (listen(listenfd_, 10) < 0) { printf("listen fail!!!\n"); exit(3); } printf("listen successful!!!\n"); } void service(int sockfd, std::string client_ip, uint16_t client_port) { while (1) { char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; printf("client say:%s\n", buffer); std::string info = "server say:"; info += buffer; write(sockfd, info.c_str(), info.size()); } else if (s == 0) { printf("client quit !!!\n"); break; } else { printf("read error !!!\n"); break; } } } static void *routine(void *arg) { pthread_detach(pthread_self()); ThreadData *td = (ThreadData *)arg; td->srv_->service(td->sockfd_, td->client_ip_, td->client_port_); close(td->sockfd_); delete td; return nullptr; } void start() { while (1) { signal(SIGCHLD, SIG_IGN); struct sockaddr_in client; socklen_t len = sizeof(client); int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len); if (sockfd < 0) { printf("accept fail!!!\n"); } printf("get a new link !!!, sockfd : %d\n", sockfd); uint16_t client_port = ntohs(client.sin_port); char client_ip[16]; inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip); // 单进程版本 // service(sockfd, client_ip, client_port); // close(sockfd); // 多进程版本 // pid_t id = fork(); // if (id == 0) // { // close(listenfd_); // if (fork() > 0) // { // exit(0); // } // service(sockfd, client_ip, client_port); // close(sockfd); // exit(0); // } // close(sockfd); // pid_t ret = waitpid(id, nullptr, 0); // 信号 // pid_t id = fork(); // if (id == 0) // { // close(listenfd_); // service(sockfd, client_ip, client_port); // close(sockfd); // exit(0); // } // close(sockfd); // 多线程版本 pthread_t tid; ThreadData *td = new ThreadData(sockfd, client_port, client_ip, this); pthread_create(&tid, nullptr, routine, td); } } ~Server() { close(listenfd_); } private: int listenfd_; uint16_t server_port_; std::string server_ip_; };

对于多线程版本的TCP服务器,我们要注意类成员函数默认隐藏一个 this 指针:

void routine(Server* this, void*)

而我们的多线程的处理函数中要求函数签名为:

void* (*)(void*)

因此就会造成类型不匹配的问题,为了解决这个问题我们要让它成为一个全局的函数,所以要增加static:

static void* routine(void*)

而且全局函数想要调用类内方法是不可以的,所以我们增加了一个this指针,这样就可以调用类内的方法了:

struct ThreadData { ThreadData(int sockfd, int16_t client_port, std::string client_ip, Server *srv) : sockfd_(sockfd), client_port_(client_port), client_ip_(client_ip), srv_(srv) { } int sockfd_; int16_t client_port_; std::string client_ip_; Server *srv_; };
            pthread_t tid;

            ThreadData *td = new ThreadData(sockfd, client_port, client_ip, this);

            pthread_create(&tid, nullptr, routine, td);

三种模型对比总结

模型并发能力资源消耗适用场景
单进程理解
多进程中小并发
多线程常见服务器

到这里,我们从最简单的单进程版本,一步一步改造成多进程、多线程版本。

代码变多了,结构也复杂了,但核心其实很简单:

让服务器同时服务多个客户端。

单进程能跑起来,多进程能并发,多线程更高效。

现在相信我们对 TCP 服务器的整体框架就有了一定清晰的认识了。

Read more

Visual C++ 6.0 中文版安装包下载及 Win11 安装教程

Visual C++ 6.0 中文版安装包下载及 Win11 安装教程

本文分享的是 Visual C++ 6.0(简称 VC++ 6.0)中文版的安装包下载及安装教程,包括在 Win11 系统下的安装和使用问题解答。如果您在安装过程中遇到任何问题,请随时留言寻求帮助! 一、安装包的下载 vc6.0 安装包下载链接:点击这里下载 https://pan.quark.cn/s/bc24c385ee87 二、安装 VC++ 6.0 等待安装完成: 点击“安装”开始安装: 创建桌面快捷方式,然后点击“下一步”。 完成后点击“下一步”。 选择 C 盘以外的盘符: 更改安装路径,建议不要安装在 C 盘(默认盘符),可以选择其他的盘符,

By Ne0inhk
C++ 14 红黑树:高效平衡的奥秘

C++ 14 红黑树:高效平衡的奥秘

红黑树的概念 红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。 规则 1. 每个结点不是红⾊就是⿊⾊2. 根结点是⿊⾊的3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的红⾊结点。4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点 为上面的红黑树,有多少条路径 答案是10条,因为要走到空才算一条 在上面的规则与图中我们发现,根节点一定为黑,

By Ne0inhk
【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“

【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“

欢迎来到一整颗红豆的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡 本文由一整颗红豆原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟 个人主页 👉 一整颗红豆 本文专栏 ➡️C++ 进阶之路 礼器九鼎,釉下乾坤,多态中的 "风水寻龙诀" * 多态的概念 * 编译时多态(静态多态) * `编译时多态的实现方式有,函数重载,运算符重载和模板。` * 函数重载 `Function Overloading` * 运算符重载(`Operator Overloading`) * 模板(`Templates`) * 编译时多态的特点 * 静态绑定(`Static Binding`) * 类型安全(`Type Safety`) * 无运行时开销 * 代码膨胀(`Code bloat`) * 运行时多态(动态多态) * 认识虚函数(`Virtual function`

By Ne0inhk

【提升代码健壮性】:C++网络模块兼容性优化的7个关键步骤

第一章:C++网络模块兼容性概述 在现代分布式系统和跨平台应用开发中,C++网络模块的兼容性成为影响软件稳定性和可移植性的关键因素。由于不同操作系统(如Windows、Linux、macOS)在网络API设计上的差异,开发者常面临套接字接口不一致、字节序处理分歧以及库依赖冲突等问题。 跨平台网络接口差异 * Windows 使用 Winsock API,需显式调用 WSAStartup() 初始化网络环境 * Unix-like 系统采用 POSIX socket 接口,无需额外初始化 * 错误码获取方式不同:Windows 使用 WSAGetLastError(),而 Linux 使用 errno 抽象层设计建议 为提升兼容性,推荐通过封装统一接口隔离底层差异。例如: // 跨平台套接字初始化封装 int initialize_network() { #ifdef _WIN32 WSADATA wsa; return WSAStartup(MAKEWORD(

By Ne0inhk