在学习计算机网络的过程中,我们知道OSI七层协议模型,但是在实际开发应
用中我们发现OSI七层协议模型并不适合实施,因为OSI上三层通常都是由开
发人员统一完成的,这三层之间在实现过程中没有一个明确的界限,所以我
们更多的是将七层模型认为是TCP/IP四层协议(除去硬件层),而TCP、IP
分别是两个网络层中非常具有代表性的网络协议,其中TCP处于传输层。而
在传输层中除却TCP协议之外,还有一个很重要的协议那就是UDP协议,所
以,今天我们就来认识一下UDP协议是什么样的。
需要注意的是,本篇文章讲述的是UDP的较为底层的知识,默认读者已经会使用socket套接字的网络编程,以及UDP和TCP的简单认识。
1. 如何认识网络协议
关于对网络协议的比较简单的认识方法,可以看我的另一篇博客:应用层协议,无论是哪一层的网络协议,它们的认识方法都是大致相同的,那就是认识网络协议在计算机中做出的约定,也就是结构化字段。
2. UDP的结构化字段
在传输层中的报文我们一般叫做数据段。
在学习某一层的某一个网络协议时,我们都要明白,这个报文如何将报头和有效载荷分离,以及如何将有效载荷向上交付?
对于两台主机的网络通信,对于传输层,我们需要知道两台主机的IP地址、端口号、以及传输层协议,这样我们就可以使用IP地址定位到唯一一台主机,协议号知道传输层使用的是哪个协议(这个是IP层的事情),然后利用端口号定位到一台主机的唯一一个进程,我们就可以在网络上精准的与一台主机上的一个进程进行网络通信了,而我们将上面需要的两台主机的IP地址、端口号、以及协议号,称为一个五元组。
所以我们自然的就解决了UDP协议如何向上交付的问题,那就是利用目的端口号。
而对于报头和有效载荷的分离,我们发现UDP协议中有一个字段就是16位报文长度,这个表示了一个UDP报文的长度,而UDP报头长度是固定的八字节,所以我们向上交付有效载荷的时候只需要根据UDP报头做出偏移就可以,至此有效载荷与报头的分离也明了了。
可能有人还有问题,那就是在TCP协议是面向字节流的,作为面向字节流的协议,这意味着上层使用该协议的应用层往往需要自行确保自己收到的报文是完整的(这通常需要应用层协议的配合),而我们在使用UDP协议进行socket编程的时候,好像从来没注意过这个问题,我们直接使用sendto、recvfrom两个系统调用将数据拿上来,这个数据就是独立且完整的,这是因为UDP协议是面向数据报的,报文和报文之间有着明显的边界,所以我们才不需要对UDP报文单独做处理。
但是好像还是不对,无论你是那个协议在底层传输时不都是字节序吗,那有什么面向数据报,面向字节流的说法,凭什么UDP的报文就不需要考虑报文的完整性和独立性的问题?
所以我在这里给出结论:UDP协议也是要确保自己收到的报文是独立且完整的,但是这一点不需要用户来做,UDP协议会自行处理。说是UDP协议处理,UDP协议是内嵌在操作系统中的,UDP协议处理,不就是操作系统处理吗?
对于接收到的报文,UDP协议会检测它是否有八字节的报头长度,没有直接丢弃,反之根据16位报文长度来进行进一步报文的完整性的验证以及报文之间的分离。期间有任何一个条件不满足,直接将报文丢弃,这也就是UDP协议不可靠的主要原因。
在UDP的协议的结构化字段中,还有一个字段是16位检验和,这个是用来检验报文是否有问题的,我们不讨论。
而上面的结构化字段中端口号无论是目的端口号还是源端口号,大小都是16位,这也是为什么socket编程中,端口变量都是使用uint16_t而不是int。
3. UDP协议的进一步理解
在上面对UDP结构化字段的认识过程中,经常会提到 “报头” 这个字眼,我们也说学习某一层的某一个网络协议我们都需要研究它的结构化字段,也就是报文,而报文就是报头 + 有效载荷。有效载荷是用户的应用层数据,我们可以使用一个缓冲区来将它们存放起来,这个好理解,那么报头如何在计算机中理解呢?
其实报头在计算机中就是一个结构体:
struct udphdr
{
uint16_t src_port; // 源端口号
uint16_t des_port; // 目的端口号
uint16_t length; // 报文长度
uint16_t check; // 校验和
};
现在我们就需要意识到一个场景,我们的计算机中会不会同时存在大量的UDP报文?这个显然是会的,那么既然存在大量的UDP报文,就需要对这些报文进行管理,如何管理?先描述,再组织。
那么我们就可以描述一下这个报文:
struct sk_buff
{
char* data;
char* tail;
sk_buff* prev;
sk_buff* naxt;
//...
};
其中如果有应用层协议向下交付了一个报文,我们需要使用UDP协议将这个有效载荷进行封装,我们可以这样封装:
我们先创建一个结构体sk_buff,此时我们需要让data指针向前移动有效载荷个大小,然后将应用层的数据拷到缓冲区中,再让data指针向前移动八字节,然后再放入UDP报头。这样我们就完成了报文的封装,我们的计算机中存在大量的UDP报文,我们的传输层中可能就是这样:
这样的话,对UDP报文的管理就转化为了对该链表的增删查改。
现在我们就明白了UDP协议的报文的管理以及报文的封装,现在我们将报文发出去了,如何接收呢?对端主机不是也遵守UDP协议吗?它也认识这个sk_buff啊,那么拿到有效载荷不是很自然吗?
而对于UDP的接收方,会将有效载荷存放在一个接收缓冲区中,UDP协议没有发送缓冲区,这一点与TCP协议是不同的:
UDP协议为什么需要接收缓冲区呢?这是因为没有接收缓冲区的话,上层应用来不及处理报文的话,继续来到的报文就直接被丢弃了,如果是这样的话UDP协议也太不靠谱了,怎么说也还是得靠谱一点。
那么为什么没有发送缓冲区呢?
这是因为UDP协议是不可靠的,而至于进一步的理解,则需要配合TCP协议来了解。
4. UDP协议知识的补充
在Linux源代码中我们可以找到上面提到的两个结构体:
UDP的特点:无连接,不可靠,面向数据报。
UDP没有发送缓冲区,只有接收缓冲区,如果接收缓冲区满了之后,继续接收到的报文会直接丢弃,直到接收缓冲区有多余空间。
这个接收缓冲区你可以理解为它就是一个队列,里面放的是交付上层的有效载荷。
UDP是全双工的,对于一个文件描述符,发的同时也可以收。
在使用UDP协议的时候需要注意发送的报文的大小不可以超过16位报文长度可表示的长度,不然多余的载荷会被丢弃。而16位能表示的大小在现如今是非常小的,只有65536字节,这也是它的局限性之一。
但是仍然有一些应用层协议使用的仍是UDP协议,这些应用层协议的特点就是传输的报文大小不会太大,以下是一些上层使用UDP协议的比较出名的应用层协议:
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
以上就是我对UDP协议的一个较为深刻的理解。