這里我們來探討一下在網絡編程過程中,有關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代碼如下
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的話這種情況我們一般都知道要發送數據的真實大小一般不發生這種情況。
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:
由上面阻塞模式的情況我們再分析一下非組塞:
還是以上的代碼: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. 如果客戶端發送了一點數據然后沒有斷開處於暫停狀態的話。
那么在調用read時就會出現EAGAIN的異常,這里當發生這種異常時表示文件描述符還沒有准備好,那么我這里直接將其返回已經讀到的size。這樣就不會造成一直阻塞在這里其他連接無法處理的現象。
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; }
其中我們還要注意在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