下面通过信件说明UDP的工作原理。寄信前先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。
无法确认对方是否收到信件,并且在邮寄过程中可能发生信件丢失的情况。也就是说,UDP是不可靠的数据传输服务。
TCP为了提供可靠的数据传输服务,在不可靠的IP层进行流控制,所以TCP比UDP可靠,但UDP在结构上比TCP更简洁。
流控制是区分UDP和TCP的最重要的标志。
IP的作用就是让离开主机B的UDP数据包准确传递到主机A。UDP最重要的作用就是根据端口号将传递到主机的数据包交付给最终的UDP套接字。
UDP具有一定的可靠性。网络传输特性导致信息丢失频发,可若要传输压缩文件(发送1W个数据包,丢失一个就会产生问题),则必须使用TCP。对于多媒体数据而言,丢失一部分没问题,只会引起短暂的画面抖动,或细微的杂音。
TCP比UDP慢的原因通常有以下两点:
-收发数据前后进行的连接设置及清除过程。
-收发数据过程中为保证可靠性而添加的流控制。
UDP只有创建套接字的过程和数据交换过程。不必调用TCP连接过程中调用的listen函数和accept函数。
TCP中套接字之间是一对一的关系。但在UDP中,不管服务器端还是客户端都只需要一个套接字。
(收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有一个邮筒,就可以通过它向任意地址寄出信件。
下图展示1个UDP套接字和2个不同的主机数据交换的过程:
UDP套接字每次传输数据都要添加目标地址信息,相当于寄信前在信件中填写地址。
填写地址并传输数据时调用的函数:
接受UDP数据的函数:
服务器端:
/* UDP回声服务器端uecho_server.c */
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2) {
printf("Usage : %s \n",argv[0]);
exit(1);
}
serv_sock = socket(PF_INET,SOCK_DGRAM,0); //创建UDP套接字
if (serv_sock == -1)
error_handling("UDP socket() error!");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock,(struct sockaddr*) &serv_adr,sizeof(serv_adr)) == -1)
error_handling("bind() error!");
while(1) {
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock,message,BUF_SIZE,0,(struct sockaddr*) &clnt_adr,&clnt_adr_sz);
sendto(serv_sock,message,str_len,0,(struct sockaddr*) &clnt_adr,clnt_adr_sz);
//通过47行的函数调用同时获取数据传输端的地址,并将接受的数据传会该地址
}
close(serv_sock);
return 0;
}
客户端:
/* UDP回声客户端 */
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr,from_adr;
if(argc != 3) {
printf("Usage : %s \n",argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_DGRAM,0);
if (sock == -1)
error_handling("socket() error!");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
while (1) {
fputs("Input message(Q to quit): ",stdout);
fgets(message,sizeof(message),stdin);
if (!strcmp(message,"q\n") || !strcmp(message,"Q\n")) // 输入q/Q退出
break;
sendto(sock,message,strlen(message),0,(struct sockaddr*) &serv_adr,sizeof(serv_adr));
// 首次sendto函数时给相应套接字自动分配IP和端口,分配的地址一直保存到程序结束为止
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*) &from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
}
UDP客户端缺少把IP和端口分配给套接字的过程。TCP客户端是调用connect函数自动完成此过程。那UDP中何时分配IP和端口号?
UDP程序中,调用sendto函数传输数据前完成套接字的地址分配工作,因此调用bind函数。如果调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。而且分配的地址一直保存到程序结束。
综上所述,调用sendto函数时自动分配IP和端口号,UDP客户端无需额外的地址分配过程。
UDP是具有数据边界的协议,传输中调用I/O函数的次数非常重要。输入函数的调用次数和输出函数的调用次数完全一致,才能保证接收全部已发送数据。例如调用3次输出函数发送的数据必须调用3次输入函数才能接受完。
示例:
host1(服务器端):
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len , i ;
if (argc != 2) {
printf("Usage : %s \n",argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_DGRAM,0);
if (sock == -1)
error_handling("socket() error!");
memset(&my_adr,0,sizeof(my_adr));
my_adr.sin_family = AF_INET;
my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
my_adr.sin_port = htons(atoi(argv[1]));
if(bind(sock,(struct sockaddr*) &my_adr,sizeof(my_adr)) == -1)
error_handling("bind() error!");
for ( i = 0; i < 3; i++ ) //循环3次,接收3次数据
{
sleep(5); //delay 5 sec
adr_sz = sizeof(your_adr);
str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*) &your_adr, &adr_sz);
printf("Message %d: %s \n", i+1,message);
}
close(sock);
return 0;
}
host2(客户端):
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
int sock;
char msg1[] = "Hi!";
char msg2[] = "I'm another UDP host!";
char msg3[] = "Nice to meet you";
struct sockaddr_in your_adr;
socklen_t your_adr_sz;
if (argc != 3) {
printf("Usage : %s \n",argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_DGRAM,0);
if (sock == -1)
error_handling("socket() error!");
memset(&your_adr,0,sizeof(your_adr));
your_adr.sin_family = AF_INET;
your_adr.sin_addr.s_addr = inet_addr(argv[1]);
your_adr.sin_port = htons(atoi(argv[2]));
/* 分三次传送数据 */
sendto(sock,msg1,sizeof(msg1),0,(struct sockaddr*) &your_adr,sizeof(your_adr));
sendto(sock,msg2,sizeof(msg2),0,(struct sockaddr*) &your_adr,sizeof(your_adr));
sendto(sock,msg3,sizeof(msg3),0,(struct sockaddr*) &your_adr,sizeof(your_adr));
close(sock);
return 0;
}
UDP通信过程中使I/O函数调用次数保持一致
通过sendto函数传输数据的过程大致可分为以下3个阶段:
-向UDP套接字注册目标IP和端口号
-传输数据
-删除UDP套接字中注册的目标地址信息
每次调用sendto函数时重复上述过程,每次都变更目标地址。因此可以重复利用同一UDP套接字向不同目标传输数据。
UDP这种未注册目标地址信息的套接字称为未连接套接字。反之,注册了目标地址的套接字称为连接connected套接字。
当向同一个端口多次传输数据时,就会多次重复以上过程。因此,要与同一主机进行长时间通信时,将UDP套接字变成已连接套接字会提高效率。
创建已连接UDP套接字的过程很简单,针对UDP套接字调用connect函数
sock = socket(PF_INET,SOCK_DGRAM,0);
memset(&adr,0,sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = ....
adr.sin_port = ....
connect(sock,(struct sockaddr*) &adr, sizeof(adr));
与TCP套接字创建过程一致,只是socket函数第二个参数变为SOCK_DGRAM。
之后就与TCP套接字一样,每次调用sendto函数时只需传输数据,因为已经指定了收发对象。
此时也可以使用write,read函数进行通信。
下面将之前的uecho.client.c改为基于已连接UDP套接字的程序。
/* UDP已连接客户端 */
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr; //不需要from_adr
if(argc != 3) {
printf("Usage : %s \n",argv[0]);
exit(1);
}
sock = socket(PF_INET,SOCK_DGRAM,0);
if (sock == -1)
error_handling("socket() error!");
memset(&serv_adr,0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sock,(struct sockaddr*) &serv_adr,sizeof(serv_adr));
while (1) {
fputs("Input message(Q to quit): ",stdout);
fgets(message,sizeof(message),stdin);
if (!strcmp(message,"q\n") || !strcmp(message,"Q\n")) // 输入q/Q退出
break;
/*
sendto(sock,message,strlen(message),0,(struct sockaddr*) &serv_adr,sizeof(serv_adr));
*/
write(sock,message,strlen(message)); //可以使用write传输数据
/*
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*) &from_adr, &adr_sz);
*/
str_len = read(sock,message,sizeof(message)-1); //可以使用read读取数据
message[str_len] = 0;
printf("Message from server: %s",message);
}
close(sock);
return 0;
}