用select實現的並發服務器,能達到的並發數一般受兩方面限制:
1)一個進程能打開的最大文件描述符限制。這可以通過調整內核參數來改變。可以通過ulimit -n(number)來調整或者使用setrlimit函數設置(需要root權限),但一個系統所能打開的最大數也是有限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看。
2)select中的fd_set集合容量的限制(FD_SETSIZE,一般為1024),這需要重新編譯內核才能改變。
對於第一個限制:
nclude其中,resource的一個取值 RLIMIT_NOFILE 代表指定比進程可打開的最大文件描述詞大一的值,超出此值,將會產生EMFILE錯誤。#include int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim);
rlim:描述資源軟硬限制的結構體,原型如下 struct rlimit { rlim_t rlim_cur; /* Soft limit */ rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */ };返回說明:
軟限制是一個建議性的, 最好不要超越的限制, 如果超越的話, 系統可能向進程發送信號以終止其運行.
而硬限制一般是軟限制的上限;
resource可用值
RLIMIT_AS
進程可用的最大虛擬內存空間長度,包括堆棧、全局變量、動態內存
RLIMIT_CORE
內核生成的core文件的最大大小
RLIMIT_CPU
所用的全部cpu時間,以秒計算
RLIMIT_DATA
進程數據段(初始化DATA段, 未初始化BSS段和堆)限制(以B為單位)
RLIMIT_FSIZE
文件大小限制
RLIMIT_SIGPENDING
用戶能夠掛起的信號數量限制
RLIMIT_NOFILE
打開文件的最大數目
RLIMIT_NPROC
用戶能夠創建的進程數限制
RLIMIT_STACK
進程棧內存限制, 超過會產生SIGSEGV信號
進程的資源限制通常是在系統初啟時由0#進程建立的,在更改資源限制時,須遵循下列三條規則:
1.任何一個進程都可將一個軟限制更改為小於或等於其硬限制。
2.任何一個進程都可降低其硬限制值,但它必須大於或等於其軟限制值。這種降低,對普通用戶而言是不可逆反的。
3.只有超級用戶可以提高硬限制。
/**示例: getrlimit/setrlimit獲取/設置進程打開文件數目**/ int main() { struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) == -1) err_exit("getrlimit error"); cout << "Soft limit: " << rl.rlim_cur << endl; cout << "Hard limit: " << rl.rlim_max << endl; cout << "------------------------->" << endl; rl.rlim_cur = 2048; rl.rlim_max = 2048; if (setrlimit(RLIMIT_NOFILE, &rl) == -1) err_exit("setrlimit error"); if (getrlimit(RLIMIT_NOFILE, &rl) == -1) err_exit("getrlimit error"); cout << "Soft limit: " << rl.rlim_cur << endl; cout << "Hard limit: " << rl.rlim_max << endl; }
測試最多可以建立多少個鏈接,下面是客戶端的代碼:
#include#include #include #include #include #include #include #include #include #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; }
我們來看一下server端輸出:
recv 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
解析:對於客戶端,最多只能開啟1021個連接套接字,因為總共是在Linux中最多可以打開1024個文件描述如,其中還得除去0,1,2。而服務器端只能accept 返回1020個已連接套接字,因為除了0,1,2之外還有一個監聽套接字listenfd,客戶端某一個套接字(不一定是最後一個)雖然已經建立了連接,在已完成連接隊列中,但accept返回時達到最大描述符限制,返回錯誤,打印提示信息。
client在socket()返回-1是調用sleep(4)解析
當客戶端調用socket准備創建第1022個套接字時,如上所示也會提示錯誤,此時socket函數返回-1出錯,如果沒有睡眠4s後再退出進程會有什麼問題呢?如果直接退出進程,會將客戶端所打開的所有套接字關閉掉,即向服務器端發送了很多FIN段,而此時也許服務器端還一直在accept ,即還在從已連接隊列中返回已連接套接字,此時服務器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連接的套接字的可讀事件,read 返回0,所以會有很多 client close 字段參雜在條目的輸出中,還有個問題就是,因為read 返回0,服務器端會將自身的已連接套接字關閉掉,那麼也許剛才說的客戶端某一個連接會被accept 返回,即測試不出服務器端真正的並發容量;
poll沒有select第二個限制, 即FD_SETSIZE的限制, 不用修改內核,但是第一個限制暫時還是無法避免的;
#includeint poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數nfds: 需要檢測事件的個數, 結構體數組大小(也可表示為文件描述符個數)(The caller should specify the number of items in the fds array in nfds.)
參數timeout: 超時時間(單位milliseconds, 毫秒),若為-1,表示永不超時。
poll 跟 select 還是很相似的,比較重要的區別在於poll 所能並發的個數跟FD_SETSIZE無關,只跟一個進程所能打開的文件描述符個數有關,可以在select 程序的基礎上修改成poll 程序,在運行服務器端程序之前,使用ulimit -n 2048 將限制改成2048個,注意在運行客戶端進程的終端也需更改,因為客戶端也會有所限制,這只是臨時性的更改,因為子進程會繼承這個環境參數,而我們是在bash命令行啟動程序的,故在進程運行期間,文件描述符的限制為2048個。
使用poll 函數的服務器端程序如下,和select大概用法差不多:
#include#include #include #include #include #include #include #include #include #include #include #include #include "read_write.h" #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while ( 0) int main() { 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); 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調整 */
可以看到現在最大的連接數已經是2045個了,雖然服務器端有某個連接沒有accept 返回。即poll 比 select 能夠承受更多的並發連接,只受一個進程所能打開的最大文件描述符個數限制。可以通過ulimit -n 修改,但一個系統所能打開的文件描述符個數也是有限的,這跟系統的內存大小有關系,所以說也不是可以無限地並發,我們在文章的開始也提到過,可以使用 cat /proc/sys/fs/file-max查看一下本機的容量。