09_百万并发的TCP服务器

百万并发的TCP服务器介绍

上一节已经介绍了TCP服务器的两个版本的实现,跳转:08_TCP服务器:一请求一线程 & epoll
但是即使是使用了epoll也无法做到100万的并发量。本节将针对上一节 08_TCP服务器:一请求一线程 & epoll的代码基础上进行优化,最终实现百万并发的TCP服务器。

并发量是什么?

并发量指的是一秒钟处理的客户端的请求数量,即qbs。

我们首先来测试一下上节使用epoll实现的TCP服务器的TCP_Server.c代码的并发数量, 运行该程序需要指定服务器的端口 (只有1个端口)

准备工作:

  1. 准备四台虚拟机(不建议使用云服务器,因为网络连接的时间开销很大,建议使用本机的虚拟机):

    • 其中一台4G内存,2核CPU (最低配置,当然越大越好)服务器Server的作用:Ubuntu_130
    • 其他三台2G内存,1核CPU (最低配置) 客户端Client的作用:Ubuntu_128/131/132
      image
  2. 服务器运行代码TCP_Server.c (可在08_TCP服务器:一请求一线程 & epoll获取)

gcc -o TCP_Server TCP_Server.c
./TCP_Server 8888
  1. 客户端虚拟机同时运行测试代码mul_port_client_epoll.c
gcc -o mul_port_client_epoll mul_port_client_epoll.c
./mul_port_client_epoll 服务器ip地址 8888

陆续发现以下几个问题。

一. Connection refused问题

当运行客户端测试代码,当服务器连接数达到1024时(clientfd从0开始),此时客户端报错connection refused.
image

image

服务器连接数到达1024后不允许连接,原因是Linux的文件系统默认每个进程的fd最多只可以有1024个。
我们可以使用ulimit -a命令可以查看。
image
解决方法: 修改open files, 有以下两种方案:

  1. 使用ulimit -n来修改, 这种方式是临时生效的方案。
su #切换管理员
ulimit -n 1048576 # 临时修改至1024*1024 => 100w
  1. 编辑/etc/security/limits.conf,永久生效方案。
sudo vim /etc/security/limits.conf

在文件尾部追加, 然后reboot重启即可生效:

*       hard    nofile  1048576
*       soft    nofile  1048576

image

二. Cannot assign requested address

即使我们已经修改了open files,但是再次测试,在客户端上发现了新的问题Cannot assign requested address, 提示连接请求的地址被占用。
image
服务器最大连接数量当前为2万级别:
image

那么这个问题指的是客户端的地址还是服务端地址呢?

思考:sockfd与网络地址之间有什么关系? \(\Rightarrow\) sockfd与ip地址之间有什么关系?

当我们进行send、recv时,传参中的sockfd,包含了五个信息,可以表示为五元组:

sockfd \(\leftarrow \rightarrow\) (远程ip, 远程端口,本机ip,本机端口,协议proto)

那这就会有一种情况,当五元组被用完了sockfd也就不能继续分配了

当前版本的代码里,服务器的远程ip和远程端口192.168.66.130:8888已经确定, 对于客户端来说,ip地址肯定不变,那只有一种情况,客户端的本机端口被用完了,不能再分配了。

解决方法:增加服务器的端口数量 \(\Rightarrow\) 开100个端口
使用循环,创建100个listen端口,sockfd保存在sockfds数组里,然后在主事件中判断当前epoll可读的sockfd是否为listen_fd,即判断是否在sockfds数组里。

TCP服务器-100个端口代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>

#include <errno.h>
#include <fcntl.h>

#include <sys/epoll.h>

#define BUFFER_LENGTH		1024
#define EPOLL_SIZE		1024

#define MAX_PORT		100

int islistenfd(int fd, int *fds) {

	int i = 0;
	for (i = 0;i < MAX_PORT;i ++) {
		if (fd == *(fds+i)) return fd;
	}

	return 0;
}

// ./tcp_server 8888

int main(int argc, char *argv[]) {

	if (argc < 2) {
		printf("Param Error\n");
		return -1;
	}
	
	int port = atoi(argv[1]); // start 
	int sockfds[MAX_PORT] = {0}; // listen fd
	int epfd = epoll_create(1);  

	int i = 0;
	for (i = 0;i < MAX_PORT;i ++) {

		int sockfd = socket(AF_INET, SOCK_STREAM, 0);

		struct sockaddr_in addr;
		memset(&addr, 0, sizeof(struct sockaddr_in));
		addr.sin_family = AF_INET;
		addr.sin_port = htons(port+i); // 8888 8889 8890 8891 .... 8987
		addr.sin_addr.s_addr = INADDR_ANY; 

		if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
			perror("bind");
			return 2;
		}

		if (listen(sockfd, 5) < 0) {
			perror("listen");
			return 3;
		}
		printf("tcp server listen on port : %d\n", port + i);

		struct epoll_event ev;
		ev.events = EPOLLIN; 
		ev.data.fd = sockfd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

		sockfds[i] = sockfd;
	}

	struct epoll_event events[EPOLL_SIZE] = {0};

	while (1) {

		int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5); // -1, 0, 5
		if (nready == -1) continue;

		int i = 0;
		for (i = 0;i < nready;i ++) {
			// 判断当前sockfd是否为listen,即是否在数组sockfd[]里
			int sockfd = islistenfd(events[i].data.fd, sockfds);
			if (sockfd) { // listen 2

				struct sockaddr_in client_addr;
				memset(&client_addr, 0, sizeof(struct sockaddr_in));
				socklen_t client_len = sizeof(client_addr);

				int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);

				fcntl(clientfd, F_SETFL, O_NONBLOCK);

				int reuse = 1;
				setsockopt(clientfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));

				struct epoll_event ev;
				ev.events = EPOLLIN | EPOLLET; 
				ev.data.fd = clientfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);

			} else {

				int clientfd = events[i].data.fd;
				
				char buffer[BUFFER_LENGTH] = {0};
				int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
				if (len < 0) {
					close(clientfd);

					struct epoll_event ev;
					ev.events = EPOLLIN; 
					ev.data.fd = clientfd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
					
				} else if (len == 0) { // disconnect
					close(clientfd);

					struct epoll_event ev;
					ev.events = EPOLLIN; 
					ev.data.fd = clientfd;
					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
					
				} else {
					printf("Recv: %s, %d byte(s), clientfd: %d\n", buffer, len, clientfd);
				}
			}

		}

	}
	return 0;
}

三. Connection timed out

客户端出现Connection timed out问题
image

  • sockfd很像65535,先检查是否为文件系统的配置问题,发现 $ >100w$, 排除。

image

  • 检查是否与防火墙有关

nf_conntrack_max:是 Linux 内核中控制连接跟踪系统(Conntrack)最大条目数的关键参数, 也就是防火墙设置了对外连接的最大数目。

cat /proc/sys/net/netfilter/nf_conntrack_max

image
修改客户端和服务器的nf_conntrack_max:

vim /etc/sysctl.conf # 在最后追加 net.nf_conntrack_max = 1048576
sudo sysctl -p # 生效

image
如果服务器未修改nf_conntrack_max,会报错误Cannot open /porc/meminfo: Too many open files in system

不过使用sudo sysctl -p生效可能会报下面的错误:
image
需要使用下面的命令:

sudo modprobe ip_conntrack
sudo sysctl -p # 生效

四. 内存分配

使用上述代码,重新运行,还是会有遇到新问题,原因是服务器的内存占用拉满,崩溃了。
当服务器的CPU占用率急剧增加的时候,系统会触发系统本身的保护,检测出来哪个进程CPU占用率过高,然后系统会自动回收 内存回收。
因为程序每个fd都有一个tcp接收缓冲区和tcp发送缓冲区。而默认的太大了,导致Linux内存不足,进程被杀死,所以我们需要适当的缩小。进程空间,代码段,堆栈都是要占用内存的。
我们只需要对net.ipv4.tcp_mem,net.ipv4.tcp_wmem,net.ipv4.tcp_rmem进行适合的修改即可
同样的,编辑sysctl.conf文件,sudo vim /etc/sysctl.conf, 在后面追加这几个参数的配置。

net.ipv4.tcp_mem = 252144 524288 786432
# tcp协议栈的大小,单位为内存页(4K),分别是 1G 2G 3G,如果大于2G,tcp协议栈会进行一定的优化,进行内存回收
net.ipv4.tcp_wmem = 1024 1024 2048
# tcp接收缓存区(用于tcp接受滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个接收缓存区
net.ipv4.tcp_rmem = 1024 1024 2048
# tcp发送缓存区(用于tcp发送滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个发送缓存区

同样使用下面的命令生效,建议每次运行前都运行一下,不然重启虚拟机,可能出现未生效的情况。

sudo modprobe ip_conntrack
sudo sysctl -p # 生效

然后重新运行测试,由于我的服务器虚拟机版本是ubuntu 22.04,修改完后,只能达到85万的连接数,于是我通过htop后台打开,发现有许多其他进程占用内存资源(比如我之前安装的mysql占了600Mb),于是我给它又分配了一个gb的内存重新测试。
image

此时,我们再运行测试,最终目标完成了,达到了100w并发量!
image

服务器的极限连接数量为1020103,因为客户端的测试命令最多只能达到340000,最终我们实现了百万连接的TCP服务器!
image
image

posted @ 2025-11-21 10:08  Xiaomostream  阅读(10)  评论(0)    收藏  举报