1、用select實現的併發服務器,能達到的併發數,受兩方面限制編程
二、select中的fd_set集合容量的限制(FD_SETSIZE,通常爲1024) ,這須要從新編譯內核。ubuntu
能夠寫個測試程序,只創建鏈接,看看最多可以創建多少個鏈接,客戶端程序以下:數組
#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; }
服務器的代碼serv.c:來自<<UNIX網絡編程——使用select函數編寫客戶端和服務器>>最後的服務器程序。bash
#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> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { 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; int client[FD_SETSIZE]; int maxi = 0; // client數組中最大不空閒位置的下標 for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; int nready; int maxfd = listenfd; fd_set rset; fd_set allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd, &allset); int count = 0; while (1) { rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (nready == -1) { if (errno == EINTR) continue; ERR_EXIT("select error"); } if (nready == 0) continue; if (FD_ISSET(listenfd, &rset)) { conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen); //accept再也不阻塞 if (conn == -1) ERR_EXIT("accept error"); printf("count = %d\n", ++count); for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) { client[i] = conn; if (i > maxi) maxi = i; break; } } if (i == FD_SETSIZE) { fprintf(stderr, "too many clients\n"); exit(EXIT_FAILURE); } printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); FD_SET(conn, &allset); if (conn > maxfd) maxfd = conn; if (--nready <= 0) continue; } for (i = 0; i <= maxi; i++) { conn = client[i]; if (conn == -1) continue; if (FD_ISSET(conn, &rset)) { char recvbuf[1024] = {0}; int ret = read(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("read error"); else if (ret == 0) { //客戶端關閉 printf("client close \n"); FD_CLR(conn, &allset); client[i] = -1; close(conn); } fputs(recvbuf, stdout); write(conn, recvbuf, strlen(recvbuf)); if (--nready <= 0) break; } } } return 0; } /* select所能承受的最大併發數受 * 1.一個進程所能打開的最大文件描述符數,能夠經過ulimit -n來調整 * 但一個系統所能打開的最大數也是有限的,跟內存有關,能夠經過cat /proc/sys/fs/file-max 查看 * 2.FD_SETSIZE(fd_set)的限制,這個須要從新編譯內核 */
先啓動select 的服務器端程序,再啓動客戶端測試程序:
服務器
huangcheng@ubuntu:~$ ./serv count = 1 recv connect ip=127.0.0.1 port=48370 count = 2 recv connect ip=127.0.0.1 port=48371 count = 3 recv connect ip=127.0.0.1 port=48372 count = 4 recv connect ip=127.0.0.1 port=48373 .................................... recv connect ip=127.0.0.1 port=49389 count = 1020 recv connect ip=127.0.0.1 port=49390 accept error: Too many open files
huangcheng@ubuntu:~$ ./cli ip=127.0.0.1 port=46327 count = 1 ip=127.0.0.1 port=46328 count = 2 ip=127.0.0.1 port=46329 count = 3 ip=127.0.0.1 port=46330 count = 4 ip=127.0.0.1 port=46331 count = 5 ip=127.0.0.1 port=46332 count = 6 ip=127.0.0.1 port=46333 ....................... ip=127.0.0.1 port=47345 count = 1020 ip=127.0.0.1 port=47346 count = 1021 socket: Too many open files
輸出太多條目,上面只截取最後幾條,從中能夠看出對於客戶端,最多隻能開啓1021個鏈接套接字,由於總共是1024個,還得除去0、一、2。而服務器端只能accept 返回1020個已鏈接套接字,由於除了0、一、2以外還有一個監聽套接字,客戶端某一個套接字(不必定是最後一個)雖然已經創建了鏈接,在已完成鏈接隊列中,但accept 返回時達到最大描述符限制,返回錯誤,打印提示信息。網絡
也許有人會注意到上面有一行 sleep(4);當客戶端調用socket準備建立第1022個套接字時,如上所示也會提示錯誤,此時socket函數返回-1出錯,若是沒有睡眠4s後再退出進程會有什麼問題呢?若是直接退出進程,會將客戶端所打開的全部套接字關閉掉,即向服務器端發送了不少FIN段,而此時也許服務器端還一直在accept ,即還在從已鏈接隊列中返回已鏈接套接字,此時服務器端除了關心監聽套接字的可讀事件,也開始關心前面已創建鏈接的套接字的可讀事件,read 返回0,因此會有不少 client close 字段 參雜在條目的輸出中,還有個問題就是,由於read 返回0,服務器端會將自身的已鏈接套接字關閉掉,那麼也許剛纔說的客戶端某一個鏈接會被accept 返回,即測試不出服務器端真正的併發容量。併發
huangcheng@ubuntu:~$ ./serv count = 1 recv connect ip=127.0.0.1 port=50413 count = 2 .................................... client close client close client close client close ................................... recv connect ip=127.0.0.1 port=51433 client close count = 1021 recv connect ip=127.0.0.1 port=51364 client close client close能夠看到輸出參雜着client close,且此次的count 達到了1021,緣由就是服務器端前面已經有些套接字關閉了,因此accept 建立套接字不會出錯,服務器進程也不會由於出錯而退出,能夠看到最後接收到的一個鏈接端口是51364,即不必定是客戶端的最後一個鏈接。
2、poll 函數應用舉例socket
#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 函數的服務器端程序以下:
#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> #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 = read(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); write(conn, recvbuf, strlen(recvbuf)); if (--nready <= 0) break; } } } return 0; } /* poll 只受一個進程所能打開的最大文件描述符限制,這個可使用ulimit -n調整 */參照前面對 select 函數 的解釋不難理解上面的程序,就再也不贅述了。來看一下輸出:
root@ubuntu:/home/huangcheng# ulimit -n 2048 root@ubuntu:/home/huangcheng# su - huangcheng huangcheng@ubuntu:~$ ulimit -n 2048 huangcheng@ubuntu:~$ ./serv ........................... count = 2042 recv connect ip=127.0.0.1 port=54499 count = 2043 recv connect ip=127.0.0.1 port=54500 count = 2044 recv connect ip=127.0.0.1 port=54501 accept error: Too many open files
root@ubuntu:/home/huangcheng# ulimit -n 2048 root@ubuntu:/home/huangcheng# su - huangcheng huangcheng@ubuntu:~$ ulimit -n 2048 huangcheng@ubuntu:~$./cli .......................... ip=127.0.0.1 port=54499 count = 2043 ip=127.0.0.1 port=54500 count = 2044 ip=127.0.0.1 port=54501 count = 2045 socket: Too many open files能夠看到如今最大的鏈接數已是2045個了,雖然服務器端有某個鏈接沒有accept 返回。即poll 比 select 可以承受更多的併發鏈接,只受一個進程所能打開的最大文件描述符個數限制。能夠經過ulimit -n 修改,但一個系統所能打開的文件描述符個數也是有限的,這跟系統的內存大小有關係,因此說也不是能夠無限地並 發,能夠查看一下本機的容量:
huangcheng@ubuntu:~$ cat /proc/sys/fs/file-max 101598本機是虛擬機,內存2G,可以打開的文件描述符個數大約在10w個左右。