數據報套接字通信
引言
數據報套接字。它提供了一種無連接、不可靠的雙向數據傳輸服務。數據包以獨立的形式被髮送,並且保留了記錄邊界,不提供可靠性保證。數據在傳輸過程中可能會丟失或重複,並且不能保證在接收端按發送順序接收數據。在TCP/IP協議簇中,使用UDP協議來實現數據報套接字。在出現差錯的可能性較小或允許部分傳輸出錯的應用場合,可以使用數據報套接字進行數據傳輸,這樣通信的效率較高。其服務靈活簡單,在現實生活中得到了廣泛的應用。
TCP傳輸數據的缺點
在瞭解UDP協議之前,我們需要先來了解一下TCP協議存在哪些缺陷呢?
相當於UDP協議來說,TCP協議增加了可靠性,流量控制、擁塞控制等機制,能夠保證數據傳遞的可靠性,那麼是不是在所有情況下使用TCP協議都是最合適的呢?
- 首先,使用TCP協議傳輸數據的代價相對於UDP協議而言要高許多。如果使用TCP協議實現一次請求-應答交換,由於TCP協議使用3次握手建立連接,並且再關閉連接時進行4四次揮手交互,那麼最小事務處理時間將是2✖RTT+SPT,其中RTT表示客戶與服務器之間的往返時間,STP表示客戶請求的服務器處理時間。相比之下,UDP沒有連接建立和釋放時間,就單個UDP請求-應答交互而言的最小處理時間僅爲RTT+SPT,比TCP減少了一個RTT。因此,傳輸代價是使用TCP協議時必須要考慮的一個損失。
- 其次,連接的存在意味着連接維護的代價。服務器要爲每一個已經建立連接的客戶分配單獨使用的資源,如用於接收和發送的TCP緩衝區、存儲連接相關參數的TCP變量等,這對於有可能胃痛是來自數百個不同客戶的請求提供服務的服務器來說,會嚴重增加該服務器的負擔,甚至於造成服務器資源的過耗。
- 最後,在每個連接的通信過程中,TCP擁塞控制中的慢啓動策略會起作用,使得每個TCP連接都要起始於慢啓動階段。由此帶來的結果是數據通信的效率不會馬上到達TCP的最大傳輸性能,也因此增大了使用TCP協議進行網絡通信的傳輸延遲。
由此分析來看,儘管TCP提供了可靠的數據傳輸服務,簡化了上層應用程序的設計複雜性,但同時也有一些性能和資源方面的損失,TCP協議未必是所有網絡應用程序在選擇傳輸協議時最佳的的選擇。
UDP傳輸特點
UDP協議是一個無連接的傳輸層協議,提供面向事務的簡單、不可靠的信息傳送服務。
UDP協議的傳輸特點表現在以下方面:
多對多通信:UDP在通信實體的數據量上具有更大的靈活性,多個發送方可以向一個接收方發送報文,一個發送方也可以向多個接收方發送數據,更重要的是,UDP能讓應用使用底層網絡的廣播或者組播設施交付報文。
不可靠服務:UDP提供的服務是不可靠交付的,即報文可以丟失、重複或失序,它沒有重傳設施,如果發生故障,也不會通知發送方。
缺乏流量控制:UDP不提供流量控制,當數據包到達的速度比接收系統或應用的處理速度快時,只是將其丟棄而不會發出警告。
報文模式:UDP提供了面向報文的傳輸方式,在需要傳輸數據的時候,發送方準確指明要求發送數據的字節數,UDP將這些數據放置在一個外發送報文中,在接受方,UDP一次交付一個傳入報文。因此當有數據交付時,接收到的數據擁有和發送方應用程序所指定的一樣的報文邊界。
UDP首部
UDP數據報文封裝在IP數據包的數據部分,UDP數據在IP數據包中的封裝如下圖所示:
(1)UDP首部的數據格式如下
(2)UDP首部個字段的含義如下
- 1.源、目的端口號。每個UDP數據的報文都包含源端口號和目的端口號,用於尋找發送端和接收端的應用進程。UDP端口號和TCP端口號是相互獨立的。
- 2.UDP長度。UDP長度字段指UDP首部和UDP數據的字節總長度,該字段的最小值是8,即數據部分位0.
- 3.UDP校驗和。UDP校驗和是一個端到端的校驗和。它由發送端計算,然後由接收端驗證,其目的是發現UDP首部和數據在發送端到接收端之間發生的任何變動。檢驗和的覆蓋範圍包括UDP首部、UDP僞首部和UDP數據。UDP的校驗和是可選的,如果UDP中校驗和字段爲0,表示不進行校驗和計算。
(3)數據報套接字編程的適用場合
數據報套接字基於不可靠的報文傳輸服務,這種服務的特點是無連接、不可靠。無連接的特點決定了數據報套接字的傳輸非常靈活,具有資源消耗小、處理速度快的優點。而不可靠的特點意味着在網絡質量不佳的環境下,發生數據包丟失的現象會比較嚴重,因此上層應用程序在設計開發時需要考慮網絡應用程序運行的環境以及數據在傳輸過程中的丟失、亂序、重複對應用程序帶來的負面影響。總體來看,數據報套接字適合於在以下場合使用:
1)音頻、視頻的實時傳輸應用。數據報套接字適合用於音頻、視頻這類對實時性要求比較高的數據傳輸應用。傳輸內容通常被切分爲獨立的數據報,其類型多爲編碼後的媒體信息。在這種應用場景下,通常要求實時音視頻傳輸,與TCP協議相比,UDP 協議減少了確認、同步等操作,節省了很大的網絡開銷。UDP協議能夠提供高效率的傳輸服務,實現數據的實時性傳輸,因此在網絡音視頻的傳輸應用中,應用UDP協議的實時性並增加控制功能是較爲合理的解決方案,如RTP和RTCP在音視頻傳輸中是兩個廣泛使用的協議組合,通常RTP基於UDP傳輸音視頻數據,RTCP基於TCP傳輸提供服務質量的監視與反饋、媒體間同步等功能。
2)廣播或多播的傳輸應用。流式套接字只能用於1對1的數據傳輸,如果應用程序需要廣播或多播傳送數據,那麼必須使用UDP協議,這類應用包括多媒體系統的的多播或廣播業務、局域網聊天室或者以廣播形式實現的局域網掃描器等。
3)簡單高效需求大於可靠需求的傳輸應用。儘管UDP不可靠,但其高效的傳輸特點使其在一些特殊的傳輸應用中受到歡迎,比如聊天軟件常常用到UDP協議傳送文件,日誌服務器通常設計位基於UDP協議來接受日誌。這些應用不希望在每次傳遞小數據時消耗昂貴的TCP連接建立與維護代價,而且即使偶爾丟失一兩個數據包,也不會對接受結果產生大影響,在這種場景下,UDP協議的簡單高效特性就會非常的合適。
數據報套接字的通信過程
使用數據報套接字傳送數據類似於生活中的郵件發送,與流式套接字的通信過程有所不同,數據報套接字不需要建立連接,而是直接根據目的地址構造數據包進行傳送。
(1)基於數據報套接字的服務器進程的通信過程
在通信過程中,服務器進程作爲服務提供方,被動接受客戶的請求,使用UDP協議與客戶交互,其基本通信過程如下:
1) Windows Sockets DLL初始化,協商版本號:
2)創建食接字,指定使用UDP (無連接的傳輸服務)進行通信:
3)指定本地地址和通信端口;
4)等待客戶的數據請求;
5)進行數據傳輸;
6)關閉套接字;
7)結束對Windows Sockets DLL的使用,釋放資源。
(2)基於數據報套接字的客戶進程的通信過程
在通信過程中,客戶進程作爲服務請求方,主動向服務器發送服務器請求,使用UDP協議與服務器交互,其基本通信過程如下:
1) Windows Sockets DLL初始化,協商版本號;
2)創建套接字,指定使用UDP (無連接的傳輸服務)進行通信;
3)指定服務器地址和通信端口;
4)向服務器發送數據請求;
5)進行數據傳輸;
6)關閉套接字;
7)結束對Windows Sockets DLL的使用,釋放資源。
(3)數據報套接字的使用模式
我們知道,UDP是一個無連接協議,也就是說,它僅僅傳輸獨立的有目的地址的數據報。“連接”的概念似乎與數據報套接字無關,而實際上,在有些情況下,“連接”在數據報套接字中的使用可以幫助網絡應用程序在可靠性和效率方面有一一定程度的優化。
1.兩種數據報套接字的使用模式
在數據報套接字的使用過程中,可以有兩種數據發送和接收的方式。
- (1)非連接模式
在非連接模式下,應用程序在每次數據發送前指定目的IP和端口號,然後調用sendto(函數或WSASendToO函數將數據發送出去,並在數據接收時調用recvfrom0函數或WSARecvFrom()函數,從函數返回參數中讀取接收數據報的來源地址。這種模式通常適用於服務器的設計,服務器面向大量客戶,接收不同客戶的服務請求,並將數據應答發送給不同的客戶地址。另外,這種模式也同樣適用於廣播地址或多播地址的發送。以廣播方式發送數據爲例,應用程序需要使用setsockopt()函數來開啓SO_BROADCAST選項,並將目的地址設置爲INADDR_ BROADCAST (相當於inet addr(“255.255.255.255" ))。
非連接模式是數據報套接字默認使用的數據發送和接收方式,這種模式的優點是數據發送的靈活性較好。
- (2)連接模式
在連接模式下,應用程序首先調用connect()函數或WSAConnect()函數指明遠端地址,即確定了唯一的通信對方地址,在之後的數據發送和接收過程中,不用每次重複指明遠程地址就可以發送和接收報文。此時,send()函數、WSASend( )函數、sendto()函數和WSASendTo()函數可以通用,recv()函數、WSARecv()函數、recvfrom()函數和WSARecvFrom()函數也可以通用。處於連接模式的數據報套接字工作過程如圖6-5所示,來自其他不匹配的IP地址或端口的數據報不會投遞給這個已連接的套接字。如果沒有相匹配的其他套接字,UDP將丟棄它們並生成相應的ICMP端口不可達錯誤。
(4)"連接"在套接字中的含義
對於TCP來說,調用connect0將導致雙方進人TCP的三次握手初始化連接階段,客戶會發送SYN段給服務器,接收服務器返回的確認和同步請求,在連接建立好後,雙方交換了一些初始的狀態信息,包括雙方的IP地址和端口號。因此,對於流式套接字的connect()函數操作而言,connect() 函數完成的功能是: 1) 在調用方爲套接字關聯遠程主機的地址和端口號; 2)與遠端主機建立連接。該函數的成功暗示着服務器是正在提供服務的且雙方的路徑是可達的。從使用次數上來看,connect() 函數只能在流式套接字上調用次。
對於UDP來說,由於雙方沒有共享狀態要交換,所以調用connect()函數完全是本地操作,不會產生任何網絡數據。因此,對於數據報套接字的connect()操作而言,conect)函數完成的功能是:在調用方爲套接字關聯遠程主機的地址和端口號。由於沒有網絡通信行爲發生,該函數的成功並不意味着對等方- -定會對後續的數據請求產生迴應,可能服務器是關閉的,也可能網絡根本就沒用連通。也可能網絡根本就沒有連通。一個 數據報套接字可以多次調用cnnect()函數,目的可能是: 1)指定新的IP地址和端口號; 2)斷開套接字。對於第一個目的,通過再次調用connect(), 可以使得數據報套接字更新所關聯的遠端端點地址;對於第二個目的,爲了斷開一個已連接的數據報套接字,在再次調用connect()函數時,把套接字地址結構的地址族成品設置爲AF_UNSPEC,此時,後續的send()/WSASend()、recvO/WSARecv()函數都將返回錯誤。
數據報通信代碼如下
客戶端:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int __cdecl main(int argc, char **argv)
{
WSADATA wsaData;
SOCKET ConnectLessSocket = INVALID_SOCKET;
struct addrinfo *result = NULL, *ptr = NULL, hints;
char *sendbuf = "this is a test";
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
if (argc != 2) {
printf("usage: %s server-name\n", argv[0]);
return 1;
}
//初始化套接字
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
//解析服務器地址和端口號
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//創建數據報套接字
ConnectLessSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ConnectLessSocket == INVALID_SOCKET) {
printf("scoket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
//發送緩衝區中的測試數據
iResult = sendto(ConnectLessSocket, sendbuf, (int)strlen(sendbuf), 0, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectLessSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
printf("Bytes Sent: %ld\n", iResult);
//接收數據
iResult = recvfrom(ConnectLessSocket, recvbuf, recvbuflen, 0, NULL, NULL);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed\n");
else
printf("recv failed with error: %d\n", WSAGetLastError());
//關閉套接字
closesocket(ConnectLessSocket);
//釋放資源
WSACleanup();
system("pause");
return 0;
}
服務端:
#define _CRT_SECURE_NO_WARNINGS 1
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
int __cdecl main(int argc, char **argv)
{
WSADATA wsaData;
int iResult;
SOCKET ServerSocket = INVALID_SOCKET;
struct addrinfo *result = NULL;
struct addrinfo hints;
sockaddr_in clientaddr;
int clientlen = sizeof(clientaddr);
int iSendResult;
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
//初始化WinSock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
//聲明IPV4地址族,流式套接字,UDP協議
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
//解析服務器地址和端口號
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
//爲無連接的服務器創建套接字
ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ServerSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
//爲監聽套接字綁定本地地址和端口號
iResult = bind(ServerSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ServerSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
printf("UDP server starting\n");
ZeroMemory(&clientaddr, sizeof(clientaddr));
//recvfrom函數直接在參數中指定接收數據的源地址
iResult = recvfrom(ServerSocket, recvbuf, recvbuflen, 0, (SOCKADDR*)&clientaddr, &clientlen);
if (iResult > 0) {
//情況1:成功接收到數據
printf("Bytes received: %d\n", iResult);
//將緩衝區的內容回送給客戶端
//sendto函數也是同理,在參數中指定數據要發送到的目的地址
iSendResult = sendto(ServerSocket, recvbuf, iResult, 0, (SOCKADDR*)&clientaddr, clientlen);
if (iSendResult == SOCKET_ERROR){
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ServerSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iSendResult);
}
else if (iResult == 0)
//情況2:關閉連接
printf("Connection closing...\n");
else {
//情況3:接收發生錯誤
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(ServerSocket);
WSACleanup();
return 1;
}
//關閉套接字
closesocket(ServerSocket);
//釋放資源
WSACleanup();
system("pause");
return 0;
}