理解UDP
UDP套接字的特点
下面通过信件说明UDP的工作原理。寄信前先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。
无法确认对方是否收到信件,并且在邮寄过程中可能发生信件丢失的情况。也就是说,UDP是不可靠的数据传输服务。
TCP为了提供可靠的数据传输服务,在不可靠的IP层进行流控制,所以TCP比UDP可靠,但UDP在结构上比TCP更简洁。
流控制是区分UDP和TCP的最重要的标志。
UDP内部工作原理
IP的作用就是让离开主机B的UDP数据包准确传递到主机A。UDP最重要的作用就是根据端口号将传递到主机的数据包交付给最终的UDP套接字。
UDP的高效使用
UDP具有一定的可靠性。网络传输特性导致信息丢失频发,可若要传输压缩文件(发送1W个数据包,丢失一个就会产生问题),则必须使用TCP。对于多媒体数据而言,丢失一部分没问题,只会引起短暂的画面抖动,或细微的杂音。
TCP比UDP慢的原因通常有以下两点:
-收发数据前后进行的连接设置及清除过程。
-收发数据过程中为保证可靠性而添加的流控制。
实现基于UDP的服务器端/客户端
UDP中的服务器和客户端没有连接
UDP只有创建套接字的过程和数据交换过程。不必调用TCP连接过程中调用的listen函数和accept函数。
UDP服务器端和客户端均只需1个套接字
TCP中套接字之间是一对一的关系。但在UDP中,不管服务器端还是客户端都只需要一个套接字。
(收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有一个邮筒,就可以通过它向任意地址寄出信件。
下图展示1个UDP套接字和2个不同的主机数据交换的过程:
基于UDP的数据I/O函数
UDP套接字每次传输数据都要添加目标地址信息,相当于寄信前在信件中填写地址。
填写地址并传输数据时调用的函数:
接受UDP数据的函数:
基于UDP的回声服务器端/客户端
服务器端:
/* UDP回声服务器端uecho_server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #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 <port> \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<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #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 <IP> <port>\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客户端套接字的地址分配
UDP客户端缺少把IP和端口分配给套接字的过程。TCP客户端是调用connect函数自动完成此过程。那UDP中何时分配IP和端口号?
UDP程序中,调用sendto函数传输数据前完成套接字的地址分配工作,因此调用bind函数。如果调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。而且分配的地址一直保存到程序结束。
综上所述,调用sendto函数时自动分配IP和端口号,UDP客户端无需额外的地址分配过程。
UDP的数据传输特性和调用connect函数
存在数据边界的UDP套接字
UDP是具有数据边界的协议,传输中调用I/O函数的次数非常重要。输入函数的调用次数和输出函数的调用次数完全一致,才能保证接收全部已发送数据。例如调用3次输出函数发送的数据必须调用3次输入函数才能接受完。
示例:
host1(服务器端):
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #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 <port>\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<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #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 <IP> <port>\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函数调用次数保持一致
已连接(connected) UDP套接字与为连接UDP套接字
通过sendto函数传输数据的过程大致可分为以下3个阶段:
-向UDP套接字注册目标IP和端口号
-传输数据
-删除UDP套接字中注册的目标地址信息
每次调用sendto函数时重复上述过程,每次都变更目标地址。因此可以重复利用同一UDP套接字向不同目标传输数据。
UDP这种未注册目标地址信息的套接字称为未连接套接字。反之,注册了目标地址的套接字称为连接connected套接字。
当向同一个端口多次传输数据时,就会多次重复以上过程。因此,要与同一主机进行长时间通信时,将UDP套接字变成已连接套接字会提高效率。
创建已连接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<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #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 <IP> <port>\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; }