TCP網絡編程MSS細節

8.I/O通信

從I/O的角度來看,套接字也是文件,它提供了同文件讀寫(fread()/fwrite())對應的收發數據操作接口:send()/recv()。

8.1 發送數據

8.1.1 send

// The send function sends data on a connected socket.

int send(

SOCKETs, // [in] Descriptor identifying a connected socket.

const char FAR *buf, // [in] Buffer containing the data to be transmitted.

int len, // [in] Length of the data in buf.

int flags// [in] Indicator specifying the way in which the call is made.

);

send()函數在一個已連接的套接字s上執行數據發送操作。對於客戶機而言,發送的目標地址即connect()調用時所指定的地址;對於服務器而言,發送的目標地址即accept()調用所返回的地址。發送的內容爲保存在緩衝區buf中,發送的內容長度爲len。最後一個參數flags,通常情況下填0。

send()函數只是將欲發送的內容從用戶緩衝區拷貝到系統緩衝區(TCP Send Socket Buffer),系統的默認socket發送緩衝區(SO_SNDBUF)的大小爲8K,我們可以調用setsockopt()將其更改,理論上最大爲64K(The maximum congestion window is related to the amount of buffer space that the kernel allocates for each socket)。

只要系統緩衝區足夠大,send()執行完拷貝立即返回實際拷貝的字節數。如果系統緩衝區不夠大,例如在網絡擁塞或帶寬下降的情況下,用戶大量地投遞send()操作導致TCP Send Socket Buffer迅速充滿,此時再調用send()操作,可能返回的值(即實際拷貝字節數)要小於我們傳入的期待發送數量(len),在超時不得受理的情況下,返回SOCKET_ERROR,WSAGetLastError()=WSAETIMEDOUT。故大塊的數據可能不能一次性“發送”完畢,通常需要檢測send()返回值,多次調用send()直到“發送”完畢,可參考CSocket::Send()實現。關於發送超時限制(send timeout),可以調用setsockopt()在SOL_SOCKET級別設置SO_SNDTIMEO選項值,以毫秒爲單位。建議最多兩分鐘,因爲TCP的MSL(Maximum Segment Lifetime)即爲兩分鐘。

需要注意的是,用戶可能短時間內需要發送多個小數據包,在TCP/IP中,Nagle算法要求主機等待數據積累到一定數量後或超過預定時間才發送。默認情況下實施Nagle算法,通信方會在向對方發送確認(ACK)信息之前,花費一定的時間來等待要傳入的數據,這樣,主機的就不必發送一個只有確認信息的數據報。發送小的數據包不僅沒有多少意義,而且徒增錯誤檢查和確認的開銷。如果不想是使用Nagle算法,以“保留髮送邊界”,用戶可調用setsockopt()函數在IPPROTO_TCP選項級別設置TCP_NODELAY爲TRUE。例如一次獨立的HTTP GET請求往往希望“保留髮送邊界”,服務器的HTTP Response Header往往希望“保留髮送邊界”以區分後續的HTTP Response Content。體現在TCP層,即開啓“PSH”選項。

具體的發送工作交由系統的傳輸層驅動程序完成。因爲TCP提供可靠有序的傳輸機制,故我們總是很放心地認爲它會將我們的數據發送到目的端。至於TCP分多少次將數據發送至對方,由協商的MSS(Max Segment Size)和接收方的TCP Window決定。

8.1.2 sendto

// The sendto function sends data to a specific destination.

int sendto(

SOCKETs,

const char FAR *buf,

int len,

int flags,

const struct sockaddr FAR *to, // [in] Optional pointer to the address of the target socket.

int tolen// [in] Size of the address in to.

);

sendto()函數只是比send()函數多出了一個目的地址信息參數,主要用於面向無連接的UDP通信。TCP套接字在建立連接(connect-accept)時,便知曉對方地址信息,而UDP套接字通信之前不建立連接,需要通信時,調用sendto()將消息發送給目的地址(to)。無論對方是否在指定端口“監聽”,sendto總是把數據發出去,要知道UDP是沒有迴應確認的。

註釋中,sendto()函數的目標地址是“optional”,當我們忽略最後兩個參數時,完全可以替換send()函數使用。實際上,這很方便我們在編程接口上提供統一。例如live555的writeSocket接口針對TCP和UDP套接字統一使用sendto()。

由於UDP協議基本上只是在IP協議上做了簡單的封裝(Source Port+Destination Port+Length+Checksum),其沒有做可靠性傳輸保障,故對UDP套接字一次sendto()的數據量不宜過大,最好以MTU爲基準。使用UDP套接字往發送大數據塊,往往因爲IP分片等原因丟包,考慮異構網絡及設備的MTU不同,一般一次發送512字節左右比較合適。

我們在一個UDP套接字上執行connect()操作,並未真正建立連接,而是執行一種目的地址“綁定”,事後我們可以使用send()函數替換sendto()函數。要取消UDP套接字與目的地址的關聯,唯一的辦法是在這個套接字上以INADDR_ANY爲目標地址調用connect()。

8.2 接收數據

8.2.1 recv

// The recv function receives data from a connected or bound socket.

int recv(

SOCKETs, // [in] Descriptor identifying a connected socket.

char FAR *buf, // [out] Buffer for the incoming data.

int len, // [in] Length of buf.

int flags// [in] Flag specifying the way in which the call is made.

);

recv()函數在一個已連接的套接字s上執行數據接收操作。對於客戶機而言,數據的源地址即connect()調用時所指定的地址;對於服務器而言,數據的源地址即accept()調用所返回的地址。接收的內容爲保存至長度爲len的緩衝區buf,最後一個參數flags,通常情況下填0。

recv()函數只是將TCP層當前接收到的數據流從系統緩衝區(TCP Receive Socket Buffer)拷貝到用戶緩衝區,系統的默認socket 接收緩衝區(SO_RCVBUF)的大小爲8K,我們可以調用setsockopt()將其更改,理論上最大爲64K(The maximum congestion window is related to the amount of buffer space that the kernel allocates for each socket)。

recv()函數返回實際接收到的數據,可能小於緩衝區的長度len,可能當前到達的有效數據大於len,但最大返回len。在超時仍無數據到來的情況下,返回SOCKET_ERROR,WSAGetLastError()=WSAETIMEDOUT。關於接收超時限制(receive timeout),可以調用setsockopt()在SOL_SOCKET級別設置SO_RCVTIMEO選項值,以毫秒爲單位。建議最多兩分鐘,因爲TCP的MSL(Maximum Segment Lifetime)即爲兩分鐘。

如果對方不停發送數據,而本機過於繁忙疲於應付,則可能導致數據大量累積,一旦TCP Receive Socket Buffer或TCP Window充滿,則可能產生數據溢出。TCP滑動窗口機制,由接收方建議性的控制發送量,即每一次確認迴應(ACK)時都告知對方自己當前的接收能力(TCP窗口的大小),發送方據此有效地控制自己的發送行爲,協調雙方的通信步伐。

由於基於流的TCP協議,未保留消息邊界(boundary)的概念,發送者發送的數據很快就會聚集在系統接收緩衝區(TCP堆棧)中。假設這樣一種情景,客戶端連接流媒體服務器(如IP攝像頭)後,發送請求碼流的請求,這以後服務器總是將連續不斷地推送數據過來(如IP攝像頭實時監控碼流)。若客戶端不執行recv()拷貝操作而又尚未關閉連接,則服務器不斷推送數據到客戶端的TCP Stack,直至TCP window size=0。

不管消息邊界是否存在,接收端都會盡量地讀取當前的有效數據。執行拷貝後,數據將立即從系統緩衝區刪除,以釋放部分TCP Window。因爲流的無邊界性,故用戶投遞了三個send(),可能接收端只需一次或兩次recv()即接收完成。若客戶三次send()的是結構化的數據,而接收端收到的是粘連在一起的一大坨數據或兩塊隨機邊界數據,這種情況即通常所說的TCP粘包問題。

具體的接收工作交由系統的傳輸層驅動程序完成。因爲TCP提供可靠有序的傳輸機制,故我們總是很放心地認爲它會將對方發送過來的數據正確的提交給我們。這裏面的“正確”是指應用層面的報文結構及格式,即使TCP層面發生了偶然的丟包重傳(retransmit out of order),但我們得到的仍然是對方提交的完整的報文。應用層協議就需要我們自己解析了。

粘包問題需要我們聯合發送方,採取有效邊界措施在應用層重組出正確的報文。例如,發送方往往在一個數據包的頭4個字節告知對方接下來的數據有多少,這樣接收方就能有效的執行接收,以保留邊界和結構性。假設接收方得知發送方將發送32KB的數據過來,便投遞一個32KB的緩衝區調用recv試圖一次性接收完畢,這將以失敗告終。實際上,發送方的TCP層將按MSS尺寸將TCP報文分解成很多個段(Segment)分多次發送給接收方。當然,它們往往具有相同的確認號(ack),以表示這些段是一個迴應報文。這樣,客戶端才能識別出TCP segment of a reassembled PDU,以正確重組報文。可參考CSocket::Receive()實現。

8.2.2 recvfrom

// The recvfrom function receives a datagram and stores the source address.

int recvfrom(

SOCKETs,

char FAR* buf,

int len,

int flags,

struct sockaddr FAR *from, // [out] Optional pointer to a buffer that will hold the source address upon return.

int FAR *fromlen// [in, out] Optional pointer to the size of the from buffer.

);

recvfrom/recv與sendto/send在行爲學上同功,因爲事先不知發送方爲誰,故只要進來的通信,都將對方的地址保存在參數from中。值得注意的是,儘管UDP中沒有TCP監聽、連接等概念,但是作爲接收方往往需要在本地某個端口上等待,這個端口必須是專用,約定用戶預知的。故通常在調用recvfrom之前,必須顯式調用bind()函數將UDP套接字關聯到本地某個指定端口,進行“監聽”。

UDP通信是基於離散消息(message)的,故要麼收到對方發送的消息包,要麼整包丟失,接收方不得而知。如果整包丟失了,由於接收方不得而知,故沒有反饋信息,也不會重發。這就是UDP通信的不可靠處。

live555中的readSocket接口針對TCP和UDP套接字統一使用recvfrom。


轉自:http://ilovedouzhou.iteye.com/blog/1626182

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