1. 程式人生 > >Socket、send/recv的迴圈傳送和接收、緩衝區、阻塞

Socket、send/recv的迴圈傳送和接收、緩衝區、阻塞

這篇文章略作刪減後轉過來了。主要有以下幾點值得自己注意的:

(1)剛開頭對套接字的理解。

(2)緩衝區的理解。

其他部分有時間重新整理。

套接字的概念及分類

       在網路中,要全域性的標識一個參與通訊的程序,需要三元組:協議,IP地址以及埠號。要描述兩個應用程序之間的端到端的通訊關聯需要五元組:協議,信源主機IP,信源應用程序埠,信宿主機IP,信宿應用程序埠。為了實現兩個應用程序的通訊連線,提出了套接字的概念。套接字可以理解為通訊連線的一端,將兩個套接字連線在一起,可以實現不同程序之間的通訊。 
針對不同的通訊需求,TCP/IP中提供了三種不同的套接字: 
(1)流套接字(SOCK_STREAM) 
流套接字用於面向連線,可靠的資料傳輸服務。它之所以能實現可靠的資料服務,是因為它使用了傳輸控制協議–TCP。適合傳輸大量的資料,但是不支援廣播和組播。 
(2)資料報套接字(SOCK_DGRAM) 
資料報套接字提供了一種無連線的服務,通訊雙方不需要建立任何顯示連線,資料可以傳送到指定的套接字。資料報套接字使用UDP進行資料傳輸,支援廣播和組播方式。 
(3)原始套接字(SOCK_RAW) 
原始套接字與標準套接字(上面兩個)區別在於:原始套接字可以讀寫核心沒有處理的IP資料報,流套接字只能讀寫TCP資料報,資料報套接字只能讀寫UDP資料報。原始套接字的主要目的是避開TCP/IP的處理機制,被傳送的資料報可以直接傳送給需要他的程式。主要用於編寫自定義地層協議的應用程式。

socket相關函式

1.socket函式: 
功能:為應用程式建立套接字。 
格式:SOCKET socket(int af, int type, int protocol)。 
引數:af-套接字使用的協議地址族,如果使用TCP或者UDP,只能使用AF_INET;type-套接字協議型別,如SOCK_STREAM、SOCK_DGRAM;protocol-套接字使用的特定協議,如果不希望特別指定協議型別,則設定為0。 
返回值:函式成功呼叫後返回一個新的套接字,是一個無符號的整型資料;失敗時返回INVALID_SOCKET。 
說明:應用程式在使用套接字通訊之前,必須擁有一個套接字。

2.bind函式: 
功能:實現套接字與主機本地IP地址和埠號的繫結。 
格式:int bind(SOCKET s, const struct sockaddr *name, int namelen)。 
引數:s-將要繫結的套接字;name-與指定協議有關的地址結構指標;namelen-name引數的長度。 
返回值:函式成功時返回0;失敗時返回SOCKET_ERROR。

3.listen函式: 
功能:設定套接字為監聽狀態,準備接收由客戶機程序發出的連線請求。 
格式:int listen(SOCKET s, int backlog)。 
引數:s-已繫結地址,但還未建立連線的套接字識別符號;backlog-指定正在等待連線的最大佇列長度。 
返回值:函式成功時返回0;失敗時返回SOCKET_ERROR。 
說明:僅適用於面向連線的套接字,且用於伺服器程序。

4.connect函式: 
功能:提出與伺服器建立連線的請求,如果伺服器程序接受請求,則伺服器程序與客戶機進城之間便建立了一條通訊連線。 
格式:int connect(SOCKET s, const struct sockaddr FAR *name, int namelen )。 
引數:s-欲要建立連線的套接字;name-指向通訊對方的套接字地址結構指標,表示s欲與其建立連線;namelen-name引數的長度。 
返回值:函式成功時返回0;失敗時返回SOCKET_ERROR。 
說明:在客戶機程序呼叫該方法請求建立連線時,將啟用建立連線的3次握手,以此來建立一條與伺服器程序的TCP連線。如果該函式呼叫之前沒有繫結地址,系統自動繫結本地地址到此套接字。

5.accept函式: 
功能:接受客戶機程序呼叫connect函式發出的連線請求。 
格式:SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen)。 
引數:s-處於偵聽狀態的套接字;addr-指向一個用來存放發出連線請求的客戶機程序IP地址資訊的地址結構指標;addrlen-addr的長度。 
返回值:呼叫成功返回一個新的套接字,這個套接字對應於已接受的那個客戶機程序的連線,失敗時返回INVALID_SOCKET。 
說明:用於面向連線的伺服器程序,在IP協議族中只適用於TCP伺服器端。

7.shutdown函式: 
功能:關閉套接字讀寫通道,即停止套接字接受傳遞的功能。 
格式:int shutdown(SOCKET s, int how)。 
引數:s-套接字;how-描述停止哪些操作。 
how-0:不再接收訊息; 
how-1:不再允許傳送訊息; 
how-2:既不接收訊息,也不再發送訊息。 
返回值:函式成功時返回0;失敗時返回SOCKET_ERROR。 
說明:只是停止套接字的功能,並沒有關閉套接字,套接字的資源還沒釋放。

8.close函式: 
功能:關閉套接字,釋放與套接字關聯的所有資源。 
格式:int close(int fd)。 
引數:s-將要關閉的套接字。 
返回值:函式成功時返回0;失敗時返回SOCKET_ERROR。 
說明:當套接字s的資料緩衝佇列中還有未發出的資料時,如果套接字設定為SO_DONTLINGER,則等待資料緩衝佇列中的資料繼續傳輸完畢關閉該套接字;如果套接字設定為SO_LINGER,則分以下兩種情況: 
(1)Timeout設為0,套接字馬上關閉,資料緩衝佇列中資料丟失。 
(2)Timeout不為0,等待資料傳輸完畢或者Timeout為0時關閉套接字。

9.recv函式: 
功能:在已建立連線的套接字上接收資料。 
格式:int recv(SOCKET s, char *buf, int len, int flags)。 
引數:s-已建立連線的套接字;buf-存放接收到的資料的緩衝區指標;len-buf的長度;flags-呼叫方式: 
(1)0:接收的是正常資料,無特殊行為。 
(2)MSG_PEEK:系統緩衝區資料複製到提供的接收緩衝區,但是系統緩衝區內容並沒有刪除。 
(3)MSG_OOB:表示處理帶外資料。 
返回值:接收成功時返回接收到的資料長度,連線結束時返回0,連線失敗時返回SOCKET_ERROR。

10.send函式: 
功能:在已建立連線的套接字上傳送資料. 
格式:int send(SOCKET s, const char *buf, int len, int flags)。 
引數:引數:s-已建立連線的套接字;buf-存放將要傳送的資料的緩衝區指標;len-傳送緩衝區中的字元數;flags-控制資料傳輸方式: 
(1)0:接收的是正常資料,無特殊行為。 
(2)MSG_DONTROUTE:表示目標主機就在本地網路中,無需路由選擇。 
(3)MSG_OOB:表示處理帶外資料。 
返回值:傳送成功時返回傳送的資料長度,連線結束時返回0,連線失敗時返回SOCKET_ERROR。

套接字程式設計流程

這裡寫圖片描述

這裡寫圖片描述

socket緩衝區

每一個socket在被建立之後,系統都會給它分配兩個緩衝區,即輸入緩衝區和輸出緩衝區。 

這裡寫圖片描述

(1)send函式並不是直接將資料傳輸到網路中,而是負責將資料寫入輸出緩衝區,資料從輸出緩衝區傳送到目標主機是由TCP協議完成的。資料寫入到輸出緩衝區之後,send函式就可以返回了,資料是否傳送出去,是否傳送成功,何時到達目標主機,都不由它負責了,而是由協議負責。

(2)recv函式也是一樣的,它並不是直接從網路中獲取資料,而是從輸入緩衝區中讀取資料。

輸入輸出緩衝區,系統會為每個socket都單獨分配,並且是在socket建立的時候自動生成的。一般來說,預設的輸入輸出緩衝區大小為8K。套接字關閉的時候,輸出緩衝區的資料不會丟失,會由協議傳送到另一方;而輸入緩衝區的資料則會丟失。

socket資料傳送與接收問題

資料的傳送和接收是獨立的,並不是傳送方執行一次send,接收方就執行以此recv。recv函式不管傳送幾次,都會從輸入緩衝區儘可能多的獲取資料。如果傳送方傳送了多次資訊,接收方沒來得及進行recv,則資料堆積在輸入緩衝區中,取資料的時候會都取出來。換句話說,recv並不能判斷資料包的結束位置。

send函式: 
在資料進行傳送的時候,需要先檢查輸出緩衝區的可用空間大小,如果可用空間大小小於要傳送的資料長度,則send會被阻塞,直到緩衝區中的資料被髮送到目標主機,有了足夠的空間之後,send函式才會將資料寫入輸出緩衝區。

TCP協議正在將資料傳送到網路上的時候,輸出緩衝區會被鎖定(生產者消費者問題),不允許寫入,send函式會被阻塞,直到資料傳送完,輸出緩衝區解鎖,此時send才能將資料寫入到輸出緩衝區。

要寫入的資料大於輸出緩衝區的最大長度的時候,要分多次寫入,直到所有資料都被寫到緩衝區之後,send函式才會返回。

recv函式: 
函式先檢查輸入緩衝區,如果輸入緩衝區中有資料,讀取出緩衝區中的資料,否則的話,recv函式會被阻塞,等待網路上傳來資料。如果讀取的資料長度小於輸出緩衝區中的資料長度,沒法一次性將所有資料讀出來,需要多次執行recv函式,才能將資料讀取完畢。

迴圈傳送和接收

防止send或者 recv不完整,這樣你想發一個 
幾MB直接呼叫下面方法就okay,不會少發~

bool SendAll(SOCKET &sock, char*buffer, int size)
{
    while (size>0)
    {
        int SendSize= send(sock, buffer, size, 0);
        if(SOCKET_ERROR==SendSize)
            return false;
        size = size - SendSize;//用於迴圈傳送且退出功能
        buffer+=SendSize;//用於計算已發buffer的偏移量
    }
    return true;
}

bool RecvAll(SOCKET &sock, char*buffer, int size)
{
    while (size>0)//剩餘部分大於0
    {
        int RecvSize= recv(sock, buffer, size, 0);
        if(SOCKET_ERROR==RecvSize)
            return false;
        size = size - RecvSize;
        buffer+=RecvSize;
    }
    return true;
}