UNIX網絡編程——使用select函數編寫客戶端和服務器

     首先看原先《UNIX網絡編程——併發服務器(TCP)》的代碼,服務器代碼serv.c:編程

#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>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

void do_service(int);

int main(void)
{
    signal(SIGCHLD, 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)

    pid_t pid;

    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列
        {
		    if( errno == EINTR )            ///////////////////////////////////////////////////////////////////必須處理被中斷的系統調用
			   continue;
			else
			   ERR_EXIT("accept error");
		}	   
        printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
               ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork error");
        if (pid == 0)
        {
            // 子進程
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn); //父進程
    }

    return 0;
}

void do_service(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)   //客戶端關閉了
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read error");
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
}

客戶端代碼cli.c:

#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>


#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)




int main(void)
{
    int sock;
    if ((sock = 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 = inet_addr("127.0.0.1");
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */

    if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect error");
    struct sockaddr_in localaddr;
	char cli_ip[20];
	socklen_t local_len = sizeof(localaddr);
	memset(&localaddr, 0, sizeof(localaddr));
	if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 )
	    ERR_EXIT("getsockname error");
	inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip));
	printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port)); 

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {

        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));


        fputs(recvbuf, stdout);

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


    close(sock);


    return 0;
}
 先運行服務器端,再運行客戶端:

huangcheng@ubuntu:~$ ./serv
huangcheng@ubuntu:~$ ./cli


先查看一下網絡狀態:

huangcheng@ubuntu:~$ netstat -anp | grep 5188
(並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶)
tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2750/serv
tcp        0      0 127.0.0.1:49484         127.0.0.1:5188          ESTABLISHED 2751/cli
tcp        0      0 127.0.0.1:5188          127.0.0.1:49484         ESTABLISHED 2752/serv

能夠看出創建了鏈接,服務器端有兩個進程,一個父進程處於監聽狀態,另外一子進程正在對客戶端進行服務。ubuntu


服務器端的子進程的pid爲2752,並kill掉它:數組

huangcheng@ubuntu:~$ kill -9 2752

再查看一下網絡狀態:
huangcheng@ubuntu:~$ netstat -anp | grep 5188
(並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶)
tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2750/serv
tcp        1      0 127.0.0.1:49484         127.0.0.1:5188          CLOSE_WAIT  2751/cli
tcp        0      0 127.0.0.1:5188          127.0.0.1:49484         FIN_WAIT2   -

     來分析一下,咱們將server子進程  kill掉,則其終止時,socket描述符會自動關閉併發FIN段給client,client收到FIN後處於CLOSE_WAIT狀態,可是client並無終止,也沒有關閉socket描述符,所以不會發FIN給 server子進程,所以server 子進程的TCP鏈接處於FIN_WAIT2狀態。服務器

爲何會出現這種狀況呢,來看client的部分程序:網絡

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {

        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));


        fputs(recvbuf, stdout);

        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }
     客戶端程序阻塞在了fgets 那裏,即從標準輸入讀取數據,因此不能執行到下面的read,也即不能返回0,不會退出循環,不會調用close關閉sock,因此出現上述的狀況,即狀態停滯,不能向前推動。
     出現上述問題的根本緣由在於客戶端程序不能併發處理從標準輸入讀取數據和從套接字讀取數據兩個事件,咱們可使用前面講過的select函數 來完善客戶端程序。


int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

參數1:讀寫異常集合中的文件描述符的最大值加1;併發

參數2:讀集合,關心可讀事件;socket

套接口緩衝區有數據可讀
鏈接的讀一半關閉,即接收到FIN段,讀操做將返回0
若是是監聽套接口,已完成鏈接隊列不爲空時。
套接口上發生了一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取。

參數3:寫集合,關心可寫事件;tcp

套接口發送緩衝區有空間容納數據。函數

鏈接的寫一半關閉。即收到RST段以後,再次調用write操做。學習

套接口上發生了一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取。

參數4:異常集合,關心異常事件;

套接口存在帶外數據(TCP頭部 URG標誌,16位緊急指針字段)

參數5:超時時間結構體


對於參數2,3,4來講,若是不關心對應事件則設置爲NULL便可。注意5個參數都是輸入輸出參數,即select返回時可能對其進行了修改,好比集合被修改以便標記哪些套接口發生了事件,時間結構體的傳出參數是剩餘的時間,若是設置爲NULL表示永不超時。用select管理多個I/O,select阻塞等待,一旦其中的一個或多個I/O檢測到咱們所感興趣的事件,select函數返回,返回值爲檢測到的事件個數,而且返回哪些I/O發送了事件,遍歷這些事件,進而處理事件。注意當select阻塞返回後,此時調用read/write 是不會阻塞的,由於正是有可讀可寫事件發生才致使select 返回,也能夠認爲是select 提早阻塞了。

下面是4個能夠對集合進行操做的宏:

void FD_CLR(int fd, fd_set *set); // 清除出集合
int  FD_ISSET(int fd, fd_set *set); // 判斷是否在集合中
void FD_SET(int fd, fd_set *set); // 添加進集合中
void FD_ZERO(fd_set *set); // 將集合清零

下面是經過select函數改進的客戶端的代碼cli.c:

#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>


#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)




int main(void)
{
    int sock;
    if ((sock = 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 = inet_addr("127.0.0.1");
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */

    if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect error");
    struct sockaddr_in localaddr;
	char cli_ip[20];
	socklen_t local_len = sizeof(localaddr);
	memset(&localaddr, 0, sizeof(localaddr));
	if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 )
	    ERR_EXIT("getsockname error");
	inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip));
	printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port)); 

	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); //select返回表示檢測到可讀事件
        if (nready == -1)
            ERR_EXIT("select error");

        if (nready == 0)
            continue;

        if (FD_ISSET(sock, &rset))
        {

            int ret = read(sock, recvbuf, sizeof(recvbuf)); 
            if (ret == -1)
                ERR_EXIT("read error");
            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;

            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
    }

    close(sock);
    return 0;
}
     即將兩個事件都添加進可讀事件集合,在while循環中,若是select返回說明有事件發生,依次判斷是哪些事件發生,若是是標準輸入有數據可讀,則讀取後再次回到循環開頭select阻塞等待事件發生,若是是套接口有數據可讀,且返回爲0則說明對方已經關閉鏈接,退出循環並調用close關閉sock。

重複前面的操做:

(1)先運行服務器,再運行客戶端

huangcheng@ubuntu:~$ ./serv
huangcheng@ubuntu:~$ ./cli
(2)查看網絡狀態:

huangcheng@ubuntu:~$ netstat -anp | grep 5188
(並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶)
tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2960/serv
tcp        0      0 127.0.0.1:49485         127.0.0.1:5188          ESTABLISHED 2963/cli
tcp        0      0 127.0.0.1:5188          127.0.0.1:49485         ESTABLISHED 2964/serv
(3)kill掉服務器的子進程,再查看網絡狀態:

huangcheng@ubuntu:~$ kill -9 2964
huangcheng@ubuntu:~$ netstat -anp | grep 5188
(並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶)
tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2960/serv
tcp        0      0 127.0.0.1:5188          127.0.0.1:49485         TIME_WAIT   -

     即 client 關閉socket描述符,server 子進程的TCP鏈接收到client發的FIN段後處於TIME_WAIT狀態,此時會再發生一個ACK段給client,client接收到以後就處於CLOSED狀態,這個狀態存在時間很短,因此看不到客戶端的輸出條目,TCP協議規定,主動關閉鏈接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximumsegment lifetime)的時間後才能回到CLOSED狀態,須要有MSL 時間的主要緣由是在這段時間內若是最後一個ack段沒有發送給對方,則能夠從新發送。

     過一小會再次查看網絡狀態:

huangcheng@ubuntu:~$ netstat -anp | grep 5188
(並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶)
tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2960/serv
     能夠發現只剩下服務器端父進程的監聽狀態了,由TIME_WAIT狀態轉入CLOSED狀態,也很快會消失。


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


     前面咱們實現的可以併發服務的服務器端程序是使用fork出多個子進程來實現的,如今學習了select函數,能夠用它來改進服務器端程序,實現單進程併發服務。先看以下程序,再來解釋:

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

    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");
            
            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("readline 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)的限制,這個須要從新編譯內核                                                                          
 */

     程序第一次進入while 循環,只把監聽套接字加入關心的事件,select返回說明監聽套接字有可讀事件,即已完成鏈接隊列不爲空,這時調用accept不會阻塞,返回一個已鏈接套接字,將這個套接字加入allset,由於第一次運行則nready = 1,直接continue跳回到while 循環開頭,再次調用select,此次會關心監聽套接字和一個已鏈接套接字的可讀事件,若是繼續有客戶端鏈接上來則繼續將其加入allset,此次nready = 2,繼續執行下面的for 循環,而後對客戶端進行服務。服務完畢再次回到while 開頭調用select 阻塞時,就關心一個監聽套接字和2個已鏈接套接字的可讀事件了,一直循環下去。

     程序大概邏輯就這樣,一些細節就你們本身想一想了,好比client數組是用來保存已鏈接套接字的,爲了不每次都得遍歷到FD_SETSIZE-1,保存一個最大不空閒下標maxi,每次遍歷到maxi就能夠了。每次獲得一個conn,要判斷一下conn與maxfd的大小。

當得知某個客戶端關閉,則須要將conn在allset中清除掉。之因此要有allset 和 rset 兩個變量是由於rset是傳入傳出參數,在select返回時rset可能被改變,故須要每次在回到while 循環開頭時須要將allset 從新賦予rset 。


相關文章
相關標籤/搜索