第六章:基于UDP的服务器端/客户端
6、1 理解UDP:
-
数据交换过程可以分为通过TCP套接字完成的TCP方式和通过UDP套接字完成的UDP方式。
-
UDP套接字的特点:
- UDP提供的是不可靠的数据传输服务。无法确认对方是否收到,也有可能发生数据丢失的情况。
- UDP结构上比TCP更简洁。UDP不会发送ACK的应答信息,也不会像SEQ那样给数据分配序号。UDP的性能比TCP高出很多。在更重视性能而非可靠性的情况下,UDP是一种很好的选择。
- 流控制是区分UDP和TCP的最重要标志。TCP的生命在于流控制。
- TCP的速度无法超过UDP,在交换的数据量越大,TCP的传输速率就越接近UDP的传输速率。
-
UDP内部工作原理:
- IP的作用就是让离开主机B的UDP数据包准确的传递到主机A。把UDP包最终交给主机A的某一UDP套接字的过程则是由UDP完成的。UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字。
- TCP比UDP慢的原因:
- 收发数据前后进行的连接设置及清除过程。
- 收发数据过程中为保证可靠性而添加的流控制。
- 收发数据量小但需要频繁连接时,UDP比TCP更高效。建议深入学习TCP/IP协议的内部构造。
6、2 实现基于UDP的服务器端/客户端
-
UDP中的服务器端和客户端没有连接:
- UDP无需经过连接过程,不必调用TCP连接过程中调用的listen函数和accept函数,UDP中只有创建套接字的过程和数据交换过程。
-
UDP服务器端和客户端均只需1个套接字:
-
TCP中,套接字之间是一一对应的,要向10个客户端提供服务,除了守门的服务器套接字,还需要10个服务器端套接字,UDP中,服务器端和客户端都只需要1个套接字。只需要1个UDP套接字就可以向任意主机传输数据。
-
图示展示了1个UDP套接字和2个不同主机交换数据的过程,只需要1个UDP套接字就能和多台主机通信。
-
-
基于UDP的数据I/O函数:
-
创建好TCP套接字后,传输数据无需添加地址信息,TCP套接字会保持与对方套接字的连接,TCP套接字知道目标地址的信息。
-
UDP套接字不会保持连接状态,每次传输数据都要添加目标地址信息。
-
填写地址并传输数据时调用的UDP相关函数:
-
#include <sys/socket.h> // 成功返回传输的字节数,失败返回-1 ssize_t sendto(int sock, void *buffer, size_t nbytes, int flags, struct sockaddr*to,socklen_t addrlen);
- sock:用于传输数据的UDP套接字文件描述符
- buffer:保存待传输数据的缓冲地址值
- nbytes:待传输的数据长度,以字节为单位
- flags:可选参数,没有则传递0
- to:存有目标地址信息的sockaddr结构体变量的地址值
- addrlen:传递给参数to的地址结构体变量长度。
- 与TCP输出函数的区别在于:函数sendto()需要向他传递目标信息。
-
#include <sys/socket.h> // 成功返回接收的字节数,失败返回-1 ssize_t recvfrom(int sock, void *buffer, size_t nbytes,int flags, struct sockaddr* from,socklen_t *addrlen);
- sock:用于接收数据的UDP套接字文件描述符
- buffer:保存接收数据的缓冲地址值
- nbytes:可接收的最大字节数,无法超过参数buffer所指的缓冲大小
- flags:可选项参数,没有传0
- from:存有发送端地址信息的sockaddr结构体变量的地址值
- addrlen:保存参数from的结构体变量长度的变量地址值
-
-
-
基于UDP的回声服务器端/客户端:
-
UDP不同于TCP,不存在请求连接和受理过程,某种意义上无法明确区分服务器端和客户端。
-
服务器端:提供服务
-
#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); int main(int argc, char argv[]){ int serv_sock; char message[BUF_SIZE]; int str_len; socklen_t clnt_adr_sz; struct sockaddr_in serv_adr,clnt_adr; if(argc!= 2){ printf("Usage : %s <port>\n",argv[0]); exit(1); } // 创建UDP套接字,参数传递SOCK_DGRAM serv_sock = socket(PF_INET,SOCK_DGRAM,0); if(serv_sock == -1) error_handling("UDP socker creation 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])); // 分配IP地址和端口号 if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1) error_handling("bind() error"); while(1){ clnt_adr_sz = sizeof(clnt_adr); // recvfrom函数调用获取数据传输端的地址,利用该地址将接收的数据逆向重传 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); } close(serv_sock); return 0; } void error_handling(char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1); }
-
-
客户端:
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(const char* message); 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_STREAM,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("Iput message(Q to quit): ",stdout); fgets(message,BUF_SIZE,stdin); if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) break; // 向服务器传输数据 sendto(sock,message,strlen(message),0,(struct sockaddr*)&from_adr,sizeof(serv_adr)); 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; } void error_handling(const char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1); }
-
-
-
UDP客户端套接字的地址分配:
- TCP客户端通过调用connect函数自动完成IP和端口的分配。
- 调用sento函数传输数据前应完成对套接字的地址分配工作,所以调用bind函数,bind函数在TCP程序中出现过,bind函数不区分TCP和UDP,在UDP程序中同样可以调用,
- 如果在调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。此时分配的地址一直保留到程序结束为止。也可以用来与其他UDP套接字进行数据交换,IP用主机IP,端口选尚未使用的任意端口号。
- 调用sendto函数时自动分配IP和端口号,UDP客户端通常无需额外的地址分配过程。
6、3 UDP的数据传输特性和调用connect函数:
-
验证UDP数据传输中存在数据边界。
-
存在数据边界的UDP套接字:
-
TCP数据传输中不存在边界,表示:“数据传输过程中调用I/O函数的次数不具有意义。”
-
UDP是具有数据边界的协议,传输中调用I/O函数的次数很重要,输入函数的调用次数应和输出函数的调用次数完全一致,这样才能保证接收全部已发送数据。
-
bound_host1.c:调用3次recvfrom函数以接收数据,recvfrom函数调用间隔为5秒。
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define BUF_SIZE 30 void error_handling(char* message); int main(int argc, char* argv[]){ int sock; struct sockaddr_in my_adr, your_adr; socklen_t adr_sz; int str_len, i; char message[BUF_SIZE]; sock = socket(PF_INET,SOCK_DGRAM,0); if(sock == -1) error_handling("socket creation error"); memset(&my_adr, 9,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++){ // 程序停顿时间等于传递来的时间,for循环中每隔5秒调用1次recvfrom函数 sleep(5); 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; } void error_handling(char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1); }
-
-
bound_host2.c:调用3次sendto函数,由于bound_host1.c中调用了sleep5秒,所以调用recvfrom函数前就已经调用了3次sendto函数,此时数据已经传输到了bound_host1.c,如果是TCP程序,只需要调用1次输入函数即可读入数据。UDP则需要调用3次recvfrom函数。
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #define BUF_SIZE 30 void error_handling(char* message); 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; } void error_handling(char* message){ fputs(message,stderr); fputc('\n',stderr); exit(1); }
-
-
+ UDP数据报(Datagram):UDP套接字传输的数据包又称为数据报,数据报也属于数据包的一种,与TCP包不同,其本身可以成为1个完整数据,UDP中存在数据边界,1个数据包即可成为1个完整的数据,所以称为数据报。
-
已连接(connected)UDP套接字与未连接(unconnected)UDP套接字:
- TCP套接字中需注册待传输数据的目标IP和端口号,而UDP中无需注册。所以通过sendto函数传输数据的过程大致如下:
- 第一阶段:向UDP套接字注册目标IP和端口号
- 第二阶段:传输数据
- 第三阶段:删除UDP套接字中注册的目标地址信息。
- 每次调用sendto函数时重读上述过程,每次都变更目标地址,所以可以重复利用同一UDP套接字向不同目标传输数据,这种未注册目标地址信息的套接字称为未连接套接字,注册了目标地址的套接字称为连接套接字,UDP默认属于未连接套接字。
- 如果要与同一主机进行长时间的通信,将UDP套接字变为已连接的套接字会提高效率。
- TCP套接字中需注册待传输数据的目标IP和端口号,而UDP中无需注册。所以通过sendto函数传输数据的过程大致如下:
-
创建已连接UDP套接字
-
创建已连接的UDP套接字只需要针对UDP套接字调用connect函数。
-
#include <sys/socket.h> sock = socket(PF_INET,SOCK_DGRAM,0); memset(&adr,0,sizeof(adr)); adr.sin_family = AF_INET; adr.sin_addr.s_addr = htonl(INADDR_ANY); adr.sin_port = htons(atoi(argv[1])); connect(sock,(struct sockaddr*)&adr,sizeof(adr));
-
代码看似与TCP套接字创建过程一致,但socket函数的第二个参数是SOCK_DGRAM,即创建的是UDP套接字。针对UDP套接字调用connect函数不意味着与对方UDP套接字连接,只是向UDP套接字注册目标IP和端口信息。
-
之后就与TCP套接字一样,每次调用sento函数只需要传输数据,因为已经指定了收发对象,不仅可以使用sendto、recvfrom函数,还可以使用write、read函数进行通信。
-