TCP程序中發送和接收數據

這裏咱們來探討一下在網絡編程過程當中,有關read/write 或者send/recv的使用細節。這裏有關經常使用的阻塞/非阻塞的解釋在網上有不少很好的例子,這裏就不說了,還有errno ==EAGAIN 異常等等。首先咱們拿一個簡單的實例代碼看一下。編程

read/write面臨的是什麼問題:

字節流套接字上調用read或write的返回值可能比請求的數量少,這並非出錯的狀態,這種狀況發生在內核中的用於套接字緩衝區的空間已經達到了極限,須要再次的調用read/write函數才能將剩餘數據讀出或寫入。那麼這裏能夠看到是內核緩衝區到達極限,那麼通常狀況下是多大呢?服務器

 CLIENT]$ cat /proc/sys/net/ipv4/tcp_wmem
4096    16384    4194304
CLIENT]$ cat /proc/sys/net/ipv4/tcp_rmem
4096    87380    6291456

第一個數據表示最小,第二個表示默認狀況下,第三個表示最大,單位是字節。若是read的緩衝區已經到達極限,那麼一次read並不能讀出本身想要的數據大小。那麼更多的狀況咱們並不知道對方發送的數據量是多大,咱們只有一個最大閥值。那麼這時該怎樣去控制read/write呢?網絡

阻塞的read和write的問題:

咱們來看<unix網絡編程> 中的read代碼以下socket

ssize_t                     /* Read "n" bytes from a descriptor. */  
readn(int fd, void *vptr, size_t n)  //這裏的n 是接收數據buffer的空間   真實狀況下咱們確實不太清楚客戶端到底它會發多少數據 通常是個閥值。
{  
    size_t  nleft;  
    ssize_t nread;  
    char    *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nread = read(fd, ptr, nleft)) < 0) {  
            if (errno == EINTR)  
                nread = 0;      /* and call read() again */  
            else  
                return(-1);  
        } else if (nread == 0)  
            break;              /* EOF */  
  
        nleft -= nread;  
        ptr   += nread;  
    }  
    return(n - nleft);      /* return >= 0 */  
}

咱們先看這個參數size_t n,由於多數狀況下,咱們並不嚴格規定客戶端到底一次要發多少數據,對於常規的服務器來講都要有一個最大閥值,超過這個閥值就表示是異常數據。想像一下若是沒有最大閥值,一個惡意的客戶端向一個服務器發送一個超大的文件,那麼這個服務器很快就會崩潰! 這裏的n的大小其實跟咱們的業務相關了。文件服務器就除外了咱們不談這種狀況。咱們繼續看 若此時文件描述符爲阻塞模式時,那麼當一個鏈接到達並開始發送一段數據後暫停發送數據(尚未斷開),由於客戶端並無斷開,同時它發送的數據尚未到達閥值 那麼勢必在read處一直阻塞,那麼若是是一個單線程服務器的話就不能處理其餘請求了。write的話這種狀況咱們通常都知道要發送數據的真實大小通常不發生這種狀況。tcp

ssize_t                     /* Write "n" bytes to a descriptor. */  
writen(int fd, const void *vptr, size_t n)  //這裏傳入的n通常就是數據的實際大小   while循環會正常返回。
{  
    size_t      nleft;  
    ssize_t     nwritten;  
    const char  *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {  
            if (nwritten < 0 && errno == EINTR)  
                nwritten = 0;       /* and call write() again */  
            else  
                return(-1);         /* error */  
        }  
  
        nleft -= nwritten;  
        ptr   += nwritten;  
    }  
    return(n);  
}


非阻塞的read/write:

由上面阻塞模式的狀況咱們再分析一下非組塞:ide

仍是以上的代碼:readn來講,若是是非阻塞,咱們仍是假定這裏客戶端發送了一點數據並無斷開。函數

ssize_t                     /* Read "n" bytes from a descriptor. */  
readn(int fd, void *vptr, size_t n)  //這裏的n 是接收數據buffer的空間   這種狀況咱們確實不太清楚客戶端到底它會發多少數據 通常是個閥值。
{  
    size_t  nleft;  
    ssize_t nread;  
    char    *ptr;  
  
    ptr = vptr;  
    nleft = n;  
    while (nleft > 0) {  
        if ( (nread = read(fd, ptr, nleft)) < 0) {  
            if (errno == EINTR)  
                nread = 0;      /* and call read() again */
       if (errno == EAGAIN)    //發生了這種異常我將它返回了,這裏表示文件描述符還不可讀,沒有準備好,我就直接將其返回,最後IO複用select/poll/epoll就會再讀取準備好的數據。
          return n - nleft;
else return(-1); } else if (nread == 0){
        //close(fd);
        break; /* EOF */     } nleft -= nread; ptr += nread; } return(n - nleft); /* return >= 0 */ }

1.  若是客戶端發送了一點數據而後沒有斷開處於暫停狀態的話。spa

那麼在調用read時就會出現EAGAIN的異常,這裏當發生這種異常時表示文件描述符尚未準備好,那麼我這裏直接將其返回已經讀到的size。這樣就不會形成一直阻塞在這裏其餘鏈接沒法處理的現象。.net

2. 若是客戶端發送了一點數據而後馬上斷開鏈接了線程

好比咱們第一次read的時候讀到了最後發來的數據,當再次讀取時讀到了EOF客戶端斷開了鏈接那咱們這個程序仍是有問題阿! 咱們這裏看到跳出來while並返回了正確讀到的數據  這時readn的返回是正確的,可是咱們有此次返回仍是不知道客戶端斷開了,雖然說咱們能夠向上述代碼加入close 可是咱們並不能有readn函數知道客戶端主動斷開鏈接。

對於2這種狀況就是咱們在開發過程當中經常遇到的狀況,這時咱們能夠在readn中再加入時當的參數就能夠解決,好比咱們傳入的是一個包含文件描述符號的結構體,結構體中含有標誌狀態的字段,再read == 0時將字段賦予一個值。再readn以後再有這個結構體的某個標誌知道已將鏈接斷開後續再斷開鏈接和刪除其事件便可。下面找到了Nginx關於recv的使用代碼:

ssize_t
ngx_unix_recv(ngx_connection_t *c, u_char *buf, size_t size)
{
    ssize_t       n;
    ngx_err_t     err;
    ngx_event_t  *rev;

    rev = c->read;

#if (NGX_HAVE_KQUEUE)

    if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: eof:%d, avail:%d, err:%d",
                       rev->pending_eof, rev->available, rev->kq_errno);

        if (rev->available == 0) {
            if (rev->pending_eof) {
                rev->ready = 0;
                rev->eof = 1;

                if (rev->kq_errno) {
                    rev->error = 1;
                    ngx_set_socket_errno(rev->kq_errno);

                    return ngx_connection_error(c, rev->kq_errno,
                               "kevent() reported about an closed connection");
                }

                return 0;

            } else {
                rev->ready = 0;
                return NGX_AGAIN;
            }
        }
    }

#endif

#if (NGX_HAVE_EPOLLRDHUP)

    if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {
        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: eof:%d, avail:%d",
                       rev->pending_eof, rev->available);

        if (!rev->available && !rev->pending_eof) {
            rev->ready = 0;
            return NGX_AGAIN;
        }
    }

#endif

    do {
        n = recv(c->fd, buf, size, 0);

        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
                       "recv: fd:%d %z of %uz", c->fd, n, size);

        if (n == 0) {
            rev->ready = 0;
            rev->eof = 1;

#if (NGX_HAVE_KQUEUE)

            /*
             * on FreeBSD recv() may return 0 on closed socket
             * even if kqueue reported about available data
             */

            if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                rev->available = 0;
            }

#endif

            return 0;
        }

        if (n > 0) {

#if (NGX_HAVE_KQUEUE)

            if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                rev->available -= n;

                /*
                 * rev->available may be negative here because some additional
                 * bytes may be received between kevent() and recv()
                 */

                if (rev->available <= 0) {
                    if (!rev->pending_eof) {
                        rev->ready = 0;
                    }

                    rev->available = 0;
                }

                return n;
            }

#endif

#if (NGX_HAVE_EPOLLRDHUP)

            if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)
                && ngx_use_epoll_rdhup)
            {
                if ((size_t) n < size) {
                    if (!rev->pending_eof) {
                        rev->ready = 0;
                    }

                    rev->available = 0;
                }

                return n;
            }

#endif

            if ((size_t) n < size
                && !(ngx_event_flags & NGX_USE_GREEDY_EVENT))
            {
                rev->ready = 0;
            }

            return n;
        }

        err = ngx_socket_errno;

        if (err == NGX_EAGAIN || err == NGX_EINTR) {
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err,
                           "recv() not ready");
            n = NGX_AGAIN;

        } else {
            n = ngx_connection_error(c, err, "recv() failed");
            break;
        }

    } while (err == NGX_EINTR);

    rev->ready = 0;

    if (n == NGX_ERROR) {
        rev->error = 1;
    }

    return n;
}
View Code

其中咱們還要注意在writen的非阻塞中,若是第一次寫入返回,當第二次寫入時對方斷了,再次寫入時就會發生EPIPE異常

while (nleft > 0) {  
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {  
            if (nwritten < 0 && errno == EINTR)  
                nwritten = 0;       /* and call write() again */  
            else if(errno == EPIPE)
            {
                    return 0;// 這裏我返回了0 由於最後一次發送數據並不保證對面已經收到了數據,這個數據到底有沒有被正確接收在這裏咱們沒法得到。若是對方關閉了就直接形成異常
            }
            else if(errno == EAGAIN)
           { 
                    return n-left;
           }
            else  
                return(-1);         /* error */  
        }  
  
        nleft -= nwritten;  
        ptr   += nwritten;  
    }                      

其實write數據並不表明數據被對方成功接收了,只是往內核緩衝區寫,若是寫入成功write就返回了,因此就沒法知道數據是否被收到,在一些嚴格要求的數據交互中經常使用應用層的確認機制。至於詳細的消息接收和發送的內容推薦下列博客: http://blog.csdn.net/yusiguyuan/article/details/24111289   和 http://blog.csdn.net/yusiguyuan/article/details/24671351

相關文章
相關標籤/搜索