这一篇博文,主要总结UDP的相关知识,包括UDP的特点、UDP客户/服务基本模型、UDP回射客户/服务器以及UDP注意事项。
1.UDP特点
(1)无连接:即UDP协议内部并没有像TCP协议那样维护端到端的状态。
(2)基于消息的数据传输服务:而TCP是基于流的数据传输服务,所以存在粘包为题;而对于UDP来说,不存在粘包问题。
(3)不可靠:主要表现在数据包可能会丢失,还可能会重复,乱序等。
(4)一般情况下UDP更加高效。
2.UDP客户/服务基本模型
(1)服务器端,首先要创建一个套接字、绑定一个地址。然后就可以接收数据(recvfrom())了,因为它不需要建立连接。
(2)客户端,首先创建一个套接字,往对方的地址发送数据(sendto())即可。
(3)随后,服务端接收到数据之后,做出应答。
3.UDP回射客户/服务器
(1)从标准输入fgets获取一行数据,将该数据sendto发送给服务器,服务端recvfrom接收到数据。
(2)服务端接收到数据之后,通过sendto再将数据发送回去。
(3)客户端recvfrom接收回数据,fputs打印输出至标准输出。
- UDP服务器端:
(1)首先创建套接字sock:
int sock;
sock = socket(AF_INET, SOCK_DGRAM, 0);
(2)然后初始化地址,对地址而言我们只关心三个参数(协议家族、端口、地址):
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
(3)接着绑定地址:
bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0);
对于UDP编程来说,是不需要调用监听函数listen的,因为这里没有连接的三次握手。
(4)然后就可以处理通信了,这里封装一个echo_srv函数,它不断的接收客户端发送过来的一行数据,然后将它再回射给客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void echo_srv(int sock) { char recvbuf[1024] = {0}; struct sockaddr_in peeraddr; socklen_t peerlen; int n; while(1) { memset(recvbuf, 0, sizeof(recvbuf)); n = recvfrom (sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&peeraddr, &peerlen); if (n == -1) { if (errno == EINTR) continue; ERR_EXIT("recvfrom"); } else if (n > 0) { fputs(recvbuf, stdout); sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen); } } close(sock); } |
其中,关于sendto和recvfrom函数的相关介绍,可以参考我的另一篇文章:APUE学习笔记-网络IPC:套接字
服务器端源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
#include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) void echo_srv(int sock) { char recvbuf[1024] = {0}; struct sockaddr_in peeraddr; socklen_t peerlen; peerlen = sizeof(peeraddr); int n; while(1){ memset(recvbuf, 0, sizeof(recvbuf)); n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0,(struct sockaddr*)&peeraddr, &peerlen); if ( n== -1){ if(errno == EINTR) continue; ERR_EXIT("recvfrom"); } else if ( n > 0){ fputs(recvbuf, stdout); sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen); } } close(sock); } int main(void) { int sock; if((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(sock,(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); echo_srv(sock); return 0; } |
- UDP客户端:
客户端与服务端类似
(1)首先创建一个套接字;跟TCP一样,客户端一般来说,是不需要绑定地址的(当然也可以绑定)。
(2)然后调用一个封装函数echo_cli来处理通信:函数内要先初始化对方的地址(即服务器的地址)。
与在服务器端服务器初始化自己的地址不同的是:在客户端内初始化服务器的地址时,IP地址不能为htonl(INADDR_ANY),因为INADDR_ANY表示的是本机的所有地址,而我们需要连接对方,对方可能不在同一台机器上,所以应该明确指定对方的一个IP地址:
servaddr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
注意:函数inet_ntoa(peeraddr.sin_addr)表示的是将网络地址转换成点分十进制地址。
客户端源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> #define ERR_EXIT(m)\ do\ {\ perror(m);\ exit(EXIT_FAILURE);\ }while(0) void echo_cli(int sock) { struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL){ sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr)); recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL); fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sock); } int main(void) { int sock; if((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0) ERR_EXIT("socket"); echo_cli(sock); return 0; } |
4.UDP注意事项
(1)UDP报文可能会丢失、重复
针对数据丢失,发送端需要启动一个定时器,实现超时重传机制;针对数据重复,应用层需要维护数据报序号,跟下面乱序的处理相同。
(2)UDP报文可能会乱序
(3)UDP缺乏流量控制
我们知道套接字本身也有一个缓冲区,当该缓冲区满了的时候,如果再往缓冲区写入数据,并不是将这些数据丢失掉,而是将数据覆盖到原来的缓冲区。UDP并不像TCP一样有滑动窗口协议。
(4)UDP协议数据报文截断
如果接收的数据报大于接收的缓冲区,报文就可能会被截断,这就要求接收缓冲区的长度要大于等于发送数据的长度。写一个程序测试可知:
1)客户端向服务器端发送四个字节的数据——“ABCD”。
2)但是服务器端的接收缓冲区只有一个字节——recvbuf[1];然后我们在for循环里recvfrom接收4次,观察四个字节是否都能接收到呢?
3)实验结果显示:只能接收一个字节——A。表明后面3个字节被截断了!
(5)recvfrom返回0,不代表连接关闭,因为UDP是无连接的
(6)ICMP异步错误
考虑这么一个情况:
1)在不启动服务端的情况下启动客户端,并在客户端下发送一行数据给服务端。这个时候,客户端程序阻塞在recvfrom处。(sendto仅仅是将应用层缓冲区数据拷贝到了sock套接口的缓冲区中,并不代表该数据已经发给对方了)
2)因为对等方(服务端)并没有启动数据无法到达对等方,客户端捕捉不到这个信息,客户端就仍然阻塞在recvfrom处。
3)与此同时,TCP/IP协议栈会有一个ICMP的错误报文,但是呢,TCP/IP规定这种异步错误是不能够返回给未连接的套接字的,所以recvfrom也得不到通知,一直阻塞。
4)如何解决这种问题呢?解决方法是,UDP也是可以调用connect的。
(7)UDP connect
1)针对上述错误,给UDP调用一个connect:
connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr));
2)这个时候,在不启动服务器的情况下,运行客户端发送一行数据,产生的异步错误就可以返回给“已连接”的套接字,打印出如下错误信息:
recvfrom: Connection refused
3)需要注意的是,这里connect并不像TCP那样真的建立了连接,UDP connect并没有进行三次握手。只是维护了一种状态,这样的话,在调用sendto时就可以不指定对方的地址了,甚至可以调用send,write:
sendto(sock, sendbuf, strlen(sendbuf), 0, NULL, 0);
以上,即UDP连接的意义所在。