網絡編程中的read,write函數

轉自: http://blog.csdn.net/chenping314159/article/details/7624029

關於TCP/IP協議,建議參考Richard Stevens的《TCP/IP Illustrated,vol1》(TCP/IP詳解卷1)。

關於第二層面,依然建議Richard Stevens的《Unix network proggramming,vol1》(Unix網絡編程卷1),這兩本書公認是Unix網絡編程的聖經。

至於第三個層面,UNP的書中有所提及,也有著名的C10K問題,業界也有各種各樣的框架和解決方案,本人才疏學淺,在這裏就不一一敷述。

 

本文的重點在於第二個層面,主要總結一下Linux下TCP/IP網絡編程中的read/write系統調用的行爲,知識來源於自己網絡編程的粗淺經驗和對《Unix網絡編程卷1》相關章節的總結。由於本人接觸Linux下網絡編程時間不長,錯誤和疏漏再所難免,望看官不吝賜教。

 

一. read/write的語義:爲什麼會阻塞?

先從write說起:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

首先,write成功返回,只是buf中的數據被複制到了kernel中的TCP發送緩衝區。至於數據什麼時候被髮往網絡,什麼時候被對方主機接收,什麼時候被對方進程讀取,系統調用層面不會給予任何保證和通知。

write在什麼情況下會阻塞?當kernel的該socket的發送緩衝區已滿時。對於每個socket,擁有自己的send buffer和receive buffer。從Linux 2.6開始,兩個緩衝區大小都由系統來自動調節(autotuning),但一般在default和max之間浮動。

# 獲取socket的發送/接受緩衝區的大小:(後面的值是在我在Linux 2.6.38 x86_64上測試的結果)
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max        #131071
sysctl net.core.wmem_default       #126976
sysctl net.core.wmem_max           #131071

已經發送到網絡的數據依然需要暫存在send buffer中,只有收到對方的ack後,kernel才從buffer中清除這一部分數據,爲後續發送數據騰出空間。接收端將收到的數據暫存在receive buffer中,自動進行確認。但如果socket所在的進程不及時將數據從receive buffer中取出,最終導致receive buffer填滿,由於TCP的滑動窗口和擁塞控制,接收端會阻止發送端向其發送數據。這些控制皆發生在TCP/IP棧中,對應用程序是透明的,應用程序繼續發送數據,最終導致send buffer填滿,write調用阻塞。

一般來說,由於接收端進程從socket讀數據的速度跟不上發送端進程向socket寫數據的速度,最終導致發送端write調用阻塞。

而read調用的行爲相對容易理解,從socket的receive buffer中拷貝數據到應用程序的buffer中。read調用阻塞,通常是發送端的數據沒有到達。

 

二. blocking(默認)和nonblock模式下read/write行爲的區別:

將socket fd設置爲nonblock(非阻塞)是在服務器編程中常見的做法,採用blocking IO併爲每一個client創建一個線程的模式開銷巨大且可擴展性不佳(帶來大量的切換開銷),更爲通用的做法是採用線程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。

1
2
3
4
5
6
7
8
// 設置一個文件描述符爲nonblock
intset_nonblocking(intfd)
{
    intflags;
    if((flags = fcntl(fd, F_GETFL, 0)) == -1)
        flags = 0;
    returnfcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

幾個重要的結論:

1. read總是在接收緩衝區有數據時立即返回,而不是等到給定的read buffer填滿時返回。

只有當receive buffer爲空時,blocking模式纔會等待,而nonblock模式下會立即返回-1(errno = EAGAIN或EWOULDBLOCK)

2. blocking的write只有在緩衝區足以放下整個buffer時才返回(與blocking read並不相同)

nonblock write則是返回能夠放下的字節數,之後調用則返回-1(errno = EAGAIN或EWOULDBLOCK)

 對於blocking的write有個特例:當write正阻塞等待時對面關閉了socket,則write則會立即將剩餘緩衝區填滿並返回所寫的字節數,再次調用則write失敗(connection reset by peer),這正是下個小節要提到的:

 

三. read/write對連接異常的反饋行爲:

對應用程序來說,與另一進程的TCP通信其實是完全異步的過程:

1. 我並不知道對面什麼時候、能否收到我的數據

2. 我不知道什麼時候能夠收到對面的數據

3. 我不知道什麼時候通信結束(主動退出或是異常退出、機器故障、網絡故障等等)

對於1和2,採用write() -> read() -> write() -> read() ->...的序列,通過blocking read或者nonblock read+輪詢的方式,應用程序基於可以保證正確的處理流程。

對於3,kernel將這些事件的“通知”通過read/write的結果返回給應用層。


假設A機器上的一個進程a正在和B機器上的進程b通信:某一時刻a正阻塞在socket的read調用上(或者在nonblock下輪詢socket)

當b進程終止時,無論應用程序是否顯式關閉了socket(OS會負責在進程結束時關閉所有的文件描述符,對於socket,則會發送一個FIN包到對面)。

”同步通知“:進程a對已經收到FIN的socket調用read,如果已經讀完了receive buffer的剩餘字節,則會返回EOF:0

”異步通知“:如果進程a正阻塞在read調用上(前面已經提到,此時receive buffer一定爲空,因爲read在receive buffer有內容時就會返回),則read調用立即返回EOF,進程a被喚醒。

socket在收到FIN後,雖然調用read會返回EOF,但進程a依然可以其調用write,因爲根據TCP協議,收到對方的FIN包只意味着對方不會再發送任何消息。 在一個雙方正常關閉的流程中,收到FIN包的一端將剩餘數據發送給對面(通過一次或多次write),然後關閉socket。

但是事情遠遠沒有想象中簡單。優雅地(gracefully)關閉一個TCP連接,不僅僅需要雙方的應用程序遵守約定,中間還不能出任何差錯。

假如b進程是異常終止的,發送FIN包是OS代勞的,b進程已經不復存在,當機器再次收到該socket的消息時,會迴應RST(因爲擁有該socket的進程已經終止)。a進程對收到RST的socket調用write時,操作系統會給a進程發送SIGPIPE,默認處理動作是終止進程,知道你的進程爲什麼毫無徵兆地死亡了吧:)

from 《Unix Network programming, vol1》 3rd Edition:

"It is okay to write to a socket that has received a FIN, but it is an error to write to a socket that has received an RST."

通過以上的敘述,內核通過socket的read/write將雙方的連接異常通知到應用層,雖然很不直觀,似乎也夠用。

這裏說一句題外話:

不知道有沒有同學會和我有一樣的感慨:在寫TCP/IP通信時,似乎沒怎麼考慮連接的終止或錯誤,只是在read/write錯誤返回時關閉socket,程序似乎也能正常運行,但某些情況下總是會出奇怪的問題。想完美處理各種錯誤,卻發現怎麼也做不對。

原因之一是:socket(或者說TCP/IP棧本身)對錯誤的反饋能力是有限的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章