一、用select實現的並發服務器,能達到的並發數,受兩方面限制
1、一個進程能打開的最大文件描述符限制。 這可以通過調整內核參數。可以通過ulimit -n來調整或者使用setrlimit函數設置, 但一個系統所能打開的最大數也是有 限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看
2、select中的fd_set集合容量的限制 (FD_SETSIZE,一般為1024) ,這需要重新編譯內核。
可以寫個測試程序,只建立連接,看看最多能夠建立多少個 連接,客戶端程序如下:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(void) { int count = 0; while(1) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { sleep(4); ERR_EXIT("socket"); } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect"); struct sockaddr_in localaddr; socklen_t addrlen = sizeof(localaddr); if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0) ERR_EXIT("getsockname"); printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); printf("count = %d\n", ++count); } return 0; }
啟動select 的服務器端程序,再啟動客戶端測試程序:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_select
......................................................................
count = 1015
recv connect ip=127.0.0.1 port=51299
count = 1016
recv connect ip=127.0.0.1 port=51300
count = 1017
recv connect ip=127.0.0.1 port=51301
count = 1018
recv connect ip=127.0.0.1 port=51302
count = 1019
recv connect ip=127.0.0.1 port=51303
count = 1020
recv connect ip=127.0.0.1 port=51304
accept error: Too many open files
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./conntest
...................................................................................................
count = 1015
ip=127.0.0.1 port=51299
count = 1016
ip=127.0.0.1 port=51300
count = 1017
ip=127.0.0.1 port=51301
count = 1018
ip=127.0.0.1 port=51302
count = 1019
ip=127.0.0.1 port=51303
count = 1020
ip=127.0.0.1 port=51304
count = 1021
socket: Too many open files
輸出 太多條目,上面只截取最後幾條,從中可以看出對於客戶端,最多只能開啟1021個連接套接字,因為總共是1024個,還得除 去0,1,2。而服務器端只能accept 返回1020個已連接套接字,因為除了012之外還有一個監聽套接字,客戶端某一個套接 字(不一定是最後一個)雖然已經建立了連接,在已完成連接隊列中,但accept 返回時達到最大描述符限制,返回錯誤, 打印提示信息。
也許有人會注意到上面有一行 sleep(4); 當客戶端調用socket准備創建第1022個套接字時,如上所 示也會提示錯誤,此時socket函數返回-1出錯,如果沒有睡眠4s後再退出進程會有什麼問題呢?如果直接退出進程,會將客 戶端所打開的所有套接字關閉掉,即向服務器端發送了很多FIN段,而此時也許服務器端還一直在accept ,即還在從已連接 隊列中返回已連接套接字,此時服務器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連接的套接字的可讀事件 ,read 返回0,所以會有很多 client close 字段 參雜在條目的輸出中,還有個問題就是,因為read 返回0,服務器端會 將自身的已連接套接字關閉掉,那麼也許剛才說的客戶端某一個連接會被accept 返回,即測試不出服務器端真正的並發容 量。
將 sleep(4); 注釋掉,觀察服務器端的輸出如下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_select
...........................................................
count = 1018
recv connect ip=127.0.0.1 port=52323
client close
count = 1019
recv connect ip=127.0.0.1 port=52324
client close
count = 1020
recv connect ip=127.0.0.1 port=52325
client close
count = 1021
recv connect ip=127.0.0.1 port=52234
client close
client close
可以看到輸出參雜著client close,且這次的count 達到了1021,原因就是服務器端前面已經有些套接字關閉了,所以accept 創建套接字不會出錯,服務器進程也不會因為出 錯而退出,可以看到最後接收到的一個連接端口是52234,即不一定是客戶端的最後一個連接。
二、poll 函數應用 舉例
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數1: 結構體數組指針,struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
結構體中的fd 即套 接字描述符,events 即感興趣的事件,如下圖所示,revents 即返回的事件。
參數2:結構體數組的成員個數,即文件描述 符個數。
參數3:即超時時間,若為-1,表示永不超時。
poll 跟 select 還是很相似的,比較重要的區別在 於poll 所能並發的個數跟FD_SETSIZE無關,只跟一個進程所能打開的文件描述符個數有關,可以在select 程序的基礎上修 改成poll 程序,在運行服務器端程序之前,使用ulimit -n 2048 將限制改成2048個,注意在運行客戶端進程的終端也需更 改,因為客戶端也會有所限制,這只是臨時性的更改,因為子進程會繼承這個環境參數,而我們是在bash命令行啟動程序的 ,故在進程運行期間,文件描述符的限制為2048個。
使用poll 函數的服務器端程序如下:
/************************************************************************* > File Name: echoser.c > Author: Simba > Mail: [email protected] > Created Time: Fri 01 Mar 2013 06:15:27 PM CST ************************************************************************/ #include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #include<sys/wait.h> #include<poll.h> #include "read_write.h" #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { int count = 0; signal(SIGPIPE, SIG_IGN); int listenfd; //被動套接字(文件描述符),即只可以accept, 監聽套接字 if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */ /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt error"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) //listen應在socket和bind之後,而在accept之前 ERR_EXIT("listen error"); struct sockaddr_in peeraddr; //傳出參數 socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值 int conn; // 已連接套接字(變為主動套接字,即可以主動connect) int i; struct pollfd client[2048]; int maxi = 0; //client[i]最大不空閒位置的下標 for (i = 0; i < 2048; i++) client[i].fd = -1; int nready; client[0].fd = listenfd; client[0].events = POLLIN; while (1) { /* poll檢測[0, maxi + 1) */ nready = poll(client, maxi + 1, -1); if (nready == -1) { if (errno == EINTR) continue; ERR_EXIT("poll error"); } if (nready == 0) continue; if (client[0].revents & POLLIN) { conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen); //accept不再阻塞 if (conn == -1) ERR_EXIT("accept error"); for (i = 1; i < 2048; i++) { if (client[i].fd < 0) { client[i].fd = conn; if (i > maxi) maxi = i; break; } } if (i == 2048) { fprintf(stderr, "too many clients\n"); exit(EXIT_FAILURE); } printf("count = %d\n", ++count); printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); client[i].events = POLLIN; if (--nready <= 0) continue; } for (i = 1; i <= maxi; i++) { conn = client[i].fd; if (conn == -1) continue; if (client[i].revents & POLLIN) { char recvbuf[1024] = {0}; int ret = readline(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("readline error"); else if (ret == 0) //客戶端關閉 { printf("client close \n"); client[i].fd = -1; close(conn); } fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); if (--nready <= 0) break; } } } return 0; } /* poll 只受一個進程所能打開的最大文件描述符限制,這個可以使用ulimit -n調整 */
參照前面對select 函數的解釋不難理解上面的程序,就不再贅述了。來看一下輸出:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ulimit -n 2048
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_select
.............................................................................................
r ecv connect ip=127.0.0.1 port=57430
count = 2039
recv connect ip=127.0.0.1 port=57431
count = 2040
recv connect ip=127.0.0.1 port=57432
count = 2041
recv connect ip=127.0.0.1 port=57433
count = 2042
recv connect ip=127.0.0.1 port=57434
count = 2043
recv connect ip=127.0.0.1 port=57435
count = 2044
recv connect ip=127.0.0.1 port=57436
accept error: Too many open files
imba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ulimit -n 2048
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./conntest
................................................................................
coun t = 2039
ip=127.0.0.1 port=57431
count = 2040
ip=127.0.0.1 port=57432
count = 2041
ip=127.0.0.1 port=57433
count = 2042
ip=127.0.0.1 port=57434
count = 2043
ip=127.0.0.1 port=57435
count = 2044
ip=127.0.0.1 port=57436
count = 2045
socket: Too many open files可
以看到現在最大的連接數已 經是2045個了,雖然服務器端有某個連接沒有accept 返回。即poll 比 select 能夠承受更多的並發連接,只受一個進程所 能打開的最大文件描述符個數限制。可以通過ulimit -n 修改,但一個系統所能打開的文件描述符個數也是有限的, 這跟系統的內存大小有關系,所以說也不是可以無限地並發,可以查看一下本機的容量:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ cat /proc/sys/fs/file-max
101078
本機是虛擬機,內存大約2G,能夠打開的文件描述符個數大約在10w個左右。