一、UDP簡介
UDP(User Datagram Protocol),用戶數據報協議,是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。UDP提供了無連接通信,且不對傳送數據包進行可靠性保證,適合於一次傳輸少量數據,UDP傳輸的可靠性由應用層負責。常用的UDP端口號有:
應用協議 | 端口號 |
---|---|
DNS | 53 |
TFTP | 69 |
SNMP | 161 |
UDP是無連接的、不可靠的數據報協議;既然他不可靠為什么還要用呢?其一:當應用程序使用廣播或多播時只能使用UDP協議;其二:由於他是無連接的,所以數據傳輸過程中延遲小、數據傳輸效率高,適合對可靠性要求不高的應用程序,或者可以保障可靠性的應用程序,如DNS、TFTP、SNMP等。因為UDP套接字是無連接的,如果一方的數據報丟失,那另一方將無限等待,解決辦法是設置一個超時。
UDP報頭由4個域組成,其中每個域各占用2個字節,具體如下:
可以看到,UDP其實就是在IP報文中添加了端口信息,使數據到達主機后送達至相應端口的應用程序。下面是通過wireshark抓的一個UDP數據包:
UDP服務器
與基於TCP的應用程序相同的是,基於UDP的服務器應用程序也被分配了公認端口或已注冊的端口。當上述應用程序或進程運行時,它們就會接受與所分配端口相匹配的數據。當UDP收到用於某個端口的數據報時,它就會按照應用程序的端口號將數據發送到相應的應用程序。
UDP客戶端
對於TCP而言,客戶端/服務器模式的通信初始化采用由客戶端應用程序向服務器進程請求數據的形式。而UDP客戶端進程則是從動態可用端口中隨機挑選一個端口號,用來作為會話的源端口。而目的端口通常都是分配到服務器進程的公認端口或已注冊的端口。
udp不一定要有服務端和客戶端
udp不一定要有服務端和客戶端,因為任何一端的程序完全不知道對方的IP,解決方案就是添加組播或廣播,接收到對方的數據包了你就知道對方的ip地址了,然后就可以傳輸了。所謂的服務器和客戶端本質是兩個客戶端,但當做認為是服務端和客戶端,這樣只是為了方便區分兩個客戶端以及便於理解。
UDP傳輸與IP傳輸的區別
UDP傳輸與IP傳輸非常類似,協議都是以數據包(datagram)的方式傳輸。那么,我們為什么不直接使用IP協議而要額外增加一個UDP協議呢? 一個重要的原因是IP協議中並沒有端口(port)的概念。IP協議進行的是IP地址到IP地址的傳輸,這意味者兩台計算機之間的對話。但每台計算機中需要有多個通信通道,並將多個通信通道分配給不同的進程使用。一個端口就代表了這樣的一個通信通道。UDP協議實現了端口,從而讓數據包可以在送到IP地址的基礎上,進一步可以送到某個端口。
二、端口與socket
端口(port)是伴隨着傳輸層誕生的概念。它可以將網絡層的IP通信分送到各個通信通道。UDP協議和TCP協議盡管在工作方式上有很大的不同,但它們都建立了從一個端口到另一個端口的通信。
隨着我們進入傳輸層,我們也可以調用操作系統中的API,來構建socket。Socket是操作系統提供的一個編程接口,它用來代表某個網絡通信。應用程序通過socket來調用系統內核中處理網絡協議的模塊,而這些內核模塊會負責具體的網絡協議的實施。這樣,我們可以讓內核來接收網絡協議的細節,而我們只需要提供所要傳輸的內容就可以了,內核會幫我們控制格式,並進一步向底層封裝。因此,在實際應用中,我們並不需要知道具體怎么構成一個UDP包,而只需要提供相關信息(比如IP地址,比如端口號,比如所要傳輸的信息),操作系統內核會在傳輸之前會根據我們提供的相關信息構成一個合格的UDP包(以及下層的包和幀)。
注:上圖中的互相通信的兩個端口號可以相同,也可以不同,注意本地端口與目標端口的去吧。
三、UDP常用函數講解
1、socket函數
#include <sys/types.h>
#include <sys/socket.h>
//函數原型
int socket(int domain, int type, int protocol);
為了執行網絡輸入輸出,一個進程必須做的第一件事就是調用socket函數獲得一個文件描述符。
第一個參數指明了協議簇,目前支持5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議)。
第二個參數指明套接字類型,有三種類型可選:SOCK_STREAM(字節流套接口)、SOCK_DGRAM(數據報套接口)和SOCK_RAW(原始套接口);TCP一般選擇SOCK_STREAM,而UDP選擇SOCK_DGRAM。
2、bind函數
為套接字分配一個本地IP和本地端口,對於網際協議,協議地址是32位IPv4地址或128位IPv6地址與16位的TCP或UDP端口號的組合;如指定端口為0,調用bind時內核將選擇一個臨時端口,如果指定一個通配IP地址,則要等到建立連接后內核才選擇一個本地IP地址。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一個參數是socket函數返回的套接字的文件描述符。第二和第三個參數分別是一個指向特定於協議的地址結構的指針和該地址結構的長度。
sockaddr在頭文件#include <sys/socket.h>
中定義,sockaddr的缺陷是:sa_data把目標地址和端口信息混在一起了,如下:
struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14]; //14字節,包含套接字中的目標地址和端口信息
}
sockaddr_in在頭文件#include<netinet/in.h>或#include <arpa/inet.h>
中定義,該結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變量中,如下:
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port ; //16位 TCP/IP 端口號
struct in_addr sin_addr; //32位 IP 地址
char sin_zero[8]; //不使用
};
sockaddr常用於bind、connect、recvfrom、sendto等函數的參數,指明地址信息,是一種通用的套接字地址。 sockaddr_in 是internet環境下套接字的地址形式。
所以在網絡編程中我們會對sockaddr_in結構體進行操作,使用sockaddr_in來建立所需的信息,最后使用強制類型轉化就可以了。一般先把sockaddr_in變量賦值后,強制類型轉換后傳入用sockaddr做參數的函數:sockaddr_in用於socket定義和賦值;sockaddr用於函數參數。
二者長度一樣,都是16個字節,即占用的內存大小是一致的,因此可以互相轉化。二者是並列結構,指向sockaddr_in結構的指針也可以指向sockaddr。
若套接字使用了bind函數,則綁定的端口號,即為該套接字的本地端口。
注:sin_addr賦值htonl(INADDR_ANY)
,則綁定的IP地址即任何主機上的地址。
3、recvfrom函數
UDP使用recvfrom()函數接收數據,他類似於標准的read(),但是在recvfrom()函數中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
//返回發送數據的長度 -非0成功 -1-失敗
第一個參數為一個綁定本地IP和本地端口的套接字的文件描述符(接收方),第二個參數為接收數據緩沖區,第三個參數為緩沖區長度,第四個參數flag是傳輸控制標志。第五個參數是表示發送方IP和發送方端口的sockaddr_in,最后一個參數為發送方sockaddr_in的長度。
4、sendto函數
UDP使用sendto()函數發送數據,他類似於標准的write(),但是在sendto()函數中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
//返回發送數據的長度 -非0成功 -1-失敗
第一個參數為一個綁定本地IP和本地端口的套接字的文件描述符(發送方),第二個參數為接收數據緩沖區,第三個參數為緩沖區長度,flags參數是傳輸控制標志。參數dest_addr指明數據將發往的目標地址和目標端口(接收方),他的大小由addrlen參數來指定。
四、參考程序
使用UDP套接字編程可以實現基於TCP/IP協議的面向無連接的通信,它分為服務器端和客戶端兩部分,其主要實現過程如下圖所示:
UDP服務器程序
UDP編程的服務器端一般步驟是:
(1)使用 socket() 來建立一個UDP socket。 (2)初始化 sockaddr_in 結構的變量,並賦值。 (3)使用 bind() 把上面的socket和定義的IP地址和端口綁定。這里檢查 bind() 是否執行成功,如果有錯誤就退出。這樣可以防止服務程序重復運行的問題。 (4)進入無限循環程序,使用recvfrom()進入等待狀態,直到接收到客戶端發送的數據,就處理收到的數據,並向客戶端發送反饋。 |
Linux下UDP服務器套接字程序,服務器接收客戶端發送的信息並顯示,同時顯示客戶的IP地址、端口號,並向客戶端發送信息。如果服務器接收的客戶信息為“bye”,則退出循環,並關閉套接字。
/* udpserver.c */
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define PORT 31188 //端口號
#define MAXDATASIZE 100
int main()
{
int sockfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t addrlen;
int num;
char buf[MAXDATASIZE];
//使用 socket() 來建立一個UDP socket
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("Creatingsocket failed.");
exit(1);
}
//初始化 sockaddr_in 結構的變量,並賦值
bzero(&server,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(PORT); //服務器本地端口為31188
server.sin_addr.s_addr= htonl (INADDR_ANY);
//使用 bind() 把上面的socket和定義的IP地址和端口綁定。
if(bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{
perror("Bind()error.");
exit(1);
}
addrlen=sizeof(client);
//進入無限循環程序,使用recvfrom()進入等待狀態,直到接收到客戶端發送的數據
while(1)
{
num =recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr*)&client,&addrlen);
//處理收到的數據
if (num < 0)
{
perror("recvfrom() error\n");
exit(1);
}
buf[num] = '\0';
printf("You got a message (%s%) from client.\nIt's ip is%s, port is %d.\n",buf,inet_ntoa(client.sin_addr),htons(client.sin_port));
//服務器本地端口為31188,並向目標端口31188(客戶端端口)發送反饋
sendto(sockfd,"Welcometo my server.\n",22,0,(struct sockaddr *)&client,addrlen);
if(!strcmp(buf,"bye"))
break;
}
close(sockfd);
}
//執行命令./ udpserver,觀察結果
UDP客戶端程序
UDP編程的客戶端端一般步驟是:
(1)使用 socket() 來建立一個UDP socket。 (2)初始化 sockaddr_in 結構的變量,並賦值。 (3)使用 bind() 把上面的socket和定義的IP地址和端口綁定。也可以使用connect()來建立與服務程序的連接。 (4)發送數據,用函數sendto()。如果使用了連接的UDP,要使用write()來替代sendto()。 |
Linux下UDP服務器套接字程序,服務器接收客戶端發送的信息並顯示,同時顯示客戶的IP地址、端口號,並向客戶端發送信息。如果服務器接收的客戶信息為“bye”,則退出循環,並關閉套接字。
/* udpclient.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 31188 //端口號
#define MAXDATASIZE 100
int main(int argc, char *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE];
struct hostent *he; //終端輸入IP地址
struct sockaddr_in server,peer;
if (argc !=3)
{
printf("Usage: %s <IP Address><message>\n",argv[0]);
exit(1);
}
//獲得argv[1]-終端輸入IP地址
if ((he=gethostbyname(argv[1]))==NULL)
{
printf("gethostbyname()error\n");
exit(1);
}
//使用 socket() 來建立一個UDP socket
if ((sockfd=socket(AF_INET, SOCK_DGRAM,0))==-1)
{
printf("socket() error\n");
exit(1);
}
//初始化 sockaddr_in 結構的變量,並賦值
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT); //客戶端本地端口為31188
server.sin_addr= *((struct in_addr *)he->h_addr);
sendto(sockfd, argv[2],strlen(argv[2]),0,(struct sockaddr *)&server,sizeof(server));
socklen_t addrlen;
addrlen=sizeof(server);
//進入無限循環程序,使用recvfrom()進入等待狀態,直到接收到客戶端發送的數據
while (1)
{
//客戶端本地端口為31188(即服務器發送的目標端口),接收來自服務器的反饋
if((num=recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr *)&peer,&addrlen))== -1)
{
printf("recvfrom() error\n");
exit(1);
}
//處理收到的數據
if (addrlen != sizeof(server) ||memcmp((const void *)&server, (const void *)&peer,addrlen) != 0)
{
printf("Receive message from otherserver.\n");
continue;
}
buf[num]='\0';
printf("Server Message:%s\n",buf);
break;
}
close(sockfd);
}
//執行命令./ udpclient 127.0.0.1 hello
實驗結果
服務器端:
客戶端:
注:發送方的發送函數的目標端口要求是接收方(接收函數)的本地端口。
參考:**