相對於TCP 協議的程序設計, UDP 協議的程序雖然程序設計的環節要少一些,但是由於UDP 協議缺少流量控制等機制,容易出現一些難以解決的問題。UDP 的報文丟失、報文亂序、connect()函數、流量控制、外出網絡接口的選擇等是比較容易出現的問題。
1 UDP 報文丟失數據
利用UDP 協議進行數據收發的時候,在局域網內一般情況下數據的接收方均能接收到發送方的數據,除非連接雙方的主機發生故障, 否則不會發生接收不到數據的情況。
1. UDP報文的正常發送過程
而在Internet 上,由於要經過多個路由器, 正常情況下一個數據報文從主機C 經過路由器A、路由器B 、路由器C 到達主機S, 數據報文的路徑如圖所示。主機C 使用函數sendto()發送數據,主機S 使用recvfrom()函數接收數據,主機S 在沒有數據到來的時候,會一直阻塞等待。
2. UDP報文的丟失
路由器要對轉發的數據進行存儲、處理、合法性判定、轉發等操作,容易出現錯誤,所以很可能在路由器轉發的過程中出現數據丟失的現象,如圖所示。當UDP 的數據報文丟失的時候,函數recvfrom()會一直阻塞,直到數據到來。
如果客戶端發送的數據丟失,服務器會一直等待直到客戶端合法數據到來;如果服務器的響應在中間被路由器丟棄,則客戶端會一直阻塞,直到服務器數據的到來。在程序正常運行的過程中是不允許出現這種清況的,所以可以設置超時時間來判斷是否有數據到來。對千數據丟失的原因,並不能通過一種簡單的方法獲得,例如,不能區分服務器發給客戶端的響應數據是在發送的路徑中被路由器丟棄,還是服務器沒有發送此響應數據。
3. UDP報文丟失的對策
UDP 協議中的數據報文丟失是先天性的,因爲UDP 是無連接的、不能保證發送數據的正確到達。下圖爲TCP 的連接中發送數據報文的過程, 主機C 發送的數據經過路由器,到達主機S 後,主機S 要發送一個接收到此數據報文的響應,主機C 要對主機S 的響應進行記錄,直到之前發送的數據報文l 已經被主機S 接收到。如果數據報文在經過路由器的時候, 被路由器丟棄,則主機C 和主機S 會對超時的數據進行重發。
當我們重傳的時候,主機S可能已經收到了,這樣就會收到一樣的數據,這裏可以用一個序列號表示,搜到相同序列號的數據就丟掉。
2 UDP 數據發送中的亂序
UDP 協議數據收發過程中,會出現數據的亂序現象。所謂亂序是發送數據的順序和接收數據的順序不一致,例如發送數據的順序爲數據包A、數據包B 、數據包C, 而接收數據包的順序變成了數據包B、數據包A、數據包C 。
1. UDP數據順序收發的過程
如圖所示, 主機C 向主機S 發送數據包0、數據包1 、數據包2、數據包3, 各個數據包途中經過路由器A、路由器B、路由器c, 先後到達主機s, 在主機S 端的循序仍然爲數據包0、數據包1 、數據包2 、數據包3, 即發送數據時的順序和接收數據時的順序是一致的。
2. UDP數據的亂序
UDP 的數據包在網絡上傳輸的時候,有可能造成數據的順序更改, 接收方的數據順序和發送方的數據順序發生了顛倒。這主要是由千路由的不同和路由的存儲轉發的順序不同造成的。
路由器的存儲轉發可能造成數據順序的更改,如圖所示。主機C 發送的數據在經過路由器A 和路由器C 的時候,順序均沒有發生順序更改。而在經過主機B 的時候,數據的順序由數據0123 變爲了0312, 這樣主機C 的數據0123 順序經過路由器到達主機S的時候變爲了數據0312 。
UDP 協議的數據經過路由器時的路徑造成了發送數據的混亂,如圖所示。從主機C 發送的數據0123, 其中數據0 和3 經過路由器B、路由器C 到達主機s, 數據1 和數據2 經過路由器A 、路由器C 到達主機s, 所以數據由發送時的順序0123 變成了順序1032 。
3. UDP亂序的對策
對千亂序的解決方法可以採用發送端在數據段中加入數據報序號的方法,這樣接收端對接收到數據的頭端進行簡單地處理就可以重新獲得原始順序的數據,如圖所示。
3. UDP 協議中的connect()函數
UDP 協議的套接字描述符在進行了數據收發之後,才能確定套接字描述符中所表示的發送方或者接收方的地址,否則僅能確定本地的地址。例如客戶端的套接字描述符在發送數據之前, 只要確定建立正確就可以了, 在發送的時候才確定發送目的方的地址: 服務器bind()函數也僅僅綁定了本地進行接收的地址和端口。
connect()函數在TCP 協議中會發生三次握手,建立一個持續的連接, 一般不用於UDP 。在UDP 協議中使用connect()函數的作用僅僅表示確定了另一方的地址,並沒有其他的含義。
connect()函數在UDP 協議中使用後會產生如下的副作用:
- 使用connect()函數綁定套接字後,發送操作不能再使用sendto()函數,要使用write()函數直接操作套接字文件描述符,不在指定目的地址和端口號。
- 使用connect()函數綁定套接字後, 接收操作不能再使用recvfrom()函數, 要使用read()類的函數,函數不會返回發送方的地址和端口號。
- 在使用多次connect()函數的時候,會改變原來套接字綁定的目的地址和端口號,用新綁定的地址和端口號代替,原有的綁定狀態會失效。可以使用這種特定來斷開原來的連接。
下面是一個使用connect()函數的例子, 在發送數據之前, 將套接字文件描述符與目的地址使用connect() 函數進行了綁定, 之後使用write()函數發送數據並使用read()函數接收數據。
static void udpclie_echo(int s, struct sockaddr*to)
{
char buff[BUFF_LEN] = "UDP TEST";//向服務器端發送的數據可
connect(s, to, sizeof(*to));//連接
n = write(s, buff, BUFF_LEN);//發送數據
read(s, buff, n);//接收數據
}
4 UDP 缺乏流量控制
UDP 協議沒有TCP 協議所具有的滑動窗口概念,接收數據的時候直接將數據放到緩衝區中。如果用戶沒有及時地從緩衝區中將數據複製出來,後面到來的數據會接着向緩衝區中放入。當緩衝區滿的時候, 後面到來的數據會覆蓋之前的數據造成數據的丟失。
1. UDP缺乏流量控制概念
如圖所示爲UDP 的接收緩衝區示意圖,共有8 個緩衝區, 構成一個環狀數據緩衝區。起點爲0 。
當接收到數據後, 會將數據順序放入之前數據的後面,並逐步遞增緩衝區的序號, 如圖。
當數據沒有接收或者接收數據比發送數據的速率要慢,之前接收的數據被覆蓋,造成數據的丟失,如圖所示。
2. 緩衝區溢出對策
解決UDP 接收緩衝區溢出的現象需要根據實際情況確定, 一般可以用增大接收數據緩衝區和接收方接收單獨處理的方法來解決局部的UDP 數據接收緩衝區溢出問題。例如在局部時間內發送方會爆發性地發送大量的數據,後面的時間則發送的數據會較小,由千在局部時間內接收方不能及時處理接收到的數據,會造成數據的丟失,如果增大緩衝區,則可以改善此問題。如果接收方的接收能力在絕對能力上要小於發送方,則接收方由千在處理能力或者容量方面的限制,造成數據肯定要丟失。
客戶端的代碼如下 ,先將發送計數的值打包進發送緩衝區,然後複製要發送的數據,再進行數據發送。每次發送的時候,計數器增加 1 。
#define PORT_SERV 8888 //服務器端口
#define NUM_DATA 100 //接收緩衝區數量
#define LENGTH 1024 //單個接收緩衝區大小
static char buff_send[LENGTH]; //接收緩衝區
static void udpclie_echo(int s, struct sockaddr*to)
{
char buff_init[BUFF_LEN] = "UDP TEST";//向服務器端發送的數據
struct sockaddr_in from; //發送數據的主機地址
int len = sizeof (*to); //地址長度
int i = 0;//計數
for(i = 0; i < NUM_DATA; i++) //循環發送
{
*((int*)&buff_send[0]) = htonl(i);//將數據標記打包
memcpy(&buff_send[4], buff_init, sizeof(buff_init));
//數據複製到發送緩衝區
sendto(s, &buff_send[0], NUM_DATA, 0 , to, len);//發送數據
}
}
服務器端的代碼如下,接收到發送方的數據後,判斷接收到數據的計數器的值,將不同計數器的值放入緩衝區不同的位咒,在使用的時候可以判斷一下計數器是否正確,即是否有數據到來,再進行使用。
#define PORT SERV 8888 /*服務器端口*/
#define NUM DATA 100 /*接收緩衝區數量*/
#define LENGTH 1024 /*單個接收緩衝區大小*/
static char buff [NUM DATA][LENGTH];/*接收緩衝區*/
static udpserv_echo(int s, struct sockaddr*client)
{
int n; /*接收數量*/
char tmp_buff[LENGTH]; /*臨時緩衝區*/
int len; /*地址長度*/
while (1)/*接收過程*/
{
len = sizeof (*client);
n = recvfrom(s, tmp_buff, LENGTH, 0, client, &len);
/*接收數據放到臨時緩衝區中*/
//根據接收到數據的頭部標誌,選擇合適的緩衝區位置複製數據
memcpy (&buff[ntohl(*( (int*) &buff[i][0]))][0], tmp_buff+4, n-4);
}
}
5 UDP 協議中的外出網絡接口
在網絡程序設計的時候,有時需要設置一些特定的條件。 例如一個主機有兩個網卡 ,由千不同的網卡連接不同的子網,用戶發送的數據從其中的一個網卡發出,將數據發送到特定的子網上。使用函數 connect()可以將套接字文件描述符與一個網絡地址結構進行綁定,
在地址結構中所設置的值是發送接收數據時套接字採用的 IP 地址和端口。下面的代碼是一個例子:
#include <sys/types.h>
#include <sys/socket.h>
#include <Linux/in.h>
#include <string.h>
#include <stdio.h>
#define PORT_SERV 8888
int main(int argc, char•argv[])
{
int s;//套接字文件描述符
struct sockaddr in addr serv;//服務器地址
struct sockaddr_in local;//本地地址
int len = sizeof(local);
s = socket(AF_INET, SOCK_DGRAM, 0);
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_addr.s_addr = inet_addr("l27.0.0.l" );
addr_serv.sin_port = htons(PORT_SERV);//服務器端口
connect(s, (struct sockaddr*) &addr_serv, sizeof(addr_serv));
//連接服務器
getsockname(s, &local, &len);//獲得套接字文件描述符的地址
printf("UDP local addr:%s\n", inet_ntoa(local.sin_addr));
close(s);
return 0;
}
編譯運行後其結果如下,系統將程序中的套接字描述符與本地的迴環接口進行了綁定。
UDP local addr : 127.0. 0 .1
6 UDP 協議中的數據報文截斷
當使用 UDP 協議接收數據的時候,如果應用程序傳入的接收緩衝區的大小小千到來數據的大小時,接收緩衝區會保存最大可能接收到的數據,其他的數據將會丟失,並且有MSG_TRUNC 的標誌 。
例如對函數 udpclie_echo()做如下修改,發送一個字符串後在一個循環中接收服務器端的響應,會發現只能接收—個 u, 程序阻塞到 recvfrom 函數中。這是因爲服務器發送的字符串到達客戶端後,客戶端的第一次接收動作沒有正確地接收到全部的數據,其餘的數據已經丟失了 。
所以服務器和客戶端的程序要進行配合,接收的緩衝區要比發送的數據大一些,防止數據丟失的現象發生 。