高並發服務器
1.線程池並發服務器
兩種模型:
- 預先創建阻塞於accept多線程,使用互斥鎖上鎖保護accept(減少了每次創建線程的開銷)
- 預先創建多線程,由主線程調用accept
線程池
3.多路I/O轉接服務器
三種模型性能分析

select模型
select用來阻塞監聽4,5,6,7是否有數據傳入,若7這個文件描述符有數據到達,select返回就緒文件描述符個數,若檢測到7有數據接收,accept接收客戶鏈接請求,創建一個新的文件描述符。
select
(1)select能監聽的文件描述符個數受限於FD_SETSIZE,一般為1024,單純改變進程打開的文件描述符個數並不能改變select監聽文件個數
(2)解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select采用的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力
#include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); nfds: 監控的文件描述符集里最大文件描述符加1(因為是計數的從0開始),因為此參數會告訴內核檢測前多少個文件描述符的狀態 readfds:監控有讀數據到達文件描述符集合,傳入傳出參數 writefds:監控寫數據到達文件描述符集合,傳入傳出參數 exceptfds:監控異常發生達文件描述符集合, 如帶外數據到達異常,傳入傳出參數 timeout:定時阻塞監控時間,3種情況 1.NULL,永遠等下去 2.設置timeval,等待固定時間 3.設置timeval里時間均為0,檢查描述字后立即返回,輪詢 struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0 int FD_ISSET(int fd, fd_set *set); 測試文件描述符集合里fd是否置1 void FD_SET(int fd, fd_set *set); 把文件描述符集合里fd位置1 void FD_ZERO(fd_set *set); 把文件描述符集合里所有位清0
sever
/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默認為 1024 */ ssize_t n; fd_set rset, allset; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */ socklen_t cliaddr_len; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); /* 默認最大128 */ maxfd = listenfd; /* 初始化 */ maxi = -1; /* client[]的下標 */ for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; /* 用-1初始化client[] */ FD_ZERO(&allset); FD_SET(listenfd, &allset); /* 構造select監控文件描述符集 */ for (;;) { rset = allset; /* 每次循環時都從新設置select監控信號集 */ nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (nready < 0) perr_exit("select error"); if (FD_ISSET(listenfd, &rset)) { /* new client connection */ cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) { client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */ break; } /* 達到select能監控的文件個數上限 1024 */ if (i == FD_SETSIZE) { fputs("too many clients\n", stderr); exit(1); } FD_SET(connfd, &allset); /* 添加一個新的文件描述符到監控信號集里 */ if (connfd > maxfd) maxfd = connfd; /* select第一個參數需要 */ if (i > maxi) maxi = i; /* 更新client[]最大下標值 */ if (--nready == 0) continue; /* 如果沒有更多的就緒文件描述符繼續回到上面select阻塞監聽,負責處理未 處理完的就緒文件描述符 */ } for (i = 0; i <= maxi; i++) { /* 檢測哪個clients 有數據就緒 */ if ((sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { if ((n = Read(sockfd, buf, MAXLINE)) == 0) { /* 當client關閉鏈接時,服務器端也關閉對應鏈接 */ Close(sockfd); FD_CLR(sockfd, &allset); /* 解除select監控此文件描述符 */ client[i] = -1; } else { int j; for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Write(sockfd, buf, n); } if (--nready == 0) break; } } } close(listenfd); return 0; }
client
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; }
pselect 給出pselect原型,此模型用的不多
#include <sys/select.h> int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); struct timespec { long tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; 用sigmask替代當前進程的阻塞信號集,調用返回后還原原有阻塞信號集
poll
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd { int fd; /* 文件描述符 */ short events; /* 監控的事件 */ short revents; /* 監控事件中滿足條件返回的事件 傳出參數*/ };
//第一個參數是一個結構體數組,nfds指結構體數組有幾個元素
POLLIN普通或帶外優先數據可讀, 即POLLRDNORM | POLLRDBAND POLLRDNORM - 數據可讀 POLLRDBAND - 優先級帶數據可讀 POLLPRI 高優先級可讀數據 POLLOUT普通或帶外數據可寫 POLLWRNORM - 數據可寫 POLLWRBAND - 優先級帶數據可寫 POLLERR 發生錯誤 POLLHUP 發生掛起 POLLNVAL 描述字不是一個打開的文件 nfds 監控數組中有多少文件描述符需要被監控 timeout 毫秒級等待 - 1:阻塞等,#define INFTIM - 1 Linux中沒有定義此宏 0:立即返回,不阻塞進程 >0:等待指定毫秒數,如當前系統時間精度不夠毫秒,向上取值
如果不再監控某個文件描述符時,可以把pollfd中,fd設置為-1,poll不再監控此pollfd,下次返回時,把revents設置為0。
ppoll GNU定義了ppoll(非POSIX標准),可以支持設置信號屏蔽字,大家可參考poll模型自行實現C/S。
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <poll.h> int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout_ts, const sigset_t *sigmask);
/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <poll.h> #include <errno.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 #define OPEN_MAX 1024 int main(int argc, char *argv[]) { int i, j, maxi, listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct pollfd client[OPEN_MAX]; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); client[0].fd = listenfd; client[0].events = POLLRDNORM; /* listenfd監聽普通讀事件 */ for (i = 1; i < OPEN_MAX; i++) client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */ maxi = 0; /* client[]數組有效元素中最大元素下標 */ for (;;) { nready = poll(client, maxi + 1, -1); /*-1表示阻塞 直到有數據到達*/ if (client[0].revents & POLLRDNORM) { /* 有客戶端鏈接請求 */ clilen = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 1; i < OPEN_MAX; i++) if (client[i].fd < 0) { client[i].fd = connfd; /* 找到client[]中空閑的位置,存放accept返回的connfd */ break; } if (i == OPEN_MAX) perr_exit("too many clients"); client[i].events = POLLRDNORM; /* 設置剛剛返回的connfd,監控讀事件 */ if (i > maxi) maxi = i; /* 更新client[]中最大元素下標 */ if (--nready <= 0) continue; /* 沒有更多就緒事件時,繼續回到poll阻塞 */ } for (i = 1; i <= maxi; i++) { /* 檢測client[] */ if ((sockfd = client[i].fd) < 0) continue; if (client[i].revents & (POLLRDNORM | POLLERR)) { if ((n = Read(sockfd, buf, MAXLINE)) < 0) { if (errno == ECONNRESET) { /* 當收到 RST標志時 */ /* connection reset by client */ printf("client[%d] aborted connection\n", i); Close(sockfd); client[i].fd = -1; } else perr_exit("read error"); } else if (n == 0) { /* connection closed by client */ printf("client[%d] closed connection\n", i); Close(sockfd); client[i].fd = -1; } else { for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Writen(sockfd, buf, n); } if (--nready <= 0) break; /* no more readable descriptors */ } } } return 0; }
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; }
epoll
epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率,因為它會復用文件描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新准備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。目前epell是linux大規模並發網絡程序中的熱門首選模型。

epoll
epoll是將就緒的文件描述符放在了一個隊列中。不用像poll一樣整個遍歷了。
epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epollepoll_wait/epoll_pwait的調用,提高應用程序效率。
一個進程打開大數目的socket描述符
cat /proc/sys/fs/file-max
設置最大打開文件描述符限制
sudo vi /etc/security/limits.conf 寫入以下配置,soft軟限制,hard硬限制 * soft nofile 65536 * hard nofile 100000
epoll API
1.創建一個epoll句柄,參數size用來告訴內核監聽的文件描述符個數,跟內存大小有關 #include(創建一顆樹,並告訴這棵樹最多能插入多少個節點,返回一個文件描述符,通過文件描述符找打整個樹)
int epoll_create(int size) size:告訴內核監聽的數目
2.控制某個epoll監控的文件描述符上的事件:注冊、修改、刪除。(在這棵樹上插入刪除一個節點)
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) epfd:為epoll_creat的句柄 op:表示動作,用3個宏來表示: EPOLL_CTL_ADD(注冊新的fd到epfd), EPOLL_CTL_MOD(修改已經注冊的fd的監聽事件), EPOLL_CTL_DEL(從epfd刪除一個fd); event:告訴內核需要監聽的事件 struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉) EPOLLOUT:表示對應的文件描述符可以寫 EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來) EPOLLERR:表示對應的文件描述符發生錯誤 EPOLLHUP:表示對應的文件描述符被掛斷; EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來 說的 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需 要再次把這個socket加入到EPOLL隊列里
3.等待所監控文件描述符上有事件的產生,類似於select()調用。(阻塞等待,當有就緒的文件是,將就緒的文件描述符拷貝到一個數組上)
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) events:用來從內核得到事件的集合, maxevents:告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size, timeout:是超時時間 - 1:阻塞 0:立即返回,非阻塞 >0:指定微秒 返回值:成功返回有多少文件描述符就緒,時間到時返回0,出錯返回 - 1
server
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <errno.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 #define OPEN_MAX 1024 //自己指定,不要超過默認系統默認的文件描述符的上界 int main(int argc, char *argv[]) { int i, j, maxi, listenfd, connfd, sockfd; int nready, efd, res; ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; int client[OPEN_MAX]; struct sockaddr_in cliaddr, servaddr; struct epoll_event tep, ep[OPEN_MAX]; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); Listen(listenfd, 20); for (i = 0; i < OPEN_MAX; i++) client[i] = -1; maxi = -1;
efd = epoll_create(OPEN_MAX);//創建一棵樹,這棵樹最多容納1024的結點,返回一個文件描述符
if (efd == -1) perr_exit("epoll_create");
tep.events = EPOLLIN; tep.data.fd = listenfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); if (res == -1) perr_exit("epoll_ctl"); for (;;) { nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞監聽 */ if (nready == -1) perr_exit("epoll_wait"); for (i = 0; i < nready; i++) { if (!(ep[i].events & EPOLLIN)) continue; if (ep[i].data.fd == listenfd) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); printf("received from %s at PORT %d \n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (j = 0; j < OPEN_MAX; j++) if (client[j] < 0) { client[j] = connfd; /* save descriptor */ break; } if (j == OPEN_MAX) perr_exit("too many clients"); if (j > maxi) maxi = j; /* max index in client[] array */ tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1) perr_exit("epoll_ctl"); } else { sockfd = ep[i].data.fd; n = Read(sockfd, buf, MAXLINE); if (n == 0) { for (j = 0; j <= maxi; j++) { if (client[j] == sockfd) { client[j] = -1; break; } } res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); if (res == -1) perr_exit("epoll_ctl"); Close(sockfd); printf("client[%d] closed connection\n", j); } else { for (j = 0; j < n; j++) buf[j] = toupper(buf[j]); Writen(sockfd, buf, n); } } } } close(listenfd); close(efd); return 0; }
client
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; }
