Linux(服務器編程):43---百萬併發服務器系統參數調優

一、本文目的

  • 在編寫服務器時,如果服務器的設計初衷是要可以承擔百萬、千萬的客戶端連接,那麼默認的情況下,Linux操作系統提供的相關配置參數(比如說進程可分配的文件數目等)是不能夠滿足我們的程序需求的,因此需要自己調整系統的相關參數

併發的概念是什麼?什麼是併發?

  • 對於服務器併發的概念,下面幾點是錯誤的定義:
    • ①服務器處理客戶端請求的數量:沒有時間、空間等限制,因此不能作爲併發的概念
    • ②單位時間內,能夠處理請求的數量:這也是不正確的定義,因爲這個定義是針對於服務器吞吐量(qps)的,而不是併發
    • 其他等......
  • 下面幾點組成在一起可以作爲服務器“併發”的概念:
    • ①服務器能夠同時承擔的客戶端數量(最基本要求)
    • ②能處理指定數量以上的相應請求
    • ③能夠對數據庫進行操作
    • ④有磁盤的操作
    • ⑤CPU的佔用率最好不要超過60%
    • ⑥內存佔用率最好不要超過80%

二、本文環境搭建

  • 本文準備了兩份代碼,作爲測試環境:
    • reactor.c:作爲服務端,採用單進程reactor模式編寫,持續接收客戶端的連接,並且與客戶端有數據的讀寫(recv()、send())
    • mul_port_client_epoll.c:作爲客戶端,會向reactor.c服務端發起不超過340000的客戶端連接,並且每個客戶端都會與服務端有數據的讀寫(recv()、send())

reactor.c

// reactor.c
// 源碼鏈接: https://github.com/dongyusheng/csdn-code/blob/master/server-client/reactor.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#define BUFFER_LENGTH		4096
#define MAX_EPOLL_EVENTS	1024
#define SERVER_PORT         8888

typedef int NCALLBACK(int ,int, void*);

struct ntyevent {
	int fd;      
	int events;  
	void *arg;  
	int (*callback)(int fd, int events, void *arg);
	int status;                 
	char buffer[BUFFER_LENGTH];
	int length;
	long last_active;
};

struct ntyreactor {
	int epfd;           
	struct ntyevent *events;
};

int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
int nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg) {
	ev->fd = fd;
	ev->callback = callback;
	ev->events = 0;
	ev->arg = arg;
	ev->last_active = time(NULL);
	
	return 0;
}

int nty_event_add(int epfd, int events, struct ntyevent *ev) {

	struct epoll_event ep_ev = {0, {0}};
	ep_ev.data.ptr = ev;
	ep_ev.events = ev->events = events;

	int op;
	if (ev->status == 1) { 
		op = EPOLL_CTL_MOD;
	} else {
		op = EPOLL_CTL_ADD; 
		ev->status = 1;
	}

	if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {
		printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		return -1;
	}

	return 0;
}

int nty_event_del(int epfd, struct ntyevent *ev) {

	struct epoll_event ep_ev = {0, {0}};
	if (ev->status != 1) {
		return -1;
	}

	ep_ev.data.ptr = ev;
	ev->status = 0;
	epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);

	return 0;
}

int recv_cb(int fd, int events, void *arg) {
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = reactor->events + fd;

	int len = recv(fd, ev->buffer, BUFFER_LENGTH, 0);
	nty_event_del(reactor->epfd, ev);

	if (len > 0) {
		
		ev->length = len;
		ev->buffer[len] = '\0';

		printf("C[%d]:%s\n", fd, ev->buffer);

		nty_event_set(ev, fd, send_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLOUT, ev);
		
		
	} else if (len == 0) {

		close(ev->fd);
		printf("[fd=%d] pos[%ld], closed\n", fd, ev-reactor->events);
		 
	} else {
		//if(errno == EAGAIN || errno == EWOULDBLOCK)
		//	continue;
		return 0;
		
		close(ev->fd);
		printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
		
	}

	return len;
}

int send_cb(int fd, int events, void *arg) {

	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = reactor->events + fd;

	int len = send(fd, ev->buffer, ev->length, 0);
	if (len > 0) {
		printf("send[fd=%d], [%d]%s\n", fd, len, ev->buffer);
		nty_event_del(reactor->epfd, ev);
		nty_event_set(ev, fd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, ev);
		
	} else {
		close(ev->fd);
		nty_event_del(reactor->epfd, ev);
		printf("send[fd=%d] error %s\n", fd, strerror(errno));
	}

	return len;
}

int accept_cb(int fd, int events, void *arg) {
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	if (reactor == NULL) return -1;
	struct sockaddr_in client_addr;
	socklen_t len = sizeof(client_addr);
	int clientfd;
	if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {
		if (errno != EAGAIN && errno != EINTR) {
		}
		printf("accept: %s\n", strerror(errno));
		return -1;
	}

	int i = 0;
	do {
		for (i = 0;i < MAX_EPOLL_EVENTS;i ++) {
			if (reactor->events[i].status == 0) {
				break;
			}
		}
		if (i == MAX_EPOLL_EVENTS) {
			printf("%s: max connect limit[%d]\n", __func__, MAX_EPOLL_EVENTS);
			break;
		}

		int flag = 0;
		if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
			printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
			break;
		}
		nty_event_set(&reactor->events[clientfd], clientfd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[clientfd]);
	} while (0);
	printf("new connect [%s:%d][time:%ld], pos[%d]\n", 
		inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), reactor->events[i].last_active, i);
	return 0;
}

int init_sock(short port) {
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	fcntl(fd, F_SETFL, O_NONBLOCK);
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(port);
	bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (listen(fd, 20) < 0) {
		printf("listen failed : %s\n", strerror(errno));
	}
	return fd;
}

int ntyreactor_init(struct ntyreactor *reactor) {
	if (reactor == NULL) return -1;
	memset(reactor, 0, sizeof(struct ntyreactor));
	reactor->epfd = epoll_create(1);
	if (reactor->epfd <= 0) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		return -2;
	}
	reactor->events = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
	if (reactor->events == NULL) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		close(reactor->epfd);
		return -3;
	}
	return 0;
}

int ntyreactor_destroy(struct ntyreactor *reactor) {
	close(reactor->epfd);
	free(reactor->events);
}

int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor) {
	if (reactor == NULL) return -1;
	if (reactor->events == NULL) return -1;
	nty_event_set(&reactor->events[sockfd], sockfd, acceptor, reactor);
	nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[sockfd]);
	return 0;
}

int ntyreactor_run(struct ntyreactor *reactor) {
	if (reactor == NULL) return -1;
	if (reactor->epfd < 0) return -1;
	if (reactor->events == NULL) return -1;
	struct epoll_event events[MAX_EPOLL_EVENTS+1];
	int checkpos = 0, i;
	while (1) {
		long now = time(NULL);
		for (i = 0;i < 100;i ++, checkpos ++) {
			if (checkpos == MAX_EPOLL_EVENTS) {
				checkpos = 0;
			}
			if (reactor->events[checkpos].status != 1) {
				continue;
			}
			long duration = now - reactor->events[checkpos].last_active;
			if (duration >= 60) {
				close(reactor->events[checkpos].fd);
				printf("[fd=%d] timeout\n", reactor->events[checkpos].fd);
				nty_event_del(reactor->epfd, &reactor->events[checkpos]);
			}
		}
		int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
		if (nready < 0) {
			printf("epoll_wait error, exit\n");
			continue;
		}
		for (i = 0;i < nready;i ++) {
			struct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;
			if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
			if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
		}
	}
}

int main(int argc, char *argv[]) {
	unsigned short port = SERVER_PORT;
	if (argc == 2) {
		port = atoi(argv[1]);
	}
	int sockfd = init_sock(port);
	struct ntyreactor *reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
	ntyreactor_init(reactor);
	ntyreactor_addlistener(reactor, sockfd, accept_cb);
	ntyreactor_run(reactor);
	ntyreactor_destroy(reactor);
	close(sockfd);
	return 0;
}

mul_port_client_epoll.c

// mul_port_client_epoll.c
// 源碼鏈接: https://github.com/dongyusheng/csdn-code/blob/master/server-client/mul_port_client_epoll.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>

#define MAX_BUFFER		128
#define MAX_EPOLLSIZE	(384*1024)
#define MAX_PORT		1

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

int isContinue = 0;

static int ntySetNonblock(int fd) {
	int flags;

	flags = fcntl(fd, F_GETFL, 0);
	if (flags < 0) return flags;
	flags |= O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) return -1;
	return 0;
}

static int ntySetReUseAddr(int fd) {
	int reuse = 1;
	return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}



int main(int argc, char **argv) {
	if (argc <= 2) {
		printf("Usage: %s ip port\n", argv[0]);
		exit(0);
	}

	// 獲取要連接的服務端的ip和端口
	const char *ip = argv[1];
	int port = atoi(argv[2]);
	
	int connections = 0;
	char buffer[128] = {0};
	int i = 0, index = 0;

	// 創建epoll
	struct epoll_event events[MAX_EPOLLSIZE];
	int epoll_fd = epoll_create(MAX_EPOLLSIZE);
	
	strcpy(buffer, " Data From MulClient\n");
		
	// 初始化服務器地址
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(ip);

	struct timeval tv_begin;
	gettimeofday(&tv_begin, NULL);

	while (1) 
	{
		if (++index >= MAX_PORT) index = 0;
		
		struct epoll_event ev;
		int sockfd = 0;

		// 如果連接數小於340000,繼續連接服務器
		if (connections < 340000 && !isContinue) 
		{
			sockfd = socket(AF_INET, SOCK_STREAM, 0);
			if (sockfd == -1) {
				perror("socket");
				goto err;
			}

			//ntySetReUseAddr(sockfd);
			addr.sin_port = htons(port + index);
			
			// 連接服務器
			if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
				perror("connect");
				goto err;
			}
			
			ntySetNonblock(sockfd);  // 將套接字設置爲非阻塞
			ntySetReUseAddr(sockfd); // 設置可重用本地地址

			// 向服務器發送數據
			sprintf(buffer, "Hello Server: client --> %d\n", connections);
			send(sockfd, buffer, strlen(buffer), 0);

			// 將套接字設置爲可讀可寫,然後加入到epoll_wait()中
			ev.data.fd = sockfd;
			ev.events = EPOLLIN | EPOLLOUT;
			epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
		
			connections ++;
		}
		
		// 如果每連接了一千個客戶端或者連接數超過340000,那麼就執行這個條件
		if (connections % 1000 == 999 || connections >= 340000) 
		{
			struct timeval tv_cur;
			memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
			
			gettimeofday(&tv_begin, NULL);

			// 打印一下每連接1000個客戶端所消耗的時間
			int time_used = TIME_SUB_MS(tv_begin, tv_cur);
			printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used);

			// 進行epoll_wait()
			int nfds = epoll_wait(epoll_fd, events, connections, 100);
			for (i = 0;i < nfds;i ++) 
			{
				int clientfd = events[i].data.fd;

				// 執行寫
				if (events[i].events & EPOLLOUT) {
					sprintf(buffer, "data from %d\n", clientfd);
					send(sockfd, buffer, strlen(buffer), 0);
				} 
				// 執行讀
				else if (events[i].events & EPOLLIN) {
					char rBuffer[MAX_BUFFER] = {0};				
					ssize_t length = recv(sockfd, rBuffer, MAX_BUFFER, 0);
					if (length > 0) {
						printf(" RecvBuffer:%s\n", rBuffer);

						if (!strcmp(rBuffer, "quit")) {
							isContinue = 0;
						}
						
					} else if (length == 0) {
						printf(" Disconnect clientfd:%d\n", clientfd);
						connections --;
						close(clientfd);
					} else {
						if (errno == EINTR) continue;

						printf(" Error clientfd:%d, errno:%d\n", clientfd, errno);
						close(clientfd);
					}
				} else {
					printf(" clientfd:%d, errno:%d\n", clientfd, errno);
					close(clientfd);
				}
			}
		}

		// 休眠1000微秒(0.01秒)
		usleep(1 * 1000);
	}
	return 0;
err:
	printf("error : %s\n", strerror(errno));
	return 0;
}

三、第1次測試

測試如下

  • 左側運行服務端程序reactor(111.229.177.161:8888),右側運行客戶端程序mul_port_client_epoll去連接服務器
  • 效果如下:
    • 左側服務端接收了1021個客戶端(除去描述符0、1、2)之後程序報錯,顯示無法繼續接收客戶端的連接
    • 右側客戶端也報錯,無法繼續連接服務端

原因分析

  • 默認情況下,系統有限制,一個進程做多隻能1024個文件(或文件描述符)。我們的客戶端發起來了超過1024個客戶端的連接,因此程序報錯
  • 通過ulimit命令可以查看,如下:

解決方案(修改最大文件數)

  • 第一步
    • 通過ulimit命令或者修改/etc/security/limits.conf配置文件,修改用戶可打開的最大文件描述符個數。由於ulimit命令修改的結果不能永久保存,因此我們此處修改/etc/security/limits.conf配置文件
    • (下面圖1)下面修改/etc/security/limits.conf,將所有用戶可打開的文件描述符個數修改爲1048576(1024*1024)
    • (下面圖2)修改完成之後退出當前會話(不必重啓),然後重新開啓會話,通過ulinit命令可以看到修改成功
    • 然後再把客戶端的機器也全部修改一下,別忘記了

  • 第二步
    • (下面圖1)上面我們雖然修改了/etc/security/limits.conf,但是該配置文件約束的是“用戶級別”的系統限制。還有兩個“內核級別”的配置參數與文件描述符有關。一個爲nr_open(表示單個進程打開文件句柄數上限),另一個爲file-max(表示系統範圍內所有進程可打開的文件句柄的數量限制)
      • 其中nr_open默認爲1048576,我們就不需要修改了
      • file-max默認爲180566,因此我們需要將其修改爲1048576
    • (下面圖2)修改/etc/sysctl.conf文件,在裏面修改file-max的值爲1048576,然後保存退出
    • (下面圖3)執行sysctl的命令,將/etc/sysctl.conf文件的內容更新到/proc/sys/fs/file-max中生效,令執行完之後查看/proc/sys/fs/file-max文件,更新成功
    • 然後再把客戶端的機器也全部修改一下,別忘記了

  • 第三步
    • 上面我們將進程可分配描述符數量都增大了,那麼還需要修改服務端程序reactor.c中的MAX_EPOLL_EVENTS宏,因爲其代表的是epoll_wait()可以處理事件的數量,因爲客戶端數量增加了,因此該宏也要增加
    • 下面我們將該宏設置爲1024*512=524288(由於本人機器內存不足,只能修改這個大,如果修改再大程序就運行不了,因爲無法爲程序分配更多的內存。如果你的機器內存夠大,那麼可以將這個數值再往上調;如果你的機器內存不足,那麼就將這個值調小)
    • 修改完成之後重新編譯reactor.c
#define MAX_EPOLL_EVENTS    (1024 * 512)

  • 上面所有的內容修改完成之後,進入下面的第2次測試

四、第2次測試

測試如下

  • 左側運行服務端程序reactor(111.229.177.161:8888),右側運行兩個客戶端程序mul_port_client_epoll去連接服務器
  • 效果如下:
    • 左側服務端持續接收客戶端的連接
    • 右側兩個客戶端向服務端發起連接,其中一個客戶端只連接了兩萬多個就報錯退出了,另一個客戶端只連接了一萬多個就報錯退出了
    • 左側服務端沒有報錯,只是沒有客戶端再連接進來了

原因分析

  • 此處要介紹“五元組”的概念了,這個概念在下面的幾次測試中都要用到
  • 一個套接字fd就對應一個“五元組”,一個五元組包含下面幾項內容:
    • 1.源IP地址
    • 2.目的IP地址
    • 3.源端口
    • 4.目的端口
    • 5.協議類型(TCP、UDP等)

  • 因此,我們上面的兩個客戶端加起來最多隻能創建四萬個套接字就報錯的原因是:
    • 源IP地址、目的IP地址、協議類型(TCP)這三者都保持不變、目的端口只有一個也保持不變,唯一可以變化的就是源端口(也就是客戶端的端口)
    • 我們知道,一個系統的端口最多隻能有65535個,其中有一些已經被其他服務使用了,因此客戶端可以使用的端口大概只有四五萬個,所以客戶端也就只能創建四五萬個套接字,所以上面我們兩個客戶端總共加起來只創建了四五萬個套接字就不能再繼續創建套接字了
    • 概括來說,你能有多少種“五元組”類型,那麼你就能建立多少個套接字連接

解決方案

  • 通過上面的分析我們知道,主要的限制原因就在端口的限制上,因此下面我們讓服務端程序監聽在多個端口上,這樣一來可以使用的“五元組”組合就可以更多,那麼我們可以創建的套接字fd也就更多

  • 第一步:
    • 修改服務端程序reactor.c,使其監聽在5個端口上,也就是啓動了5個Tcp Server(隨意多少個,只要端口多了,就能承載更多的客戶端,此處我們就只設置爲5個)
    • 代碼變化不多,只是新增了一個LISTEN_PORT_COUNT宏,然後修改了main()函數使其監聽在5個端口上,其餘代碼全部不變
    • 修改完成之後重新編譯
// reactor.c其餘代碼不變, 只需要修改下面的內容即可
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#define BUFFER_LENGTH		4096
#define MAX_EPOLL_EVENTS	1024
#define SERVER_PORT         8888

// 新增一個宏, 表示監聽的端口數量
#define LISTEN_PORT_COUNT   5

typedef int NCALLBACK(int ,int, void*);

struct ntyevent {
	int fd;      
	int events;  
	void *arg;  
	int (*callback)(int fd, int events, void *arg);
	int status;                 
	char buffer[BUFFER_LENGTH];
	int length;
	long last_active;
};

struct ntyreactor {
	int epfd;           
	struct ntyevent *events;
};

int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
int nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg) {
	ev->fd = fd;
	ev->callback = callback;
	ev->events = 0;
	ev->arg = arg;
	ev->last_active = time(NULL);
	
	return 0;
}

int nty_event_add(int epfd, int events, struct ntyevent *ev) {

	struct epoll_event ep_ev = {0, {0}};
	ep_ev.data.ptr = ev;
	ep_ev.events = ev->events = events;

	int op;
	if (ev->status == 1) { 
		op = EPOLL_CTL_MOD;
	} else {
		op = EPOLL_CTL_ADD; 
		ev->status = 1;
	}

	if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {
		printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		return -1;
	}

	return 0;
}

int nty_event_del(int epfd, struct ntyevent *ev) {

	struct epoll_event ep_ev = {0, {0}};
	if (ev->status != 1) {
		return -1;
	}

	ep_ev.data.ptr = ev;
	ev->status = 0;
	epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);

	return 0;
}

int recv_cb(int fd, int events, void *arg) {
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = reactor->events + fd;

	int len = recv(fd, ev->buffer, BUFFER_LENGTH, 0);
	nty_event_del(reactor->epfd, ev);

	if (len > 0) {
		
		ev->length = len;
		ev->buffer[len] = '\0';

		printf("C[%d]:%s\n", fd, ev->buffer);

		nty_event_set(ev, fd, send_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLOUT, ev);
		
		
	} else if (len == 0) {

		close(ev->fd);
		printf("[fd=%d] pos[%ld], closed\n", fd, ev-reactor->events);
		 
	} else {
		//if(errno == EAGAIN || errno == EWOULDBLOCK)
		//	continue;
		return 0;
		
		close(ev->fd);
		printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
		
	}

	return len;
}

int send_cb(int fd, int events, void *arg) {

	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = reactor->events + fd;

	int len = send(fd, ev->buffer, ev->length, 0);
	if (len > 0) {
		printf("send[fd=%d], [%d]%s\n", fd, len, ev->buffer);
		nty_event_del(reactor->epfd, ev);
		nty_event_set(ev, fd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, ev);
		
	} else {
		close(ev->fd);
		nty_event_del(reactor->epfd, ev);
		printf("send[fd=%d] error %s\n", fd, strerror(errno));
	}

	return len;
}

int accept_cb(int fd, int events, void *arg) {
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	if (reactor == NULL) return -1;
	struct sockaddr_in client_addr;
	socklen_t len = sizeof(client_addr);
	int clientfd;
	if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {
		if (errno != EAGAIN && errno != EINTR) {
		}
		printf("accept: %s\n", strerror(errno));
		return -1;
	}

	int i = 0;
	do {
		for (i = 0;i < MAX_EPOLL_EVENTS;i ++) {
			if (reactor->events[i].status == 0) {
				break;
			}
		}
		if (i == MAX_EPOLL_EVENTS) {
			printf("%s: max connect limit[%d]\n", __func__, MAX_EPOLL_EVENTS);
			break;
		}

		int flag = 0;
		if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
			printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
			break;
		}
		nty_event_set(&reactor->events[clientfd], clientfd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[clientfd]);
	} while (0);
	printf("new connect [%s:%d][time:%ld], pos[%d]\n", 
		inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), reactor->events[i].last_active, i);
	return 0;
}

int init_sock(short port) {
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	fcntl(fd, F_SETFL, O_NONBLOCK);
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(port);
	bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (listen(fd, 20) < 0) {
		printf("listen failed : %s\n", strerror(errno));
	}
	return fd;
}

int ntyreactor_init(struct ntyreactor *reactor) {
	if (reactor == NULL) return -1;
	memset(reactor, 0, sizeof(struct ntyreactor));
	reactor->epfd = epoll_create(1);
	if (reactor->epfd <= 0) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		return -2;
	}
	reactor->events = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
	if (reactor->events == NULL) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		close(reactor->epfd);
		return -3;
	}
	return 0;
}

int ntyreactor_destroy(struct ntyreactor *reactor) {
	close(reactor->epfd);
	free(reactor->events);
}

int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor) {
	if (reactor == NULL) return -1;
	if (reactor->events == NULL) return -1;
	nty_event_set(&reactor->events[sockfd], sockfd, acceptor, reactor);
	nty_event_add(reactor->epfd, EPOLLIN, &reactor->events[sockfd]);
	return 0;
}

int ntyreactor_run(struct ntyreactor *reactor) {
	if (reactor == NULL) return -1;
	if (reactor->epfd < 0) return -1;
	if (reactor->events == NULL) return -1;
	struct epoll_event events[MAX_EPOLL_EVENTS+1];
	int checkpos = 0, i;
	while (1) {
		long now = time(NULL);
		for (i = 0;i < 100;i ++, checkpos ++) {
			if (checkpos == MAX_EPOLL_EVENTS) {
				checkpos = 0;
			}
			if (reactor->events[checkpos].status != 1) {
				continue;
			}
			long duration = now - reactor->events[checkpos].last_active;
			if (duration >= 60) {
				close(reactor->events[checkpos].fd);
				printf("[fd=%d] timeout\n", reactor->events[checkpos].fd);
				nty_event_del(reactor->epfd, &reactor->events[checkpos]);
			}
		}
		int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
		if (nready < 0) {
			printf("epoll_wait error, exit\n");
			continue;
		}
		for (i = 0;i < nready;i ++) {
			struct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;
			if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
			if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
		}
	}
}

int main(int argc, char *argv[]) {
	unsigned short port = SERVER_PORT;
	if (argc == 2) {
		port = atoi(argv[1]);
	}

	struct ntyreactor *reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
	ntyreactor_init(reactor);
	
    int listened[LISTEN_PORT_COUNT];
    int i = 0;
    for(i = 0; i < LISTEN_PORT_COUNT; ++i)
    {
        listened[i] = init_sock(port + i);
        ntyreactor_addlistener(reactor, listened[i], accept_cb);
    }
	
	ntyreactor_run(reactor);

	ntyreactor_destroy(reactor);
	
    for(i = 0; i < LISTEN_PORT_COUNT; ++i)
    {
        close(listened[i]);
    }
	return 0;
}
  • 第二步:
    • 修改客戶端程序mul_port_client_epoll.c,將其MAX_PORT宏修改爲5即可,修改完成之後,其就會向5個服務端都發起連接
    • 修改完成之後重新編譯
#define MAX_PORT    5

備註(必看)

  • 此處出錯的場景在實際開發中是不會出現的,因爲在實際生活中,不會出現一個系統中有上萬的客戶端(IP都一致),因此出現了這種“五元組”組合用盡的情況
  • 在實際中,每個客戶端的IP地址都是不同的,因此它們的“五元組”類型都是不同的,因此就不會出現這種因爲端口被用完從而導致“五元組”組合被用盡的情況
  • 修改完成之後進行下面的第3次測試

五、第3次測試

測試如下

  • 左側運行服務端程序reactor(111.229.177.161:8888),右側運行兩個客戶端程序mul_port_client_epoll去連接服務器
  • 效果如下:
    • 左側服務端持續接收客戶端的連接
    • 右側兩個客戶端向服務端發起連接,但是當兩個客戶端都連接到64999的時候,客戶端不動了,阻塞了(程序沒有報錯,也沒有退出)
    • 左側服務端也跟着阻塞了(程序沒有報錯,也沒有退出)
    • 過了一小會之後,客戶端顯示連接超時,然後程序退出

原因分析

  • nf_conntrack_max限制了每一個程序可以建立的TCP的連接的最大數目(是連接數目,而不是套接字描述符的個數)

解決方案

  • 我們只需要修改客戶端的內核參數nf_conntrack_max就可以,可以將其修改爲1048576
  • 第一步:nf_conntrack_max參數默認是不開啓的,需要先加載下面的內核模塊,加載完成之後其值默認爲65536
sudo modprobe ip_conntrack

  • 第二步:然後在客戶端的/etc/sysctl.conf配置文件中將nf_conntrack_max設置爲1048576

  • 第三步:調用sysctl命令將/etc/sysctl.conf配置文件的內容更新到/proc/sys/net/nf_conntrack_max中

備註一下

  • 與第3次測試類似,在實際過程中也不會出現這個問題,因爲不可能有一個客戶端程序會發起上完個連接,因爲此處我們是測試,所以程序要發起上萬個測試,才修改這個參數
  • 修改完成之後進行第4次測試

六、第4次測試

測試如下

  • 左側運行服務端程序reactor(111.229.177.161:8888),右側運行兩個客戶端程序mul_port_client_epoll去連接服務器
  • 效果如下:
    • 左側服務端持續接收客戶端的連接
    • 右側兩個客戶端向服務端發起連接,但是當兩個客戶端都連接到140999個的時候,客戶端顯示無法分配地址,從而程序退出
    • 左側服務端沒有報錯,只是沒有客戶端再連接進來了

原因分析

  • 在第2次測試中,我們讓服務端監聽在5個端口上,這次錯誤的原因可能是因爲所有分配端口可用的地址都用完了

解決方案

  • 調整服務端reactor.c程序的LISTEN_PORT_COUNT宏,使其監聽在更多的端口上,這樣就可以承載更多的客戶端。例如,此處我們將LISTEN_PORT_COUNT宏調整爲50
#define LISTEN_PORT_COUNT   50
  • 相應的,客戶端mul_port_client_epoll.c中的MAX_PORT也要調整爲50,因爲要連接50種服務端
#define MAX_PORT    50
  • 修改完成之後進行下面第5次測試

七、第5次測試

測試如下

  • 左側運行服務端程序reactor(111.229.177.161:8888),右側運行兩個客戶端程序mul_port_client_epoll去連接服務器
  • 效果如下:
    • 左側服務端持續接收客戶端的連接
    • 右側兩個客戶端向服務端發起連接,每個客戶端都連接二十多萬的時候程序阻塞(加起來就是五十多萬)
    • 左側服務端接收到五十多萬客戶端的時候就阻塞了

原因分析

  • 在第1次測試的時候,我們將服務端程序reactor.c中的MAX_EPOLL_EVENTS設置爲1024*512=524288個,此處可以看到剛好服務端在接收到五十多萬客戶端的時候阻塞。因此分析應該是MAX_EPOLL_EVENTS宏達到上限也就是epoll_wait()處理的事件數組達到上限

解決方案

  • 增加reactor.c中的MAX_EPOLL_EVENTS,使其可以處理的epoll事件數組變多,例如修改爲1024*1024=1048576個
  • 因爲第1次測試的時候,我們的MAX_EPOLL_EVENTS設置不能過大,如果過大程序運行時會顯示內存不足,現在想到一個辦法,那就是使用posix_memalign()函數來創建epoll的事件數組,posix_memalign()函數是專門用來分配大內存的
  • 修改完成之後下面進行第6此測試

八、第6次測試

  • 下面我們不再測試了,也不再尋找客戶端或服務端的錯誤,而是來分析一下如何使客戶端更多的去連接服務端
  • 在前面的測試中,我們的幾臺機器都是在局域網內的,每連接1000個客戶端約耗時3秒。如果是在局域網之外,那麼耗時更長

一種加快連接的思路

  • 服務是調用accept()函數來接收客戶端的連接的,因此如果想要加快客戶端的連接,那麼可以在accept()函數上下手
  • 提供的思路有:
    • 1.多個accept()放在一個線程中
    • 2.多個accept()分配在不同的線程
    • 3.多個accept()分配在不同的進程(Nginx爲多進程服務器,它就是這樣做的,每個進程都有自己獨立的資源)
  •  
  • 爲什麼多進程比多線程好:
    • 1.多進程不需要加鎖
    • 2.多進程可以承載比多線程更多的fd,因爲每個進程都有自己一份獨立的資源

九、總結附加

用戶態協議棧設計

  • 在服務器承載百萬客戶端的時候,這時候會有大量的數據在進行交互。對於默認的應用程序來說,其數據的傳輸都要經過協議棧緩衝區,如下圖所示:
    • 客戶端發送數據到服務端時,數據先達到網卡,然後將數據拷貝到協議棧中,最後再把數據從協議棧中拷貝到服務端程序中
    • 服務端回送數據時是相反的順序,先把數據從服務端程序拷貝到協議棧,然後再把數據拷貝到網卡發送給客戶端

  • 從上圖可以瞭解,當服務端與百萬客戶端數據交互的時候會帶來下面的弊端:
    • 數據要進行大量的拷貝,從而造成程序的性能降低
    • 數據交互速度受限與緩慢
  • 用戶態協議棧設計:通過上面我們知道,數據要經過一層協議棧,會對性能帶來影響,因此可以自己設計“用戶態協議棧”,大致原理如下:
    • 數據交互時不再經過協議棧,將網卡與服務端的數據mmap映射到內存中,直接從內存中進行交互,從而減少了中間的拷貝
    • 這就是常說的“零拷貝”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章