1. 程式人生 > >Socket學習四

Socket學習四

Socket 的五種I/O模型

  • 阻塞I/O、非阻塞I/O、I/O複用、訊號驅動I/O、非同步I/O
  • 現在用的最多的是I/O複用和非同步I/O
  • 阻塞I/O
  • 在這之前我們所用的套介面I/O模型都是阻塞I/O的方式來進行通訊的。一旦套介面接收完成後,我們就可以接收資料。此時像系統提交一個recv請求接收資料,即阻塞在這裡,直到對等方傳送資料填充了recv的接收緩衝區,阻塞解除。然後這些資料就會從緩衝區被複制到使用者空間中即 buf中,此時recv返回,這是侯我們就可以對返回得到的資料進行處理了。
  • 非阻塞I/O
  • 將套介面設定為非阻塞可以用 fcntl(fd, F_SETFD, flag|O_NONBLOCK),此時即使recv沒有收到資料也不會阻塞,它會返回一個錯誤,返回值為 -1,但是需要不斷地判斷是否有資料到來,這種等待我們稱之為忙等待。後面的處理和阻塞的處理是一樣的。這種最不推薦使用,極大的浪費了CPU資源
  • 這兩種模式走了兩種極端,前者死等,後者直接不等,但是在不斷地輪詢等待著資料。
  • 那麼有沒有這麼一種機制,集中管理檔案描述符。一旦檔案描述符的狀態發生變化,就是某一個socket的資料到來的時候,這個機制會告訴我們它的狀態發生變化了,然後才取讀資料。這樣就不用輪詢或者阻塞了。
  • 答案是必定的。I/O複用就是這樣的機制。
  • 這種模型主要是通過 select 函式來實現的。
  • 思想:用 select 函式管理多個描述符。每當其中的一個檔案描述符的狀態發生變化時,即資料到來時,select就返回,這時候再呼叫rev函式時就不會阻塞了,就可以把資料從核心空間複製到使用者空間。其實時將阻塞提前到了select函式這裡。
  • 訊號驅動I/O
  • 非同步I/O
  • 非同步 I/O, 呼叫 aio_read 函式 ,並提交一個緩衝區buf,即使核心中沒有資料到來,這個函式也會立刻返回,一旦返回,應用程序就可以處理其他的事情。當有資料到來時,核心會自動地將資料複製到使用者空間,複製完成後,會通過訊號來通知應用程序的程式來處理資料。但是這也需要一定的機制來來通知上層應用,比如 aio_read 中指定的訊號 SIGIO 也可能是其他的機制,這取決與 aio_read 的內部實現。因為不同的 aio_read 實現的方式可能不太一樣,而且非同步 I/O在大部分系統中或多或少都有一定的問題,所以非同步 I/O也沒有得到很好的推廣。
  • 與訊號驅動的區別時:前者時拉訊號,後者是核心主動將訊號複製給使用者
  • select模型:
  • 函式原型:int select(int n, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout);
  • 返回值:失敗返回-1,成功返回檔案描述符的個數,超時沒檢測到檔案描述符返回0
  • 引數解析:
  • fd:讀、寫、異常集合描述符的最大值+1

    • readfds 讀集合:監視readfds來檢視是否read的時候會被堵塞,注意,即便到了end-of-file,fd也是可讀的。
    • writefds 寫集合:監視writefds看寫的時候會不會被堵塞。
    • 監視exceptfd是否出現了異常。主要用來讀取OOB資料,異常並不是指出錯。
    • 注意當一個套接口出錯時,它會變得既可讀又可寫。
      如果有了狀態改變,會將其他fd清零,只有那些發生改變了的fd保持置位,以用來指示set中的哪一個改變了狀態。引數n是所有set裡所有fd裡,具有最大值的那個fd的值加1
    • fd_set 異常集合
      四個巨集用來對fd_set進行操作:
      FD_CLR(int fd, fd_set set); //將檔案描述從集合中移除
      FD_ISSET(int fd, fd_set 
      set); //判斷檔案描述符是否在集合中
      FD_SET(int fd, fd_set set); //將檔案描述符新增到集合中
      FD_ZERO(fd_set 
      set); //清空集合
    • time_out 超時結構體

      • timeout是從呼叫開始到select返回前,會經歷的最大等待時間。
      • 兩種特殊情況:如果為值為0,會立刻返回。
      • 如果timeout是NULL,會阻塞式等待。
      • struct timeval {
        long tv_sec; / seconds /
        long tv_usec; / microseconds /
        };

      • 一些呼叫使用3個空的set, n為zero, 一個非空的timeout來達到較為精確的sleep.

      • Linux中, select函式改變了timeout值,用來指示還剩下的時間,但很多實現並不改timeout。
      • 為了較好的可移植性,timeout在迴圈中需要被重新賦初值。

      • timeout== NULL

      • 無限等待
      • 被訊號打斷時返回1, errno 設定成 EINTR
      • timeout->tv_sec == 0 && tvptr->tv_usec == 0
      • 不等待立即返回
      • timeout->tv_sec != 0 || tvptr->tv_usec != 0
      • 等待特定時間長度, 超時返回0
    • 通過前面的程式碼我們可以知道,客戶端斷開會阻塞在TIME_WAIT_2,無法繼續向下推進到 TIME_WAIT 狀態???在下面我們用select模型來解決這個問題。
    • 思想:
      • select 作為管理者
      • 用 select 管理多個I/O;一旦其中的一個或者多個I/O檢測到我們所感興趣的事件,select 函式返回,返回值為檢測到的事件個數。並且返回了那些I/O。
      • 遍歷這些事件,進而處理這些事件
客戶端
伺服器程式碼見前面socket學習三

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include <sys/select.h>
#include <sys/time.h>

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>


ssize_t readn(int fd,void *buf,size_t count)
{
//ssize_t=int,size_t=unsigned int
//接收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;//讀到EOF,對方關閉
        bufp+=nread;
        nleft-=nread;
    }
    return count;
}

ssize_t writen(int fd,void *buf,size_t count)
{
    size_t nleft=count;//剩餘傳送位元組數
    ssize_t nwritten;//已經發送位元組數
    char *bufp=(char *)buf;
    while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
    {
        if((nwritten=write(fd,bufp,nleft))<0)
        {
            if(errno==EINTR)//被中斷
                continue;
            return -1;
        }
        else if(nwritten==0)//對等方關閉
            continue;//讀到EOF,對方關閉
        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)//操作被訊號中斷,recv認為連結正常,繼續偷窺
            continue;
        return ret;//返回讀取的位元組數
    }

}
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
    int ret;
    int nread;
    char *bufp=buf;
    int nleft=maxline;//讀取遇到\n返回,不會超過maxline
    while(1)
    {
        ret=recv_peek(sockfd,bufp,nleft);
        if(ret<=0)
            return ret;
        nread=ret;
        //接下來判斷接收的緩衝區是否有\n
        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)   //偷窺到的資料不能大於maxline
        {
            exit(EXIT_FAILURE);
        }
        nleft-=nread;//剩餘位元組數
        ret=readn(sockfd,bufp,nread);//將nread資料從緩衝區移除
        if(ret!=nread)
            exit(EXIT_FAILURE);
        bufp+=nread;//繼續偷窺
    }
    return 1;                       
}

void echo_client(int sock)
{
/*  char        sendbuf[1024] = {0};
    char        recvbuf[1024] = {0};
    while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        //writen(sock,&sendbuf,strlen(sendbuf));//將接收到的資料傳送出去
        //為檢驗 SIGPIPE 訊號
        writen(sock, sendbuf, 1);
        writen(sock,sendbuf+1,strlen(sendbuf)-1);

        int ret=readline(sock,recvbuf,sizeof(recvbuf));//函式從開啟的檔案,裝置中讀取資料
        if(ret==-1)
        {
            printf("readline err");
        }
        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);//為什麼這裡不用STD_FILENO,因為這個巨集的值為0;但是我們不能保證這個標準輸入是否被重定向,導致這個檔案描述符可能部位0
    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)
            printf("select err\n");

        if(nready == 0)
            continue;
        if(FD_ISSET(sock, &rset)) //判斷檢測到sock(可讀)事件是否在集合中
        {   
            int ret=readline(sock,recvbuf,sizeof(recvbuf));//函式從開啟的檔案,裝置中讀取資料
            if(ret==-1)
            {
                printf("readline err");
            }
            else if(ret==0)
            { 
                printf("server is closed\n");
                break;
            }

            fputs(recvbuf,stdout);//傳送資料到檔案
            memset(recvbuf,0,sizeof(recvbuf));
        }
        if(FD_ISSET(fd_stdin, &rset))  //判斷檢測到fd_ISSET(寫入)事件是否在集合中
        {
           if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
           {
               writen(sock,&sendbuf,strlen(sendbuf));
               memset(sendbuf, 0, sizeof(sendbuf));
           }
           else
               break;

        }
    }
    close(sock);
}

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

int main()
{
    //signal(SIGPIPE, handle_sigpipe);
    signal(SIGPIPE, SIG_IGN);
    int sock;
    /*if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
    if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
        printf("socket err\n");

    //IPV4地址結構
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;//地址家族
    servaddr.sin_port=htons(8001);//埠,主機轉網路
    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)
        printf("connect err\n");
    //獲取本地埠號、IP
    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
        printf("getsockname err\n");
    printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));

    echo_client(sock);

    return 0;

}
  • 執行伺服器與客戶端,然後將斷掉伺服器與客戶端的連線,結果如下:
  • 可以看到,客戶端直接進入到了TIME_WAIT 狀態,並沒有阻塞在TIME_WAIT_2狀態。
  • 讀、寫、異常事件發生的條件

    • 可讀:
      • 套介面緩衝區有資料可讀(對等方傳送資料過來,填充了本地套介面緩衝區,導致了套介面緩衝區有資料可讀)
      • 連線的讀一半關閉,即接收到FIN段,讀操作將返回0 (這個時候也能通知select表示某個套介面產生了可讀事件,這時候執行讀操作,返回值為 0,表示對等方關閉)
      • 如果是監聽套介面,已完成連線佇列不為空時。(就是說對等方在connect連線完成時,則已完成連線對列就不為空了,監聽套介面就會產生可讀事件通知select檢測到)
      • 套介面上發生了一個錯誤待處理(同樣會產生可讀事件,通知select),錯誤可以通過getsockopt 指定SO_ERROR選項來獲取。
  • 可寫:

    • 套介面傳送緩衝區有空間容納資料(在緩衝區寫入產生可寫事件,在緩衝區沒有滿的時候,會頻繁的產生可寫事件)
    • 連線的寫一半關閉。及收到的RST段之後,再次呼叫write操作。
    • 套介面上發生了一個錯誤待處理,錯誤可以通過getsockopt 指定SO_ERROR選項來獲取。
  • 異常

    • 套介面存在帶外資料。
  • 用select改進伺服器

/*********************************************************************************
 *      Copyright:  (C) 2018 anzhihong<[email protected]>
 *                  All rights reserved.
 *
 *       Filename:  socket_server.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2018年08月05日)
 *         Author:  anzhihong <[email protected]>
 *      ChangeLog:  1, Release initial version on "2018年08月05日 08時23分10秒"
 *                 
 ********************************************************************************/

#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>

#include<stdio.h>
#include<stdlib.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)//read是可中斷的,所以當被外來訊號中斷打斷時,不算錯誤,任然繼續執行
                continue;
            return -1;
        }
        else if(nread==0)//對等方關閉
            return count-nleft;//讀到EOF,對方關閉
        bufp  += nread;
        nleft -= nread;
     }
    return count;
}

ssize_t writen(int fd,void *buf,size_t count)
{
    size_t nleft=count;//剩餘傳送位元組數
    ssize_t nwritten;//已經發送位元組數
    char *bufp=(char *)buf;

     while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
     {
        if((nwritten=write(fd,bufp,nleft))<0)
        {
            if(errno==EINTR)//被中斷
                continue;
            return -1;
        }
        else if(nwritten==0)//對等方關閉
            continue;//讀到EOF,對方關閉
        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)//操作被訊號中斷,recv認為連結正常,繼續執行。
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd,void *buf,size_t maxline)
{
    int ret = 0;
    int nread;
    char *bufp = buf;
    int nleft = maxline; //讀取遇到\n返回,不會超過maxline

    while(1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        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)    //偷窺到的資料不能大於maxline
            exit(EXIT_FAILURE);
        nleft -= nread; //剩餘位元組數
        ret = readn(sockfd, bufp, nread); //將已經讀到資料nread從緩衝區移除
        if(ret != nread)
            exit(EXIT_FAILURE);
        bufp += nread;   //繼續偷窺
    }
    return 1;
}

void echo_service(int conn)
{
    char recvbuf[1024];

    while(1)
    {
        memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
        int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
        if(ret == -1)
            ERR_EXIT("readline");
        if(ret == 0)
        {
            printf("client close");
            break;
        }

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

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

int main(void)
{
    //處理殭屍程序
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    /* if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)   //第一次開啟 listenfd
        ERR_EXIT("socket err");

     //IPV4地址結構
     struct sockaddr_in servaddr;
     memset(&servaddr,0,sizeof(servaddr));
     servaddr.sin_family=AF_INET;//地址家族
     servaddr.sin_port=htons(8001);//埠,主機轉網路
     /*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
     inet_aton("127.0.0.1",&servaddr.sin_addr);*/
     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 err");

    //繫結埠號,ip地址
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //第二次開啟 listenfd
        ERR_EXIT("bind err");

    //監聽
    if(listen(listenfd,SOMAXCONN)<0)   //第三次開啟 listenfd
        ERR_EXIT("bind err");

    struct sockaddr_in peeraddr;
   // socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
    socklen_t peerlen;
    int conn;//已連線套接字
/*     pid_t pid;
     while(1)
     {
        if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
            ERR_EXIT("accept err");

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

        pid = fork();
        if(pid==-1)
            ERR_EXIT("fork err");
        if(pid==0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);//將子程序退出,要不它還會fork()
        }

        else
            close(conn);
     }
*/
    int nready; //select返回的監測個數
    int maxfd = listenfd;   //所以這裡的 listenfd 的值為3
    fd_set allset; //建立集合
    fd_set rset;
    FD_ZERO(&allset);  //清除集合
    FD_ZERO(&rset);
    FD_SET(listenfd, &allset); //將接聽套介面放入 allset 中
    /*  當有新的客戶端連線過來時,conn 會被新的客戶端覆蓋,所以定義一個數組來存放客戶端的資訊,為什麼使用 fork()時 conn 沒有被覆蓋呢?這是因為每個 fork() 中的 conn 時獨立的,而我們現在採用的是單程序來實現,一個單程序只有一個 conn,所以需要一個套介面陣列集合來儲存客戶端 conn 的連線資訊 */
    int client[FD_SETSIZE]; //select最多可以處理這麼多的描述符
    int i;
    for(i=0;i<FD_SETSIZE;i++)
    {
        client[i] = -1;   //表示空閒
    }


    while(1)
    {
        /*在程式執行以後,allset 當中會有兩個套介面,一個是 listenfd, 一個是 conn。此時就會有多種事件發生。一種時listenfd套介面產生的事件,一種是conn套介面產生的事件,另一種是兩種套介面產生的事件。這裡需要allset的原因是這裡rset的套介面陣列集的的內容會變化,它只會儲存套介面數族當前的內容,在下一次select時,只會監聽當前的套介面,而不會監聽所有的套介面,所以需要allset來臨時儲存rset以前的內容*/
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if(nready == -1)  //如果 nready 等於-1,監聽失敗
        {
            if(errno == EINTR)  //還有一種情況就是被中斷訊號打斷,可能導致 select不能重啟
            continue;  //繼續監聽
            ERR_EXIT("select");
        }
        if(nready == 0)    //如果超時,繼續執行
            continue;
        /* 監聽套介面發生可讀事件意味著對方連線三次握手已經成功,我們這邊已完成連線佇列中的條目不為空了,那麼此時再呼叫 accept 函式將不會再阻塞*/
        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小於0
                {
                    client[i] = conn;
                    break;
                }
        }
            if(i == FD_SETSIZE)   //如果i等於select設定的長度,表明client的連線數量超出了我們的連線範圍
             {
                  fprintf(stderr, "too many clients\n");
                  exit(EXIT_FAILURE);
             }
            printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
            /* 現在我們已經得到了一個已經連線的套介面conn,下一次我們仍然需要關心 conn 的可讀事件的發生,也就是說下一次呼叫select時我們也要把它放入 select 中處理 */
            FD_SET(conn, &allset);
            /* 因為不斷地往陣列集合中新增套介面,所以maxfd可能不是恆大於以前的maxfd,所以要更新maxfd的值 */
            if(conn > maxfd)
                maxfd = conn;
            /* 因為程式的不斷執行,導致新增到陣列中的套介面越來越多,從而導致select的返回值可能大於1,所以我們需要一個一個的處理 */
            //首先時conn產生的可讀事件
            if(--nready <= 0)    //證明這裡檢測到的事件已經處理完畢,然後繼續監聽
                continue;
        }
        //處理已連線套介面conn的事件
        for(i=0; i<FD_SETSIZE; i++)
        {
            conn = client[i];
            if(conn == -1)  //表示空閒的位置,繼續執行
                continue;
            if(FD_ISSET(conn, &rset))  //檢測讀集合中是否有conn
            { 
                char recvbuf[1024] = {0};
                memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
                int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
                if(ret == -1)
                ERR_EXIT("readline");
                if(ret == 0)
                    {
                        printf("client close\n");
                        FD_CLR(conn, &allset); //當客戶端關閉,那麼就不需要再關心套介面conn產生的事件了
                        printf("conn already delete\n");
                    }

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

                if(--nready <= 0) //這裡意味著所有的事件都已經處理完了,就跳出迴圈。
                    break;
            }
        }

    }
     return 0;

}

  • 如圖所示集合總的容量是 FD_SETSIZE ,下標為 0 的表示沒有將其放入集合當中,也就說檔案描述符0,1,2都沒有放入。因為我們的監聽套介面的檔案描述符為3,所以將其放入集合中。接下來如果套介面產生了可讀事件,accept返回一個新的套介面,此時套介面的文字描述符為4,然後將新的套介面加入空閒的陣列中。如第一個,client[0],文字描述符為5的套介面放入client[1]中,以此類推。當套介面為4的關閉client[0]的值又要重新置為1,並且將其從集合中的值置為0,即去除套介面為4的那個。但是這個時候不會更新maxfd的值,意味著select函式要從0開始遍歷所有的套介面(這裡是檢測到所有的可讀事件才返回,並不是一檢測到可讀事件就返回),然後返回發生事件的個數。一旦產生了可讀事件,要遍歷所有已連線的套介面,其範圍是0~FD_SETSIZE,那麼我們能不能縮小一下範圍呢?我們可以記錄一個最大的不空閒的位置 maxi。
  • 程式碼
/*********************************************************************************
 *      Copyright:  (C) 2018 anzhihong<[email protected]>
 *                  All rights reserved.
 *
 *       Filename:  socket_server.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2018年08月05日)
 *         Author:  anzhihong <[email protected]>
 *      ChangeLog:  1, Release initial version on "2018年08月05日 08時23分10秒"
 *                 
 ********************************************************************************/

#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>

#include<stdio.h>
#include<stdlib.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)//read是可中斷的,所以當被外來訊號中斷打斷時,不算錯誤,任然繼續執行
                continue;
            return -1;
        }
        else if(nread==0)//對等方關閉
            return count-nleft;//讀到EOF,對方關閉
        bufp  += nread;
        nleft -= nread;
     }
    return count;
}

ssize_t writen(int fd,void *buf,size_t count)
{
    size_t nleft=count;//剩餘傳送位元組數
    ssize_t nwritten;//已經發送位元組數
    char *bufp=(char *)buf;

     while(nleft>0)//一般而言,write緩衝區大於傳送資料緩衝區,不阻塞
     {
        if((nwritten=write(fd,bufp,nleft))<0)
        {
            if(errno==EINTR)//被中斷
                continue;
            return -1;
        }
        else if(nwritten==0)//對等方關閉
            continue;//讀到EOF,對方關閉
        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)//操作被訊號中斷,recv認為連結正常,繼續執行。
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd,void *buf,size_t maxline)
{
    int ret = 0;
    int nread;
    char *bufp = buf;
    int nleft = maxline; //讀取遇到\n返回,不會超過maxline

    while(1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        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)    //偷窺到的資料不能大於maxline
            exit(EXIT_FAILURE);
        nleft -= nread; //剩餘位元組數
        ret = readn(sockfd, bufp, nread); //將已經讀到資料nread從緩衝區移除
        if(ret != nread)
            exit(EXIT_FAILURE);
        bufp += nread;   //繼續偷窺
    }
    return 1;
}

void echo_service(int conn)
{
    char recvbuf[1024];

    while(1)
    {
        memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
        int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
        if(ret == -1)
            ERR_EXIT("readline");
        if(ret == 0)
        {
            printf("client close");
            break;
        }

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

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

int main(void)
{
    //處理殭屍程序
    signal(SIGCHLD, handle_sigchld);
    int listenfd;
    /* if((listenfd=socket(AF_INET,SOCK_STEAM,IPPOTO_TCP))<0) */
    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)   //第一次開啟 listenfd
        ERR_EXIT("socket err");

     //IPV4地址結構
     struct sockaddr_in servaddr;
     memset(&servaddr,0,sizeof(servaddr));
     servaddr.sin_family=AF_INET;//地址家族
     servaddr.sin_port=htons(8001);//埠,主機轉網路
     /*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
     inet_aton("127.0.0.1",&servaddr.sin_addr);*/
     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 err");

    //繫結埠號,ip地址
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //第二次開啟 listenfd
        ERR_EXIT("bind err");

    //監聽
    if(listen(listenfd,SOMAXCONN)<0)   //第三次開啟 listenfd
        ERR_EXIT("bind err");

    struct sockaddr_in peeraddr;
   // socklen_t peerlen =sizeof(peeraddr);//typedef int socklen_t
    socklen_t peerlen;
    int conn;//已連線套接字
/*     pid_t pid;
     while(1)
     {
        if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
            ERR_EXIT("accept err");

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

        pid = fork();
        if(pid==-1)
            ERR_EXIT("fork err");
        if(pid==0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);//將子程序退出,要不它還會fork()
        }

        else
            close(conn);
     }
*/
    int nready; //select返回的監測個數
    int maxfd = listenfd;   //所以這裡的 listenfd 的值為3
    fd_set allset; //建立集合
    fd_set rset;
    FD_ZERO(&allset);  //清除集合
    FD_ZERO(&rset);
    FD_SET(listenfd, &allset); //將接聽套介面放入 allset 中
    /*  當有新的客戶端連線過來時,conn 會被新的客戶端覆蓋,所以定義一個數組來存放客戶端的資訊,為什麼使用 fork()時 conn 沒有被覆蓋呢?這是因為每個 fork() 中的 conn 時獨立的,而我們現在採用的是單程序來實現,一個單程序只有一個 conn,所以需要一個套介面陣列集合來儲存客戶端 conn 的連線資訊 */
    int client[FD_SETSIZE]; //select最多可以處理這麼多的描述符
    int maxi = 0;   //新增一個遍歷範圍,初始值為0
    int i;
    for(i=0;i<FD_SETSIZE;i++)
    {
        client[i] = -1;   //表示空閒
    }

   // int count = 0;  //測試伺服器接受客戶端連線數量
    while(1)
    {
        /*在程式執行以後,allset 當中會有兩個套介面,一個是 listenfd, 一個是 conn。此時就會有多種事件發生。一種時listenfd套介面產生的事件,一種是conn套介面產生的事件,另一種是兩種套介面產生的事件。這裡需要allset的原因是這裡rset的套介面陣列集的的內容會變化,它只會儲存套介面數族當前的內容,在下一次select時,只會監聽當前的套介面,而不會監聽所有的套介面,所以需要allset來臨時儲存rset以前的內容*/
        rset = allset;
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if(nready == -1)  //如果 nready 等於-1,監聽失敗
        {
            if(errno == EINTR)  //還有一種情況就是被中斷訊號打斷,可能導致 select不能重啟
            continue;  //繼續監聽
            ERR_EXIT("select");
        }
        if(nready == 0)    //如果超時,繼續執行
            continue;
        /* 監聽套介面發生可讀事件意味著對方連線三次握手已經成功,我們這邊已完成連線佇列中的條目不為空了,那麼此時再呼叫 accept 函式將不會再阻塞*/
        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小於0
                {
                    client[i] = conn;
                    if(i > maxi)
                    {
                        maxi = i; //最大不空閒的位置發生了改變
                        printf("maxi is:%d\n", maxi);
                    }
                    break;
                }
        }

            if(i == FD_SETSIZE)   //如果i等於select設定的長度,表明client的連線數量超出了我們的連線範圍
            {
                 fprintf(stderr, "too many clients\n");
                 exit(EXIT_FAILURE);
            }
            printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
            /* 現在我們已經得到了一個已經連線的套介面conn,下一次我們仍然需要關心 conn 的可讀事件的發生,也就是說下一次呼叫select時我們也要把它放入 select 中處理 */
            FD_SET(conn, &allset);
            /* 因為不斷地往陣列集合中新增套介面,所以maxfd可能不是恆大於以前的maxfd,所以要更新maxfd的值 */
            if(conn > maxfd)
                maxfd = conn;
            /* 因為程式的不斷執行,導致新增到陣列中的套介面越來越多,從而導致select的返回值可能大於1,所以我們需要一個一個的處理 */
            //首先時conn產生的可讀事件
            if(--nready <= 0)    //證明這裡檢測到的事件已經處理完畢,然後繼續監聽
                continue;
        }
        //處理已連線套介面conn的事件
        //for(i=0; i<FD_SETSIZE; i++) 
        for(i=0; i<=maxi; i++)   //遍歷的範圍從FD_SETSIZE縮小到maxi
        {
            conn = client[i];
            if(conn == -1)  //表示空閒的位置,繼續執行
                continue;
            if(FD_ISSET(conn, &rset))  //檢測讀集合中是否有conn
            { 
                char recvbuf[1024] = {0};
                memset(recvbuf, 0, sizeof(recvbuf)); //初始化recvbuf
                int ret=readline(conn,recvbuf,1024);//一行一行接收資料。
                if(ret == -1)
                ERR_EXIT("readline\n");
                if(ret == 0)
                    {
                        printf("client close\n");
                        FD_CLR(conn, &allset); //當客戶端關閉,那麼就不需要再關心套介面conn產生的事件了
                        printf("conn already delete\n");
                        client[i] = -1;   //重新將client[i]置為空閒。
            close(conn);
                    }
            //printf("count is %d\n", ++count);
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));

                if(--nready <= 0) //這裡意味著所有的事件都已經處理完了,就跳出迴圈。
                    break;
            }
        }

    }
     return 0;

}
  • 注意這裡的套介面不只受FD_SETSIZE的限制,還受到一個程序中能夠開啟的I/O的數目的限制。I/O的數目是可以更改的。通過命令ulimit -n來更改程序數(臨時的)。FD_SETSIZE需要更改核心才行。
程式碼更改
/*********************************************************************************
 *      Copyright:  (C) 2018 anzhihong<[email protected]>
 *                  All rights reserved.
 *
 *       Filename:  socket_test1024.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2018年08月05日)
 *         Author:  anzhihong <[email protected]>
 *      ChangeLog:  1, Release initial version on "2018年08月05日 08時23分10秒"
 *                 
 ********************************************************************************/

#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/resource.h>


#include<stdio.h>
#include<stdlib.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;

}
  • 但是這個只能更改當前程序的最大限制,並不能更改父程序的。
  • select主要限制是其套介面數量的限制。
  • 測試程式碼

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include <sys/select.h>
#include <sys/time.h>

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

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


int main()
{
    int count = 0;
    while(1)
    {
        int sock;
        if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
    {
        sleep(4);
            ERR_EXIT("socket");
    }

        //IPV4地址結構
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;//地址家族
        servaddr.sin_port=htons(8001);//埠,主機轉網路
        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");
        //獲取本地埠號、IP
        struct sockaddr_in localaddr;
        socklen_t addrlen = sizeof(localaddr);
        if(getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
            printf("getsockname err\n");
        printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
        printf("count = %d\n", ++count);
    }

        return 0;

}
  • 測試伺服器的程式碼見上面服務程式碼的註釋。最後伺服器的結果是1020。也許有人會注意到上面有一行 sleep(4); 當客戶端呼叫socket準備建立第1022個套接字時,如上所示也會提示錯誤,此時socket函式返回-1出錯,如果沒有睡眠4s後再退出程序會有什麼問題呢?如果直接退出程序,會將客戶端所開啟的所有套接字關閉掉,即向伺服器端傳送了很多FIN段,而此時也許伺服器端還一直在accept ,即還在從已連線佇列中返回已連線套接字,此時伺服器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連線的套接字的可讀事件,read 返回0,所以會有很多 client close 欄位 參雜在條目的輸出中,還有個問題就是,因為read 返回0,伺服器端會將自身的已連線套接字關閉掉,那麼也許剛才說的客戶端某一個連線會被accept 返回,即測試不出伺服器端真正的併發容量。
  • 客戶端結果:
  • 伺服器結果:
  • 將 sleep(4); 註釋掉,可以看到輸出參雜著client close,且這次的count 達到了1021,原因就是伺服器端前面已經有些套接字關閉了,所以accept 建立套接字不會出錯,伺服器程序也不會因為出錯而退出,可以看到最後接收到的一個連線埠是33742,即不一定是客戶端的最後一個連線。觀察伺服器端的輸出如下:
  • 套接字I/超時設定方法
  • 用select實現超時
    • read_timeout 函式封裝
    • write_timeout 函式封裝
    • accept_timeout 函式封裝
    • connect__timeout 函式封裝
  • 三種設定方法:

    • 鬧鐘方式設定alarm(但是這種方法可能會與其他的鬧鐘產生衝突,不推薦使用)
      • 超時之後產生一個SIG_ALRM訊號
    • 套接字選項(也不推薦使用,移植性不太好)

      • SO_SNDTIMEO
      • SO_RCVTIMEO
    • select (主要使用select來設定超時)

      • read_timeout 函式封裝
 read_timeout - 讀超時檢測函式,不含讀操作 
* fd:檔案描述符 
* wait_seconds:等待超時秒數, 如果為0表示不檢測超時; 
* 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT 
*/ 
int read_timeout( int fd, unsigned int wait_seconds) 
{ 
int ret = 0 ; 
if (wait_seconds > 0 ) 
{ 
    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);  //select會阻塞直到檢測到事件或者超時 
         // 如果select檢測到可讀事件傳送,則此時呼叫read不會阻塞 
    } 
     while  (ret <  0  && errno == EINTR); 

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

} 

 return  ret;
}
  • write_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 , &write_fdset,  NULL , &timeout); 
    } 
     while  (ret <  0  && errno == EINTR); 

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

} 

 return  ret; 
}
  • accept_timeout 函式封裝

/* accept_timeout - 帶超時的accept 
* fd: 套接字 
* addr: 輸出引數,返回對方地址 
* wait_seconds: 等待超時秒數,如果為0表示正常模式 
* 成功(未超時)返回已連線套接字,失敗返回-1,超時返回-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 ) 
{ 

    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 ) 
         return  - 1 ; 
     else   if  (ret ==  0 ) 
    { 
        errno = ETIMEDOUT; 
         return  - 1 ; 
    } 
} 

 if  (addr !=  NULL ) 
    ret = accept(fd, ( struct  sockaddr *)addr, &addrlen); 
 else 
    ret = accept(fd,  NULL ,  NULL ); 
 if  (ret == - 1 ) 
    ERR_EXIT( "accpet error" ); 

 return  ret
  • connect__timeout 函式封裝
/* activate_nonblock - 設定IO為非阻塞模式 
* fd: 檔案描述符 
*/ 
void activate_nonblock( int fd) 
{ 
int ret; 
int flags = fcntl(fd, F_GETFL); 
if (flags == - 1 ) 
ERR_EXIT( “fcntl error” ); 
flags |= O_NONBLOCK; 
ret = fcntl(fd, F_SETFL, flags); 
 if  (ret == - 1 ) 
    ERR_EXIT( "fcntl error" ); 
}

/* deactivate_nonblock - 設定IO為阻塞模式 
* fd: 檔案描述符 
*/ 
void deactivate_nonblock( int fd) 
{ 
int ret; 
int flags = fcntl(fd, F_GETFL); 
if (flags == - 1 ) 
ERR_EXIT( “fcntl error” ); 
flags &= ~O_NONBLOCK; 
ret = fcntl(fd, F_SETFL, flags); 
 if  (ret == - 1 ) 
    ERR_EXIT( "fcntl error" ); 
}


/* 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) 
{ 

    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 ) 
    { 
        errno = ETIMEDOUT; 
         return  - 1 ; 
    } 
     else   if  (ret <  0 ) 
         return  - 1 ; 

     else   if  (ret ==  1 ) 
    { 
         /* ret返回為1,可能有兩種情況,一種是連線建立成功,一種是套接字產生錯誤 
         * 此時錯誤資訊不會儲存至errno變數中(select沒出錯),因此,需要呼叫 
         * 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; 
}