Linux网络编程之socket编程(十四)-UDP

这一篇博文,主要总结UDP的相关知识,包括UDP的特点、UDP客户/服务基本模型、UDP回射客户/服务器以及UDP注意事项。

1.UDP特点

(1)无连接:即UDP协议内部并没有像TCP协议那样维护端到端的状态。

(2)基于消息的数据传输服务:而TCP是基于流的数据传输服务,所以存在粘包为题;而对于UDP来说,不存在粘包问题。

(3)不可靠:主要表现在数据包可能会丢失,还可能会重复,乱序等。

(4)一般情况下UDP更加高效

2.UDP客户/服务基本模型

UDP客户/服务模型

(1)服务器端,首先要创建一个套接字绑定一个地址。然后就可以接收数据(recvfrom())了,因为它不需要建立连接。

(2)客户端,首先创建一个套接字,往对方的地址发送数据(sendto())即可。

(3)随后,服务端接收到数据之后,做出应答。

3.UDP回射客户/服务器

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函数,它不断的接收客户端发送过来的一行数据,然后将它再回射给客户端。

其中,关于sendto和recvfrom函数的相关介绍,可以参考我的另一篇文章:APUE学习笔记-网络IPC:套接字

服务器端源码如下

  • 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)表示的是将网络地址转换成点分十进制地址。

客户端源码如下:

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连接的意义所在。

发表评论

电子邮件地址不会被公开。 必填项已用*标注