前言:基于epoll的反應(yīng)堆模式(reactor)的服務(wù)器程序,進行百萬并發(fā)量的連接測試。通過代碼優(yōu)化,以及服務(wù)器與客戶端的硬件配置優(yōu)化,達到百萬并發(fā)。
一、服務(wù)器:epoll-reactor
- 代碼實現(xiàn)
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <sys/poll.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <sys/time.h> using namespace std; #define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) struct timeval tv_last; #define BUFFER_LEN 512 #define CLIENT_MAX_COUNT 1048576 typedef int (*EPOLL_CALLBACK)(int fd); struct CONNECT_ITEM { char rBuffer[BUFFER_LEN] = {0}; int rLen = 0; char wBuffer[BUFFER_LEN] = {0}; int wLen = 0; union { EPOLL_CALLBACK accept_callback = nullptr; EPOLL_CALLBACK recv_callback; }recv_t; EPOLL_CALLBACK send_callback = nullptr; }connect_item[CLIENT_MAX_COUNT]; int epfd = 0; enum _EPOLL_CTRL{ ADD, MOD }; void setEvent(int fd, EPOLL_EVENTS events, _EPOLL_CTRL ctrl) { epoll_event ev; ev.events = events; //默認水平觸發(fā)(LT),有(數(shù)據(jù))事件就會一直觸發(fā),知道全部處理完 /* EPOLLET為邊沿觸發(fā)(ET),當有事件發(fā)生時只觸發(fā)一次, 比如來數(shù)據(jù)了,如果一次沒有讀完,不會再觸發(fā)了,所以必須全部讀完,在進行下一次epoll_wait */ //ev.events = EPOLLIN | EPOLLET; ev.data.fd = fd; epoll_ctl(epfd, ctrl == ADD ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, &ev); } int recv_cb(int fd) { char* buffer = connect_item[fd].rBuffer; int index = connect_item[fd].rLen; int count = recv(fd, buffer + index, BUFFER_LEN - index, 0); if (count == 0) { //printf("disconnect: %d\n", fd); epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); return count; }else if (count < 0) { //printf("error\n"); return count; } connect_item[fd].rLen += count; //這里的rLen可能會超出buffer的大小,這里就不做處理了 //printf("RECV===>>> clientfd: %d, count: %d, buffer: %s\n", fd, count, buffer); //改變該文件描述符的事件類型為EPOLLOUT setEvent(fd, EPOLLOUT, MOD); //發(fā)送buffer賦值 memcpy(connect_item[fd].wBuffer, connect_item[fd].rBuffer, connect_item[fd].rLen); connect_item[fd].wLen = connect_item[fd].rLen; //發(fā)送了多少,代表處理了多少 connect_item[fd].rLen -= connect_item[fd].wLen; return count; } int send_cb(int fd) { char* buffer = connect_item[fd].wBuffer; int index = connect_item[fd].wLen; int count = send(fd, buffer, connect_item[fd].wLen, 0); //改變該文件描述符的事件類型為EPOLLIN setEvent(fd, EPOLLIN, MOD); return count; } int accept_cb(int fd) { struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len); setEvent(clientfd, EPOLLIN, ADD); memset(connect_item[clientfd].rBuffer, 0, sizeof(connect_item[clientfd].rBuffer)); connect_item[clientfd].rLen = 0; memset(connect_item[clientfd].wBuffer, 0, sizeof(connect_item[clientfd].wBuffer)); connect_item[clientfd].wLen = 0; connect_item[clientfd].recv_t.recv_callback = recv_cb; connect_item[clientfd].send_callback = send_cb; //printf("ACCEPT===>>> clientfd:%d\n", clientfd); if (clientfd % 1000 == 999) { struct timeval tv_cur; gettimeofday(&tv_cur, NULL); printf("clientfd: %d, time_used: %d\n", clientfd, int(TIME_SUB_MS(tv_cur, tv_last))); tv_last = tv_cur; } return clientfd; } int init_server(int port) { int listenfd = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in serverAddr; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(port); bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr)); listen(listenfd, 10); return listenfd; } int main() { epfd = epoll_create(1); // int size gettimeofday(&tv_last, NULL); for (int i = 0; i < 20; i++) { int listenfd = init_server(2048 + i); connect_item[listenfd].recv_t.accept_callback = accept_cb; setEvent(listenfd, EPOLLIN, ADD); } struct epoll_event events[100000] = {0}; while (1) { int nready = epoll_wait(epfd, events, 100000, -1); for (int i = 0; i < nready; i++) { int connfd = events[i].data.fd; if (events[i].events & EPOLLIN) { int count = connect_item[connfd].recv_t.recv_callback(connfd); }else if (events[i].events & EPOLLOUT) { connect_item[connfd].send_callback(connfd); } } } for (int i = 0; i < 20; i++) { close(i + 3); } return 0; }
二、客戶端:multi_port_client_epoll
- 代碼實現(xiàn):
#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 <sys/time.h> #include <unistd.h> #define MAX_BUFFER 128 #define MAX_EPOLLSIZE (384*1024) #define MAX_PORT 20 #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); } const char *ip = argv[1]; int port = atoi(argv[2]); int connections = 0; char buffer[128] = {0}; int i = 0, index = 0; 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; 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", sockfd); send(sockfd, buffer, strlen(buffer), 0); ev.data.fd = sockfd; ev.events = EPOLLIN | EPOLLOUT; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev); connections++; } //connections ++; if (connections % 1000 == 999 || connections >= 340000) { struct timeval tv_cur; memcpy(&tv_cur, &tv_begin, sizeof(struct timeval)); gettimeofday(&tv_begin, NULL); int time_used = TIME_SUB_MS(tv_begin, tv_cur); printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used); 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(clientfd, buffer, strlen(buffer), 0); //printf("send:%s\n", buffer); } if (events[i].events & EPOLLIN) { char rBuffer[MAX_BUFFER] = {0}; ssize_t length = recv(clientfd, 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); // } } } usleep(10); } return 0; err: printf("error : %s\n", strerror(errno)); return 0; }
三、百萬并發(fā)測試過程
1、硬件配置
- 1臺服務(wù)器:8G運行內(nèi)存 8核CPU
- 3臺客戶端:4G運行內(nèi)存 4核CPU
這些硬件配置可以通過虛擬機配置。
按照客戶端測試用例代碼的實現(xiàn),一臺客戶端最大可以創(chuàng)建340000個連接,而一臺電腦的端口最多有65535個,一個端口建立一個連接,除去系統(tǒng)用的前1024個端口,我們可用的也最多只有65535-1024 = 64511個連接,而我們的客戶端代碼之所以能達到340000個連接,這是因為在tcp協(xié)議中的四元組概念,即(源IP,源port,目的IP,目的port),這個四元組構(gòu)成一個tcp連接,只要四元組中的任何一個參數(shù)不同那么就是一個完全不同的連接,因此在源IP與目的IP都固定的情況下,我們從端口來入手,突破65535的限制,我們可以在服務(wù)端創(chuàng)建多個監(jiān)聽socket,分別綁定不同的端口,比如創(chuàng)建了20個監(jiān)聽socket,分別綁定在2048 ~ 2067端口,此時客戶端就可以循環(huán)綁定這20個端口了,客戶端的1個端口就可以在四元組里與服務(wù)端建立20個連接(這是因為服務(wù)端的目的port有20個),而客戶端有1025 ~ 65535的端口可供應(yīng)用程序使用,因此理論上就能建立(65535-1025) x 20 = 1290200個連接,已經(jīng)超超過了百萬,只不過我們的客戶端代碼限定了最大340000個連接。
三臺客戶端就可以實現(xiàn)百萬級并發(fā)測試。我們接下來只演示一個客戶端的并發(fā)測試,另外兩臺操作一樣。
2、測試流程
(1) 在服務(wù)器上編譯運行 目錄一 的服務(wù)器代碼:
g++ -o epoll-reactor epoll-reactor.cpp
./epoll-reactor
(2) 在客戶端上編譯運行 目錄二 的客戶端代碼:
gcc -o mul_port_client_epoll mul_port_client_epoll.c
./mul_port_client_epoll 192.168.1.20 2048
192.168.1.20 2048 是服務(wù)器的地址與端口(根據(jù)自己的電腦傳入自己的地址)
(3) 運行結(jié)果如圖:文章來源:http://www.zghlxwxcb.cn/news/detail-828223.html
- 先看客戶端提示:Too many open files,這是因為系統(tǒng)設(shè)置的最多可打開的文件描述符太少了,通過ulimit -a命令查看open files的限制大小為1024,最多可以打開1024個文件描述符,所以會提示connections只有999個,就報錯了。通過ulimit -n 1048576 將其改為百萬級的可打開的文件描述符個數(shù)(1048576為1024x1024)。如圖:
- 再次在客戶端運行測試用例,服務(wù)端也運行起來:
- 我們發(fā)現(xiàn)客戶端仍然只創(chuàng)建了999個(實際比這多,只是代碼每1000個連接打印一次)連接,就出錯了,不過提示的錯誤不一樣了:Connection refused(連接被拒絕),這是服務(wù)器拒絕了客戶端的連接,我們看服務(wù)器的提示信息:Segmentation fault (core dumped),出現(xiàn)了段錯誤,仔細看代碼沒有內(nèi)存越界的問題,可能也是open files的問題,我們也通過命令ulimit -n 1048576 將其也改為百萬級,再次運行測試:
- 這次達到了19999個連接后,出現(xiàn)了Cannot assign requested address(不能分配請求的地址),這是因為linux系統(tǒng)還有一個參數(shù)限制了我們連接的數(shù)目,在/etc/sysctl.conf文件里有這么一個參數(shù):net.ipv4.ip_local_port_range = 1024 2048,可供應(yīng)用程序使用的端口范圍是1024 ~ 2048,但是為什么客戶端能創(chuàng)建19999個連接才掛掉呢,按照端口范圍最多有2048-1024 = 1024個連接,這是因為我們前面所說到的四元組概念,我們的服務(wù)端提供了20個端口可供客戶端去綁定,因此最多可創(chuàng)建20 x 1024 = 20480個連接,所以客戶端達到了19999個連接后掛掉了。所以我們需要修改客戶端的/etc/sysctl.conf文件里的這個參數(shù)net.ipv4.ip_local_port_range = 1024 65535。修改完成后,執(zhí)行 sudo sysctl -p讓其生效,如圖:
- 如果出現(xiàn)cannot stat /proc/sys/net/nf_conntrack_max: No such file or directory的錯誤,執(zhí)行 sudo modprobe ip_conntrack命令,然后再次執(zhí)行sudo sysctl -p命令,讓其生效。我們再次運行測試:
- 這次客戶端連接數(shù)量達到了62999,然后服務(wù)器掛掉了,廢話不多說,直接給出答案:這是因為客戶端的/etc/sysctl.conf文件里還有一個參數(shù):fs.file-max = 65535,這個參數(shù)代表的是文件描述符的最大值是65535,所以上圖的sockfd只達到了63002(實際比63002多,因為測試用例的代碼沒有打印出來),我們修改fs.file-max = 1048576,保存退出后,再次執(zhí)行sudo sysctl -p命令使其生效。然后我們再次測試,如下圖:如果服務(wù)端再次出現(xiàn)段錯誤,并且客戶端出現(xiàn)連接被拒絕,這是因為服務(wù)端的fs.file-max參數(shù)值沒有被修改,我們也將其修改為1048576。
(在這里我提示一點,我在測試的過程中,不小心將fs.file-max改成了1024,改的太小了,以至于系統(tǒng)的任何編輯文件的命令都無法執(zhí)行了,這樣系統(tǒng)就不能用了。但是還有補救辦法:執(zhí)行 echo 65535 > /proc/sys/fs/file-max命令,然后再次打開/etc/sysctl.conf文件修改fs.file-max參數(shù)。)
我們再次運行測試:文章來源地址http://www.zghlxwxcb.cn/news/detail-828223.html
- 這次終于達到了340000個連接,我們的測試流程到這里也就結(jié)束了,想要達到百萬級并發(fā),我們只需要再準備兩臺電腦作為客戶端,重復(fù)上述的操作即可。
到了這里,關(guān)于五、Linux C/C++ 對epoll-reactor服務(wù)器的百萬級高并發(fā)實現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!