Linux - socket2

阻塞I/O

先用一個圖來描述它:編程

實際上,以前咱們使用的套接口I/O編程都是用該模型,針對上面的圖進行說明一下:一旦套接口鏈接成功以後,就能夠recv數據了,以下:數組

會向系統發起請求來接收數據,而這個recv請求是阻塞的,那何時解除阻塞呢,直到對方等方數據過來,填充了recv這個套接口所對應的接收緩衝區,纔會解除,也就是說如圖:bash

也就是說,接收緩衝區以前是「沒有數據的」,一旦對等方發送數據過來,將接收緩衝區數據填充,這時候就會將數據從內核空間(也就是套接口接收緩衝區)複製到用戶空間,以下:服務器

由於咱們在recv的時候,會提供一個buffer,以下:網絡

一旦拷貝完成,recv函數就返回了,這時就能夠進行數據處理了,以下:多線程

非阻塞I/O【用得不多,作了解既可】

這個模式也是調用recv函數進行數據接收,可是會將其設置爲非阻塞模式(用fcntl(fd, F_SETFL, flag|O_NONBLOCK),以前有學過),這時recv函數既可沒有數據到來,也不會阻塞,若是內核中沒有數據時,它不會阻塞,會返回一個錯誤,錯誤代碼爲-1,會返回:EWOULDBLOCK,以下:併發

若是說還想獲取數據,則須要再次判斷若是等於-1,錯誤碼等於EWOULDBLOCK,再次提交請求:異步

直到有數據到來,這時的動做跟阻塞模式差很少,會將數據從內核空間拷貝到用戶空間,而後recv返回,而後就能夠數據處理了。 可是這種模型應用是不多的,由於在數據沒有到來的過程中,咱們須要不停地循環接收,直到數據到來,而這個循環接收並無阻塞,其實是對CPU資源的極大的浪費,對於這種循環接收,能夠稱爲忙等待(須要等待數據,可是數據又沒有到來,而又須要佔用CPU時間片),這種不推薦使用。socket

I/O複用(select和poll)【這個是主要研究的對象】

這種模型主要是經過select來實現的,該模式的一個思想是:用select來管理多個文件描述符,一旦有一個文件描述符檢測到了有數據到來,這時select就會返回,這時recv就不會阻塞了,就能夠將數據從內核空間拷到用戶空間,其中阻塞提早到了select了,這個模型是此次主要研究的對象,先大體知道了解一下。tcp

信號驅動I/O【用得不多,作了解既可】

在用戶空間中,利用sigaction安裝一個SIGIO的信號處理程序,這時程序就能夠作其它事情了,一旦有數據到來,就會以信號的方式來通知應用程序,而後處理程序就能夠調用recv來接收數據,緊接着就是從內核空間到用戶空間的數據拷貝過程,這時候recv函數是不會阻塞的,可見這種方式是通用信號來通知應用程序的,因此應用進程須要在信號處理程序中去處理接收數據的細節,那麼這樣的話就使得異步處理成爲可能,因此信號是異步處理的一種方式,這裏瞭解一下既可。

異步I/O

關於這個模形,因爲也沒有獲得推廣,因此這裏就略過,咱們把重點花在有意義的事上。

對於上面列的五種I/O模型, 接下來主要研究I/O複用模型中的select模型,對於它,上面介紹該模型時也說過,它能夠管理多個文件描述符,或者說能夠管理多個I/O,它的函數原形以下:

一旦某個I/O檢測到了咱們所感興趣的事件就馬上返回,若是有多個I/O,當發生事件時,將返回到的一些事件,填充到對應的集合當中(readfds),而且返回一個事件的個數,這時候就能夠輪循事件,一個個處理它,而這時候的事件是不會阻塞的,由於select已經提早阻塞了,它的返回意味着事件已經到來了,爲了更好的理解select的用法,咱們首先來看一下上次咱們所實現的回射客戶/服務器程序具備什麼樣的問題,來作一個回顧,而後再用select函數來解決這個問題:

這時候,將服務端關才掉,這裏用kill命令來模擬:

這時再來查看下狀態:

而根據TCP關閉鏈接的狀態來看:

若是客戶端read返回爲0時,則應該會調close,進而服務端最終狀態爲TIME_WAIT狀態,爲啥沒有進入此狀態呢?簡略的說就是因爲客戶端阻塞在這個位置了:

本質的緣由是從鍵盤接收數據跟網絡接收數據這兩個事件沒有辦法同時進行處理,這時能夠用select來進行管理,管理fgets標準輸入的I/O和sock套接口I/O,一旦其中一個或者多個產生可讀事件,則進行處理,也就是這個時候,既能夠在接收鍵盤數據的同時,也能夠檢測到網絡數據的到來,這時就能夠進行相應的處理,由於select函數可以解除阻塞,因此,接下來,利用它來改進回射客戶端程序。

理解參數:

select能夠當作是一個管理者,能夠管理多個I/O,一旦其中的一個I/0檢測到咱們感興趣的事件,select函數就返回,返回值爲檢測到的事件個數,而且返回哪些I/O發生了事件,遍歷這些事件,進而處理事件,根據這些理論,對於其函數的參數就比較容易理解了:

①、fd_set *readfds:這時一個集合,表示一個讀的集合,也是最經常使用的一個集合,表示若是檢測到有讀的套接口則放到這個集合中,一旦數據可讀,select就能夠返回。

②、fd_set *writefds【此次學習先用不上,能夠直接填空】:這個從單詞上來看,就很容易理解,表示可寫的集合。

③、fd_set *exceptfds【此次學習先用不上,能夠直接填空】:異常的集合。

④、struct timeval *timeout:這表示超時時間,若是填寫NULL,則不會超時,必定要檢測到事件後纔會返回;若是指定是超時時間,則在超時時間到來的時候尚未檢測到事件,也會返回,這時返回的事件個數就等於0,另外select返回失敗爲-1。

⑤、nfds:它表示存放在集合中(readfds、writefds、exceptfds)的這些描述符的最大值+1,好比:readfds集合中存放了描述符三、五、8,而writefds集合中存放了描述符四、9,那麼這個參數就是集合中最大描述符9+1=10。

另對,對於返回值,是返回哪些I/O發生了事件,這是什麼意思呢,假如readfds集合提交了三、四、5,我要關心這三個I/O的可讀事件,這時若是3跟5發生了可讀事件,我如何標識它呢?實際上就是將readfds這個集合改變,集合的內容改爲了三、5,這是返回的準備到的個數爲2,用圖來表示以下:

從圖中描述能夠看出,readfds是輸出輸出參數,同理,writefds、exceptfds、timeout也是輸出輸出參數(好比指定的是2s的時間,可是1s內就返回了,這時後它的值就爲剩餘的時間)。

與select這些集合操做相配合的有四個宏進行操做,下面來簡述一下,以後都會用到:

將文件描述符fd從集合set當中移除。

斷定文件描述符fd是否在集合set中,注意:這裏的set不是輸入輸出參數,也就是隻讀的。

將文件描述符fd添加到集合set當中。

清空集合。

好了,下面就用上面的一些理論來改進程序,來更好的理解select函數的用法,對於以前的代碼,只須要對客戶端這個函數的實現進行改進,以下:

首先先將函數的實現註釋掉,而後一步步用select來改造,根據select的參數來編寫:

第一步,首先得到最大的文件描述符,也是第一個填充第一個參數:

另外能夠將sock和stdin兩個文件描述符加入到集合中:

接下來,因爲事件成功返回了,那就能夠判斷標準輸入fd_stdin、sock是否在rset集合裏,若是在集合中就證實已經檢測到了事件,而後就能夠分別進行判斷處理了:

對於套接口產生事件,應該將原來的代碼挪進來:

實現以下:

對於鍵盤的輸入事件,也應該將原來的代碼挪過來,以下:

下面編譯運行看下以前的問題有沒有解決:

這些狀態都比較好理解,下面到了關鍵驗證步驟,就是先將服務端關閉掉:

從中能夠看到,服務端變爲了TIME_WAIT狀態,也就是向前進常邁進了,此次就不會由於fgets阻塞形成沒法進入此正常狀態了,而客戶端這時就變爲CLOSED狀態了,固然經過命令就看不到此狀態了,以下:

最後再貼一下通過改用select修改的客戶服務回射的完整代碼以下:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

void handle_sigchld(int sig)
{
/*    wait(NULL);*/
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

int main(void)
{
/*    signal(SIGCHLD, SIG_IGN);*/
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        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 = 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");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            echo_srv(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
複製代碼

echocli.c:

#include <unistd.h>
#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)

ssize_t readn(int fd, void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nread = read(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nread == 0)
                        return count - nleft;

                bufp += nread;
                nleft -= nread;
        }

        return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nwritten = write(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nwritten == 0)
                        continue;

                bufp += nwritten;
                nleft -= nwritten;
        }

        return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
        while (1)
        {
                int ret = recv(sockfd, buf, len, MSG_PEEK);
                if (ret == -1 && errno == EINTR)
                        continue;
                return ret;
        }
}


ssize_t readline(int sockfd, void *buf, size_t maxline)
{
        int ret;
        int nread;
        char *bufp = buf;
        int nleft = maxline;
        while (1)
        {
                ret = recv_peek(sockfd, bufp, nleft);
                if (ret < 0)
                        return ret;
                else if (ret == 0)
                        return ret;

                nread = ret;
                int i;
                for (i=0; i<nread; i++)
                {
                        if (bufp[i] == '\n')
                        {
                                ret = readn(sockfd, bufp, i+1);
                                if (ret != i+1)
                                        exit(EXIT_FAILURE);

                                return ret;
                        }
                }

                if (nread > nleft)
                        exit(EXIT_FAILURE);

                nleft -= nread;
                ret = readn(sockfd, bufp, nread);
                if (ret != nread)
                        exit(EXIT_FAILURE);

                bufp += nread;
        }

        return -1;
}

void echo_cli(int sock)
{
    /*
    char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                        ERR_EXIT("readline");
                else if (ret == 0)
                {
                        printf("client close\n");
                        break;
                }

                fputs(recvbuf, stdout);
                memset(sendbuf, 0, sizeof(sendbuf));
                memset(recvbuf, 0, sizeof(recvbuf));
        }

        close(sock);
*/

    fd_set rset;//聲明一個可讀的集合
    FD_ZERO(&rset);//將集合清空

    int nready;//檢測到的事件個數

    //得到最大的文件描述符
    int maxfd;
    int fd_stdin = fileno(stdin);
    if (fd_stdin > sock)
        maxfd = fd_stdin;
    else
        maxfd = sock;

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (1)
    {
        FD_SET(fd_stdin, &rset);
        FD_SET(sock, &rset);
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if (nready == -1)
            ERR_EXIT("select");

        if (nready == 0)
            continue;

        if (FD_ISSET(sock, &rset))
        {//套接口產生了事件
            int ret = readline(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                    ERR_EXIT("readline");
            else if (ret == 0)
            {
                    printf("server close\n");
                    break;
            }

            fputs(recvbuf, stdout);
            memset(recvbuf, 0, sizeof(recvbuf));
        }
        if (FD_ISSET(fd_stdin, &rset))
        {//標準輸入產生的事件
            if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
                break;
            writen(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
    }

    close(sock);
}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}

int main(void)
{
/*
    signal(SIGPIPE, handle_sigpipe);
*/
    signal(SIGPIPE, SIG_IGN);
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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));


    echo_cli(sock);

    return 0;
}
複製代碼

上面是函數的原形,下面用理論闡述一下它,它能夠當作是一箇中心管理器,可以統一管理多個I/O,一旦其中的一個或多個I/O產生了咱們所感興趣的事件,就會被select檢測到並返回,再來回顧一下參數,第一個參數是nfds,表示感興趣的文件描述符的最大值+1,這裏多解釋一下,爲何是最大值+1呢?實際上select函數是在遍歷它所感興趣的文件描述符是否產生了事件,是從0開始遍歷,至nfds的一個[0,nfds)的一個閉開區間,而一般咱們寫for循環時都會這樣寫:

for(i=0; i<n; i++){

//TODO

}

正好這也是一個閉開區間,因此第一個參數nfds爲最大描述符+1,

第二個參數readfds:可讀事件集合

第三個參數writefds:可寫事件集合

第四個參數exceptfds:異常事件集合

第五個參數timeout:超時時間

一旦檢測到了多個事件,就須要一個一個遍歷它,一一處理這些I/O事件,一般將用select實現的服務器稱之爲併發服務器。爲啥叫併發服務器呢?由於當咱們檢測到多個I/O事件以後,其實是沒法並行處理這些I/O事件。實際上select處理事件是按順序執行的,好比產生了三個事件,則是先執行第一個事件,而後再執行第二個事件,以此類推,因此說它不是並行的,而是併發,爲啥是併發,是由於處理這些事件時間也不能太長,也就是說select沒法實現並行處理,也就是沒法充分利用多核CPU的特色,實際上對於單核的cpu來講,是根據沒有並行可言的,而對於多核cpu,select是沒法充分利用的,那這時該怎麼辦呢?能夠採用多進程或多線程,關於併發與並行處理,這個以後會研究,這裏先大體瞭解一下概念既可。

上節中用select改進了回射客戶端的問題,程序能夠同時監測兩種事件,一種是標準輸入I/O事件,還一種是網絡I/O,而不至於由於程序阻塞在標準輸入I/O,而同時網絡I/O也已經到達了而不能處理,這就是使用select的好處。

上節中只使用了讀條件了,此次會對其它事件也進行學習。

以上是可讀事件產生的四種狀況。

下面,就用select函數來改進回射服務器程序,上節只是改進了回射客戶端程序。

先來回顧一下目前的服務器程序,是每鏈接成功一個客戶端,就會建立一個子進程出來進行處理:

這種服務器也叫作併發服務器,經過建立一個進程來達到併發的目的,當有多個客戶端鏈接時,就會有多個進程,那有沒有可能用一個進程來實現併發呢?固然是能夠作到的,也就是用select,其最終緣由是由於它能管理多個I/O,實際上,對於單核CPU來講,select處理併發並不會比多進程效率低,由於多進程在單核的狀況下實際上仍是按順序來進行處理的,因此,下面則正式進行修改:

首先將這些代碼註釋掉,由於是須要改爲用select實現的:

編寫方法基本跟上節當中的客戶端的差很少:

【提示】:記得先記住這個allset,以後隨着不斷加入代碼邏輯,就會天然而然顯現它的做用了。

接下來,因爲事件成功返回了,那就能夠判斷標準輸入listenfd是否在rset集合裏,若是在集合中就證實已經檢測到了事件,而後就能夠分別進行判斷處理了:

另外,因爲這一次是單進程的實現方式,當有多個客戶端鏈接時,其conn客戶端鏈接信息是須要用戶個數組來保存的,

int main(void)
{
/*    signal(SIGCHLD, SIG_IGN);*/
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        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 = 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");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    /*
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            echo_srv(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    */
    
    int client[FD_SETSIZE];//定義一個數組用來保存conn,其中FD_SETSIZE爲最大文件描述符個數,不能超過它
    int i;
    for (i=0; i<FD_SETSIZE; i++)//對裏面的數組都初使化爲-1
        client[i] = -1;

    int nready;//檢測到的事件個數
    int maxfd = listenfd;//獲取最大的文件描述符,目前listenfd最大
    
    fd_set rset;//聲明一個可讀的集合
    fd_set allset;
    //如下兩句是將集合清空
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);//將監聽套接口放到allset當中
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            for (i=0; i<FD_SETSIZE; i++)//將conn放到數組當中
            {
                if (client[i] < 0)
                {
                    client[i] = conn;
                    break;
                }
            }

            if (i == FD_SETSIZE)//沒有找到空閒的位置,也就是鏈接數已經達到了上線,則給出提示
            {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }
        &emsp;&emsp;printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));//打印輸出對方的IP和端口信息
        }
    }

    return 0;
}
複製代碼

接下來要作一件事,這時已經獲得了conn的套接口,下一次再調用select時咱們也須要關心它的可讀事件,這時須要作以下處理:

那思考一下爲何要用到allset這個變量?這時由於rset會被select函數所改變,因此對於全部感興趣的事件須要存放在allset當中,

接下來,則處理已鏈接套接口事件了,對於這個套接口會有不少個,由於能夠鏈接不少客戶端,因此處理以下:

int main(void)
{
/*    signal(SIGCHLD, SIG_IGN);*/
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        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 = 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");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    int client[FD_SETSIZE];//定義一個數組用來保存conn,其中FD_SETSIZE爲最大文件描述符個數,不能超過它
    int i;
    for (i=0; i<FD_SETSIZE; i++)//對裏面的數組都初使化爲-1
        client[i] = -1;

    int nready;//檢測到的事件個數
    int maxfd = listenfd;//獲取最大的文件描述符,目前listenfd最大
    
    fd_set rset;//聲明一個可讀的集合
    fd_set allset;
    //如下兩句是將集合清空
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);//將監聽套接口放到allset當中
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            for (i=0; i<FD_SETSIZE; i++)//將conn放到數組當中
            {
                if (client[i] < 0)
                {
                    client[i] = conn;
                    break;
                }
            }

            if (i == FD_SETSIZE)//沒有找到空閒的位置,也就是鏈接數已經達到了上線,則給出提示
            {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }
            printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
            
            FD_SET(conn, &allset);

            if (--nready <= 0)
                continue;
        }

        for (i=0; i<=FD_SETSIZE; i++)
        {
            conn = client[i];
            if (conn == -1)
                continue;

            if (FD_ISSET(conn, &rset))
            {
                char recvbuf[1024] = {0};
                int ret = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline");
                if (ret == 0)
                {//對方關閉
                    printf("client close\n");
                    FD_CLR(conn, &allset);//從集合中將此已鏈接接口清除
                    client[i] = -1;//而且還原默認標識
                }

                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
                
            }
        }
    }

    return 0;
}
複製代碼

另外,還須要關心一下最大描述符maxfd,當產生了新的鏈接套接口時,是須要將其進行更新的,因而修改代碼以下:

好了,下面來編譯運行一下:

可見,用單進程的方式也實現了多個客戶端併發的處理。

另外,此處程序還有可優化的地方,就是處理已鏈接事件的時候,老是遍歷FD_SETSIZE,能夠再加一個變量,用來記錄最大的不空閒的i值,修改代碼以下:

int main(void)
{
/*    signal(SIGCHLD, SIG_IGN);*/
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        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 = 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");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    int client[FD_SETSIZE];//定義一個數組用來保存conn,其中FD_SETSIZE爲最大文件描述符個數,不能超過它
    int maxi = 0;//用來記錄最大的可鏈接套接口存放的位置
    int i;
    for (i=0; i<FD_SETSIZE; i++)//對裏面的數組都初使化爲-1
        client[i] = -1;

    int nready;//檢測到的事件個數
    int maxfd = listenfd;//獲取最大的文件描述符,目前listenfd最大
    
    fd_set rset;//聲明一個可讀的集合
    fd_set allset;
    //如下兩句是將集合清空
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);//將監聽套接口放到allset當中
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            for (i=0; i<FD_SETSIZE; i++)//將conn放到數組當中
            {
                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("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 = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline");
                if (ret == 0)
                {//對方關閉
                    printf("client close\n");
                    FD_CLR(conn, &allset);//從集合中將此已鏈接接口清除
                    client[i] = -1;//而且還原默認標識
                }

                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
                
            }
        }
    }

    return 0;
}
複製代碼

最後再來看一下整個服務端的代碼以下:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

void handle_sigchld(int sig)
{
/*    wait(NULL);*/
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

int main(void)
{
/*    signal(SIGCHLD, SIG_IGN);*/
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        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 = 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");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    int client[FD_SETSIZE];//定義一個數組用來保存conn,其中FD_SETSIZE爲最大文件描述符個數,不能超過它
    int maxi = 0;//用來記錄最大的可鏈接套接口存放的位置
    int i;
    for (i=0; i<FD_SETSIZE; i++)//對裏面的數組都初使化爲-1
        client[i] = -1;

    int nready;//檢測到的事件個數
    int maxfd = listenfd;//獲取最大的文件描述符,目前listenfd最大
    
    fd_set rset;//聲明一個可讀的集合
    fd_set allset;
    //如下兩句是將集合清空
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);//將監聽套接口放到allset當中
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);

        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            for (i=0; i<FD_SETSIZE; i++)//將conn放到數組當中
            {
                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("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 = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline");
                if (ret == 0)
                {//對方關閉
                    printf("client close\n");
                    FD_CLR(conn, &allset);//從集合中將此已鏈接接口清除
                    client[i] = -1;//而且還原默認標識
                }

                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
                
            }
        }
    }

    return 0;
}
複製代碼

echocli.c:

#include <unistd.h>
#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)

ssize_t readn(int fd, void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nread = read(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nread == 0)
                        return count - nleft;

                bufp += nread;
                nleft -= nread;
        }

        return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nwritten = write(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nwritten == 0)
                        continue;

                bufp += nwritten;
                nleft -= nwritten;
        }

        return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
        while (1)
        {
                int ret = recv(sockfd, buf, len, MSG_PEEK);
                if (ret == -1 && errno == EINTR)
                        continue;
                return ret;
        }
}


ssize_t readline(int sockfd, void *buf, size_t maxline)
{
        int ret;
        int nread;
        char *bufp = buf;
        int nleft = maxline;
        while (1)
        {
                ret = recv_peek(sockfd, bufp, nleft);
                if (ret < 0)
                        return ret;
                else if (ret == 0)
                        return ret;

                nread = ret;
                int i;
                for (i=0; i<nread; i++)
                {
                        if (bufp[i] == '\n')
                        {
                                ret = readn(sockfd, bufp, i+1);
                                if (ret != i+1)
                                        exit(EXIT_FAILURE);

                                return ret;
                        }
                }

                if (nread > nleft)
                        exit(EXIT_FAILURE);

                nleft -= nread;
                ret = readn(sockfd, bufp, nread);
                if (ret != nread)
                        exit(EXIT_FAILURE);

                bufp += nread;
        }

        return -1;
}

void echo_cli(int sock)
{
/*
    char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                        ERR_EXIT("readline");
                else if (ret == 0)
                {
                        printf("client close\n");
                        break;
                }

                fputs(recvbuf, stdout);
                memset(sendbuf, 0, sizeof(sendbuf));
                memset(recvbuf, 0, sizeof(recvbuf));
        }

        close(sock);
*/

    fd_set rset;
    FD_ZERO(&rset);

    int nready;
    int maxfd;
    int fd_stdin = fileno(stdin);
    if (fd_stdin > sock)
        maxfd = fd_stdin;
    else
        maxfd = sock;

    char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};

    while (1)
    {
        FD_SET(fd_stdin, &rset);
        FD_SET(sock, &rset);
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready == -1)
            ERR_EXIT("select");

        if (nready == 0)
            continue;

        if (FD_ISSET(sock, &rset))
        {
            int ret = readline(sock, recvbuf, sizeof(recvbuf));
                    if (ret == -1)
                            ERR_EXIT("readline");
                    else if (ret == 0)
                    {
                            printf("server close\n");
                            break;
                    }

                       fputs(recvbuf, stdout);
                    memset(recvbuf, 0, sizeof(recvbuf));
        }
        if (FD_ISSET(fd_stdin, &rset))
        {
            if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
                break;
            writen(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
    }

    close(sock);
}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}

int main(void)
{
/*
    signal(SIGPIPE, handle_sigpipe);
*/
    signal(SIGPIPE, SIG_IGN);
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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));


    echo_cli(sock);

    return 0;
}
複製代碼

①close終止了數據傳送的兩個方向。

②shutdown能夠有選擇的終止某個方向的數據傳送或者終止數據傳送的兩個方向。 可是,在實際應用中,可能會遇到這樣一個狀況,既使咱們關閉了其中的某一端應用,但對於先前發送出去的數據可以獲得對方的應答,下面能夠用圖來表示一下:

進一步說明一下"close終止了數據傳送的兩個方向":

查看一下man幫助:

①、SHUT_RD=1表明不能向管道中讀取數據了。

②、SHUT_WR表明不能向管道中寫入數據了。

③、SHUT_RDWR表明不能向管道中讀寫數據了。

因此正好反映了,"shutdown能夠有選擇的終止某個方向的數據傳送或者終止數據傳送的兩個方向"。 【說明】:

③shutdown how=1就能夠保證對等方接收到一個EOF字符【也就是向對方發送了FIN TCP段】,而無論其餘進程是否已經打開了套接字。而close不能保證,直到套接字引用計數減爲0時才發送。也就是說直到全部的進程都關閉了套接字。

對於上面提到的,要想讓client只終止寫的一端,能夠用shutdown設置成SHUT_WR,也就是how=1:

從上面的解釋中來看:"close不能保證,直到套接字引用計數減爲0時才發送。也就是說直到全部的進程都關閉了套接字",並非只要調用了close就會立馬向對方發送FIN tcp段,而是須要引用計數減爲0時才發送,回想一下原來用fork()實現服務端的代碼:

而若是改用shutdown:

說了一系列理論事後,下面用實驗來進一步加深對shutdown的理解,也就是用shutdown來改寫客戶端。

目前咱們的回射客戶/服務端程序都已經改用select函數來實現了,下面咱們來作一個這樣的實驗:

echocli.c:

echosrv.c:

下面來看下效果:

可見,並沒回將數據回射回客戶端,而且客戶端這邊報了一個錯:

這是哪打印出來的呢?分析一下代碼流程:

並且客戶端的這個錯誤,還會致使服務端也崩潰掉了。

那若是這樣處理是否能夠避免這個報錯呢?

編譯運行:

能夠看出,客戶端並無報錯了,可是服務端仍是崩潰了,這是爲何呢?

因此,解決服務端崩潰很簡單,處理以下:

再次編譯運行:

這時,能夠看到服務端打印了"client close",並且沒有崩潰了,這是因爲執行到了這個流程:

可是目前殘留在管道當中的數據都沒法回顯給客戶端,因此接下來用shutdown來進行改進,讓它在客戶端關閉的狀況下還能回射回來:

編譯以下:

從上面能夠看到,客戶端成功回顯了,可是,發現有個小問題,就是當客戶端回顯數據以後,服務端沒有把客戶端給關閉掉,因此,程序應該有個bug,檢查一下服務端的代碼:

因此,修改以下:

編譯運行:

這時問題就成功解決,此時的客戶端就相對要完善一些了,實際上在編寫TCP程序時是須要考慮到這一點的,實際上客戶端程序還有一個地方是須要注意的:

因此能夠加入一個flag進行處理:

固然程序運行效果是同樣的,只是程序更嚴謹一些。

①、alarm【不經常使用,瞭解既可】

它的實現思路是這樣的:

可是這種方案有必定的問題,由於鬧鐘可能會做爲其它的用途,這時所設置的鬧鐘跟其它用途的鬧鐘會產生衝突,而這些衝突的解決,會比較麻煩,這裏就很少討論了,由於不使用它,僅瞭解既可,是不會用鬧鐘的方式來實現超時的。

②、套接字選項【不經常使用,瞭解既可】

SO_SNDTIMEO:發送的超時時間

      SO_RCVTIMEO:接收的超時時間
複製代碼

具體實現思路是這樣的:

可是,也不會用這種方式,由於存在移植兼容的問題。

③、select【經常使用,此次學習的重點】

read_timeout函數封裝:

下面會仔細分析是如何封裝的,在封裝以後,先看下函數原形:

因此,先不關心它是如何實現的,依照這個函數原形,其使用方法以下【僞代碼】:

另外,若是想按照正常的方式來處理,能夠將超時參數傳0既可,以下:

/**
 * read_timeout - 讀超時檢測函數,不含讀操做
 * @fd: 文件描述符
 * @wait_seconds: 等待超時秒數,若是爲0表示不檢測超時
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;//默認返回值爲0,也就是未超時
    if (wait_seconds > 0)
    {//若是當傳過來的超時時間大於0時才作select超時處理
        fd_set read_fdset;
        struct timeval timeout;//超時參數

        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);//將描述符加入到可讀集合中

        //設置超時
        timeout.tv_sec = wait_seconds;//只關心秒
        timeout.tv_usec = 0;//不關心微秒
        do
        {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);//這時傳入超時時間
        } while (ret < 0 && errno == EINTR/** 若是是中斷信號,則忽略 **/);

        if (ret == 0)
        {//表示已經超時
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)//表示沒有超時,成功產生了可讀事件
            ret = 0;
    }

    return ret;
}
複製代碼

【說明】:對於這個工具方法,重在理解是如何封裝的,不必定要本身徹底寫,以後能夠直接拿過來用。

write_timeout函數封裝:

當理解了read_timeout函數的實現,對於寫函數的實現就不難了,下面直接貼出來,基本相似,就很少說了:

/**
 * write_timeout - 讀超時檢測函數,不含寫操做
 * @fd: 文件描述符
 * @wait_seconds: 等待超時秒數,若是爲0表示不檢測超時
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set write_fdset;//只是這時變成了寫的集合
        struct timeval timeout;

        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);//將入到寫集合中

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, NULL, NULL, &write_fdset, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}
複製代碼

accept_timeout函數封裝:

關於這個函數的封裝也不是太難理解,下面也以註釋的方式貼出來:

/**
 * accept_timeout - 帶超時的accept
 * @fd: 套接字
 * @addr: 輸出參數,返回對方地址
 * @wait_seconds: 等待超時秒數,若是爲0表示正常模式
 * 成功(未超時)返回已鏈接套接字,超時返回-1而且errno = ETIMEDOUT
 */
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;//返回值
    socklen_t addrlen = sizeof(struct sockaddr_in);//定義一個地址的長度

    if (wait_seconds > 0)
    {//若是超時時間大於0才進行select超時處理,不然不檢測超時,直接調用accept
        fd_set accept_fdset;//定義一個集合
        struct timeval timeout;//定義一個超時結構體
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);//加入集合
        timeout.tv_sec = wait_seconds;//設置超時時間
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == -1)//表明select失敗了
            return -1;
        else if (ret == 0)
        {//超時了
            errno = ETIMEDOUT;
            return -1;
        }
    }
    
    //若是走到這裏,證實檢測到了事件,則須要對其進行處理;或者是超時時間沒有設置也會走到這
    if (addr != NULL)//有地址的accept,這時再也不阻塞
        ret = accept(fd, (struct sockaddr*)addr, &addrlen);//此時返回鏈接套接字
    else//無地址的accept
        ret = accept(fd, NULL, NULL);
    if (ret == -1)//表示accept失敗
        ERR_EXIT("accept");

    return ret;
}
複製代碼

connect_timeout函數封裝:這個函數最難~

首先先明白一點,爲啥要設置鏈接超時呢?這裏須要從鏈接創建的三次握手提及,以下圖:

下面來看下具體函數的實現,相比前幾個,這個要複雜一些,由於不可以直接調用connect(),一旦調用了它,就意味着阻塞了,因此說但願不能阻塞的方式調用,因此須要將文件描述符設置爲非阻塞模式,這裏封裝成了一個方法,以下:

/**
 * activate_noblock - 設置I/O爲非阻塞模式
 * @fd: 文件描符符
 */
void activate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);//得到原來的模式
    if (flags == -1)
        ERR_EXIT("fcntl");

    flags |= O_NONBLOCK;//設置非阻塞模式
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl");
}
複製代碼

另外,還配對一個清除非阻塞模式的方法:

/**
 * deactivate_nonblock - 設置I/O爲阻塞模式
 * @fd: 文件描符符
 */
void deactivate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1)
        ERR_EXIT("fcntl");

    flags &= ~O_NONBLOCK;
    ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1)
        ERR_EXIT("fcntl");
}
複製代碼

【說明】:關於上面兩個函數的實現,能夠參考以前學習的fcntl函數。

/**
 * connect_timeout - connect
 * @fd: 套接字
 * @addr: 要鏈接的對方地址
 * @wait_seconds: 等待超時秒數,若是爲0表示正常模式
 * 成功(未超時)返回0,失敗返回-1,超時返回-1而且errno = ETIMEDOUT
 */
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
        activate_nonblock(fd);//設置套接字爲非阻塞模式

    ret = connect(fd, (struct sockaddr*)addr, addrlen);
    if (ret < 0 && errno == EINPROGRESS)
    {//鏈接正在處理,這時應該用select檢測鏈接的超時
        fd_set connect_fdset;//定義一個鏈接的集合
        struct timeval timeout;
        FD_ZERO(&connect_fdset);//將鏈接加入集合中
        FD_SET(fd, &connect_fdset);
        timeout.tv_sec = wait_seconds;//定義超時時間
        timeout.tv_usec = 0;
        do
        {
            /* 一量鏈接創建,套接字就可寫,這裏是將關心寫的事件 */
            ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == 0)
        {//表示鏈接超時了
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret < 0)//鏈接失敗了
            return -1;
        else if (ret == 1)
        {//這時檢測到有可寫事件了
            /* ret返回爲1,可能有兩種狀況,一種是鏈接創建成功,一種是套接字產生錯誤,*/
            /* 此時錯誤信息不會保存至errno變量中,所以,須要調用getsockopt來獲取。 */
            int err;
            socklen_t socklen = sizeof(err);
            int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);//獲取套接字的錯誤
            if (sockoptret == -1)
            {//表示獲取套接字錯誤
                return -1;
            }
            if (err == 0)
            {//鏈接創建成功
                ret = 0;
            }
            else
            {//套接字產生錯誤
                errno = err;
                ret = -1;
            }
        }
    }
    if (wait_seconds > 0)
    {
        deactivate_nonblock(fd);//還原套接字爲阻塞模式
    }
    return ret;
}
複製代碼

下面用程序來使用一下上面的超時函數,仍是用回射服務端/客戶程序,可是不是用以前的,而是用一個最簡單的,重在測試:

srv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.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 listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
        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 = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen;    
    int conn;
    
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

    return 0;
}
複製代碼

cli.c:

#include "sysutil.h"

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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");


    int ret = connect_timeout(sock, &servaddr, 5);//設置超時爲5秒
    if (ret == -1 && errno == ETIMEDOUT)
    {
        printf("timeout...\n");
        return 1;
    }
    else if (ret == -1)
        ERR_EXIT("connect_timeout");

    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));

    return 0;
}
複製代碼

爲了看清楚connect_timeout內部執行的流程是怎麼樣,能夠在其內部打印一些日誌就知道了,加入日誌以下:

int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if (wait_seconds > 0)
        activate_nonblock(fd);//設置套接字爲非阻塞模式

    ret = connect(fd, (struct sockaddr*)addr, addrlen);
    if (ret < 0 && errno == EINPROGRESS)
    {//鏈接正在處理,這時應該用select檢測鏈接的超時
        printf("AAAAA\n");
        fd_set connect_fdset;//定義一個鏈接的集合
        struct timeval timeout;
        FD_ZERO(&connect_fdset);//將鏈接加入集合中
        FD_SET(fd, &connect_fdset);
        timeout.tv_sec = wait_seconds;//定義超時時間
        timeout.tv_usec = 0;
        do
        {
            /* 一量鏈接創建,套接字就可寫,這裏是將關心寫的事件 */
            ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == 0)
        {//表示鏈接超時了
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret < 0)//鏈接失敗了
            return -1;
        else if (ret == 1)
        {//這時檢測到有可寫事件了
            printf("BBBBB\n");
            /* ret返回爲1,可能有兩種狀況,一種是鏈接創建成功,一種是套接字產生錯誤,*/
            /* 此時錯誤信息不會保存至errno變量中,所以,須要調用getsockopt來獲取。 */
            int err;
            socklen_t socklen = sizeof(err);
            int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);//獲取套接字的錯誤
            if (sockoptret == -1)
            {//表示獲取套接字錯誤
                return -1;
            }
            if (err == 0)
            {//鏈接創建成功
                printf("CCCCCC\n");
                ret = 0;
            }
            else
            {//套接字產生錯誤
                printf("DDDDDDD\n");
                errno = err;
                ret = -1;
            }
        }
    }
    if (wait_seconds > 0)
    {
        deactivate_nonblock(fd);//還原套接字爲阻塞模式
    }
    return ret;
}
複製代碼

編譯運行:

因爲connect本地模擬不了超時效果,由於沒有網絡擁塞的狀況,下面能夠演示一個錯誤,就是在服務端沒有運行時,直接運行客戶端,以下:

這是在客戶端這一段代碼報出來的:

因爲數據比較少,雖然說已經封裝好了超時的函數,可是很差演示網絡擁塞致使的超時,不過,重在理解代碼。

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

①、一個進程能打開的最大文件描述符限制。這能夠經過調整內核參數。

那最大文件描述符是多少呢?能夠用如下命令查看出來:

其實這個數是能夠進行調整的,可是前提得是root用戶纔有權限調整,以下:

此時再更改:

以上是經過命令調整進程支持的最大描述符個數,那用代碼能改麼?固然也能,下面來看下如何編寫:

獲取資源的限制能夠經過getrlimit()函數,查看man幫助:

其中第一個參數咱們須要設置它:

因而乎編寫一個測試程序先來得到最大描述符的個數:

編譯運行:

下面來調整一下它的大小:

nofile_limit.c:

#include <signal.h>
#include <sys/wait.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)
{
    struct rlimit rl;
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        ERR_EXIT("getrlimit");

    printf("%d\n", (int)rl.rlim_max);

    rl.rlim_cur = 2048;
    rl.rlim_max = 2048;
    if (setrlimit(RLIMIT_NOFILE, &rl) < 0)//調整描述符限制
        ERR_EXIT("setrlimit");

    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)//再次打印修改後的大小
        ERR_EXIT("getrlimit");

    printf("%d\n", (int)rl.rlim_max);
    return 0;
}
複製代碼

編譯運行:

那麼下面先將切至root用戶,而後再執行該程序:

可是,它只能改變當前進程的大小,若是用ulimit -n查看,看到的大小是父進程的,仍是沒有變:

關於調整內核增大描述符個數,是很容易改變select併發數的,可是還有另一個因素,就不那麼容易了,由於得編譯內核才行,以下:

關於最大限制,下面用程序來驗證下,先編寫一個簡單客戶端,不斷循環向服務端鏈接,看最終鏈接成功的個數,不加其它邏輯:

conntest.c:

#include <unistd.h>
#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;//用來記數,當客戶端與服務端鏈接成功則加1
    while(1)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            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;
}
複製代碼

服務端仍是用以前的,代碼以下:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

void handle_sigchld(int sig)
{
/*    wait(NULL);*/
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}

int main(void)
{
    signal(SIGPIPE, handle_sigpipe);
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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 = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen;    
    int conn;

    int i;
    int client[FD_SETSIZE];
    int maxi = 0;

    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);
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            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("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 = readline(conn, recvbuf, 1024);
                        if (ret == -1)
                                ERR_EXIT("readline");
                        if (ret == 0)
                        {
                                printf("client close\n");
                    FD_CLR(conn, &allset);
                    client[i] = -1;
                    close(conn);
                        }

                        fputs(recvbuf, stdout);
                        writen(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
                
            }
        }
    }    
    return 0;
}
複製代碼

編譯運行:

其中異常是報在這一段代碼:

爲了看到服務端的鏈接數,也打印一下個數:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

void handle_sigchld(int sig)
{
/*    wait(NULL);*/
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}

int main(void)
{
    int count = 0;//客戶端鏈接個數累加器
    signal(SIGPIPE, handle_sigpipe);
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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 = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen;    
    int conn;

    int i;
    int client[FD_SETSIZE];
    int maxi = 0;

    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);
    while (1)
    {
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            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("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
            printf("count = %d\n", ++count);//當客戶端鏈接成功,打印一下個數    
            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 = readline(conn, recvbuf, 1024);
                        if (ret == -1)
                                ERR_EXIT("readline");
                        if (ret == 0)
                        {
                                printf("client close\n");
                    FD_CLR(conn, &allset);
                    client[i] = -1;
                    close(conn);
                        }

                        fputs(recvbuf, stdout);
                        writen(conn, recvbuf, strlen(recvbuf));

                if (--nready <= 0)
                    break;
                
            }
        }
    }    
    return 0;
}
複製代碼

編譯運行:

下面來解釋一下爲啥服務端會收到了不少client close:

因爲客戶端有關閉,因此服務端打印的count=1021個就不許了,下面來解決一下客戶端關閉的問題,其很簡單的,就是在發送1022個鏈接時,先休眠一段時間,而後再讓進程退出既可,具體以下:

編譯運行:

②、select中的fd_set集合容量的限制(FD_SETSIZE,該宏默認是1024) ,這須要從新編譯內核。

這是第二個受限的地方,從代碼中來解釋:

下面要學習一個函數,它沒有FD_SETSIZE的限制,以下:

經過man幫助查看下,struct pollfd結構體的結構:

那結構體中的events均可以取哪些值呢,以下:

對比select函數以下:

下面用poll函數來改裝咱們的代碼,主要是改裝服務端,來忽然FD_SETSIZE大小的限制,下面會一點點對比原來的作法進行改裝:

修改以下:

替換以下:

下面當事件產生了,則須要判斷一下是哪些文件描述符產生了事件,那poll方式是如何判斷的呢?具體以下:

替換以下:

而後接受客戶端的鏈接,並加保存起來,修改以下:

替換以下:

接下來,處理已鏈接套接口事件,具體修改以下:

替換以下:

接下來編譯運行一下:

須要包含頭文件,以下:

可見改用poll函數以後的程序運行一切正常,下面再來測試併發數:

下面就來調整描述符最大個數爲2048,注意:ulimit調整的是當前進程,因此要想服務端與客戶端都突破1024這個限制,都須要切至root用戶進行更改,切記切記~

這時再運行看下這時的鏈接數是否能突破1024呢?

因此,這就是poll針對select的改進,下面貼出服務端,客戶端的代碼:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <poll.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)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

void handle_sigchld(int sig)
{
/*    wait(NULL);*/
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}

int main(void)
{
    int count = 0;
    signal(SIGPIPE, handle_sigpipe);
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        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 = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen;    
    int conn;

    int i;
    struct pollfd client[2048];
    int maxi = 0;

    for (i=0; i<2048; i++)
        client[i].fd = -1;

    int nready;
    client[0].fd = listenfd;//第一個事件是監聽套接字
    client[0].events = POLLIN;//表明可讀事件

    while (1)
    {
        nready = poll(client, maxi+1, -1);
        if (nready == -1)
        {
            if (errno == EINTR)
                continue;
            
            ERR_EXIT("select");
        }
        if (nready == 0)
            continue;

        if (client[0].revents & POLLIN)
        {
            peerlen = sizeof(peeraddr);
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
            if (conn == -1)
                ERR_EXIT("accept");

            for (i=0; 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("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
            printf("count = %d\n", ++count);    
            
            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].events & POLLIN)
            {
                char recvbuf[1024] = {0};
                int ret = readline(conn, recvbuf, 1024);
                if (ret == -1)
                        ERR_EXIT("readline");
                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;
}
複製代碼

contest.c:

#include <unistd.h>
#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;//用來記數,當客戶端與服務端鏈接成功則加1
    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;
}
複製代碼
相關文章
相關標籤/搜索