第六章 基于UDP的服务器端/客户端
6.1 理解UDP
TCP像电话一样可靠,UDP像寄信一样不可靠。
UDP可靠性略弱,但性能更强。
TCP为了提供可靠的数据传输服务,在不可靠的IP层进行流控制,而UDP缺少这种流控制机制。
每次交换的数据量越大,TCP的传输速率就越接近UDP的传输速率。
UDP内部工作原理
IP的作用:让离开主机B的UDP数据包准确传递给主机A
UDP的作用:根据端口号将传到主机的数据包交付给最终的UDP套接字
UDP的高效使用
UDP也是具有一定的可靠性的,不要认为它完全不可靠。
针对压缩数据包,丢一个包都可能导致最终无法解压缩成功,这种一般用TCP。
针对网络实时传输的视频、音频等多媒体数据,丢失一小部分没啥问题;但因为要提供实时的服务,速度就是非常重要的因素,此时需要用UDP。
TCP比UDP慢的原因:
- 收发数据前后进行的连接设置及清除过程
- 收发数据过程中为保证可靠性而添加的流控制
如果收发的数据量小,但需要频繁连接时,UDP比TCP更高效。
6.2 实现基于UDP的服务器端/客户端
UDP中的服务器端和客户端没有连接
无需listen和accept,只有创建套接字过程和数据交换过程。
UDP服务器端和客户端均只需1个套接字
TCP中套接字是一对一关系,除了守门的服务器套接字,还需要10个服务器端套接字。
UDP中,不管是服务器端还是客户端,都只需要一个套接字。
一个UDP套接字能和多态主机通信。只需要一个套接字就可以向任意主机传送数据。
基于UDP的数据I/O函数
UDP每次传输数据都要添加目标地址信息。
发送UDP数据的函数:
#include<sys/socket.h>
ssize_t sendto(int sock, void * buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);
sock 用于传输数据的UDP套接字文件描述符
buff 保存待传输数据的缓冲地址值
nbytes 待传输的数据长度,以字节为单位
flags 可选项参数,若没有则传递0
to 存有目标地址信息的sockaddr结构体变量的地址值
addrlen 传递给参数to的地址值结构体变量长度
成功时返回传输的字节数,失败时返回-1
接收UDP数据的函数:
#include<sys/socket.h>
ssize_t recvfrom(int sock, void * buff, size_t nbytes, int flags, struct sockaddr* from, socklen_t * addrlen);
sock 用于接收数据的UDP套接字文件描述符
buff 保存接收数据的缓冲地址值
nbytes 可接收的最大字节数,故无法超过参数buff所指的缓冲大小
flags 可选项参数,若没有则传入0
from 存有发送端地址信息的sockaddr结构体变量的地址值
addrlen 保存参数from的结构体变量长度的变量地址值
成功时返回接收的字节数,失败时返回-1
基于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);
int main(int argc,char * argv[]){
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
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);
}
serv_sock = socket(PF_INET,SOCK_DGRAM,0);
if(serv_sock == -1){
error_handling("UDP socket 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]));
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);
}
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 30
void error_handling(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_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("Insert message(q to quit):",stdout);
fgets(message,sizeof(message),stdin);
if(!strcmp(message,"q\n")|| !strcmp(message,"Q\n"))
break;
sendto(sock,message,strlen(message),0,(struct sockaddr*)&serv_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(char * message){
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
运行结果:
[root@VM_0_10_centos udp]# ./uclient 127.0.0.1 9190
Insert message(q to quit):Hi UDP Server?
Message from server:Hi UDP Server?
Insert message(q to quit):Nice to meet you!
Message from server:Nice to meet you!
Insert message(q to quit):Good Bye~
Message from server:Good Bye~
Insert message(q to quit):q
UDP客户端套接字的地址分配
调用sendto函数的时候,自动分配IP和端口号
UDP客户端中通常无需额外的地址分配过程
6.3 UDP的数据传输特性和调用connect函数
UDP数据传输中存在数据边界。
调用I/O的次数非常重要,输入函数的调用次数应该等于输出函数的调用次数。
已连接UDP套接字 与 未连接UDP套接字
通过sendto函数传输数据的过程大致可以分为以下3个阶段:
- 第一阶段:向UDP套接字注册目标IP和端口号
- 第二阶段:传输数据
- 第三阶段:删除UDP套接字中注册的目标地址信息
因此可以重复利用同一套UDP套接字向不同的目标传输数据。
未注册目标地址信息的套接字——>未连接套接字
注册了目标地址信息的套接字——>已连接套接字
但是,如果要与同一台主机进行长时间通信时,将UDP套接字变成已连接套接字会提高效率。
创建已连接UDP套接字
很简单,只需针对UDP套接字调用connect函数:
sock = socket(PF_INET,SOCK_DGRAM,0); 这里确实是在建立UDP的套接字
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)); 关键的一句话
对UDP套接字调用connect函数并非意味着要与对方UDP套接字连接,只是向UDP套接字注册目标IP和端口信息罢了。
调用connect
之后,每次调用sendto
只需传输数据,因为已经指定了收发对象,所以不仅可以使用sendto
和recvfrom
函数,还可以使用write
和read
函数进行通信。
6.4 习题
(1)UDP为什么比TCP速度快?为什么TCP数据传输可靠而UDP数据传输不可靠?
UDP和TCP不同,不进行流控制。由于该控制涉及到套接字的连接和结束,以及整个数据收发过程,因此,TCP传输的数据是可以信赖的。相反,UDP不进行这种控制,因此无法信任数据的传输,但正因UDP不进行流量控制,所以比TCP更快。
(2)UDP的特点(不是很全)
不存在连接概念;
有两个目标,也可以只用一个UDP套接字实现连接;
UDP套接字中可以使用已经分配给TCP的同一端口号;
可以在同一台主机进行TCP和UDP的数据传输;
(3)UDP数据包向对方主机的UDP套接字传递过程中,IP和UDP分别负责哪些部分?
IP负责链路选择。UDP负责端到端的传输。
(4)UDP一般比TCP快,但根据交换数据的特点,其差异可大可小。请说明何种情况下UDP的性能优于TCP。
UDP与TCP不同,不经过连接以及断开SOCKET的过程,因此,在频繁的连接及断开的情况下,UDP的数据收发能力会凸显出更好的性能。
(5)客户端TCP套接字调用connect函数时自动分配IP和端口号。UDP中不调用bind函数,那何时分配IP和端口号?
首次调用sendto
函数时,发现尚未分配信息,则给相应的套接字自动分配IP和端口号
(6)TCP客户端必须调用connect函数,而UDP中可以选择性调用。请问,在UDP中调用connect函数有哪些好处?
每当以UDP套接字为对象调用sendto函数时,都要经过以下过程:
第一阶段:为目标UDP注册端口和IP
第二阶段:数据传输
第三阶段:删除UDP注册的IP和端口信息
其中,只要调用connect函数,就可以忽略每次传输数据时反复进行的第一阶段和第三阶段。然而,调用connect函数并不意味着经过连接过程,只是将IP地址和端口号指定在UDP的发送对象上。这样connect函数使用后,还可以用write、read函数进行数据处理,而不必使用sendto、recvfrom。