UNIX網絡編程——select函數的併發限制和 poll 函數應用舉例

1、用select實現的併發服務器,能達到的併發數,受兩方面限制編程


       一、一個進程能打開的最大文件描述符限制。這能夠經過調整內核參數。能夠經過ulimit -n來調整或者使用setrlimit函數設置, 但一個系統所能打開的最大數也是有限的,跟內存大小有關,能夠經過cat /proc/sys/fs/file-max 查看

       二、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個左右。
相關文章
相關標籤/搜索