Linux相关概念和易错知识点(41)(UDP、TCP报头结构)
目录
- 1.UDP
- (1)传输层
- (2)UDP报头
- (3)缓冲区和sk_buff
- ①缓冲区
- ②sk_buff
- 2.TCP
- (1)发送和接受缓冲区
- (2)报头结构
- ①按序到达
- ②.可靠传输
- ③流量控制
- ④紧急指针
1.UDP
(1)传输层
UDP处在传输层(应用层之下,网络层之上),负责将数据从发送端传输到接收端。 其中我们已经知道:IP标识主机,端口号标识主机内的进程。因此,源IP、目的IP、源端口号、目的端口号、使用的运输层协议这五个元素(五元组)可以标识一个通信。
其中0 - 1023为知名端口号,和常见服务强绑定,如HTTP等。一个进程可以bind多个端口号,从多个端口接收数据,但一个端口号不能被多个进程bind,每个端口接收的数据只能上交给一个进程。
HTTP协议我们已经认识了,传输层协议本质也是一种约定,其底层实现是C语言。不仅如此,网络层、数据链路层都有自己的协议,OS内核各个模块的通信也有自己的协议。其中OS内核通信实现可以直接用二进制传输数据,无需做明显的序列化,并且效率高、兼容性好。
(2)UDP报头
和应用层一样,添加报头在逻辑上是在要传输的数据前后包装一些约定的对象,在代码上就是定义相应协议的结构体对象,调用功能函数把我们的数据和要添加的数据合并起来,这就是封装。
解析报头时,UDP直接读取收到的报文前8个字节,剩下就是有效载荷。报文的前8个字节包括:16位源端口号和16位目的端口号,16位当前UDP报文有效载荷长度(自描述字段),16位校验和。
由于记录了16位当前UDP报文有效载荷长度和16位校验和,因此报文会被完整合法地交给上层。如果出现了UDP检验和失败,那这个报文会被直接丢弃,而无需向接收方响应任何数据。
在这里我们要特别说明一下UDP是不可靠传输,不可靠在哪?16位当前UDP报文有效载荷长度,16位校验和都无法保证可靠性吗?UDP的不可靠之处在于如果数据不完整的话会直接丢弃,上层却不会知道这一切。而TCP就有自己的重传机制,会保证接收方一定会收到相同字节数的数据。
现在我们再次强调UDP三个特性的含义,以免混淆:
无连接意味着无需listen和connect;不可靠意味着没有确认和重传机制等;面向数据报意味着接收方必须 像收快递那样一次性必须全部把一个数据包传入应用层(不会出现粘包问题,只能一块一块地IO),而相对而言面向字节流就是以bit为单位的(这也是TCP需要序列化的根本原因)。
我们要消除一个误解,就是可靠和不可靠其实并不是优缺点,而是特性。TCP要做很多处理才能保证可靠传输,不可靠传输往往意味着协议会更简单。UDP在直播、视频领域很常用,因为一些丢包根本不影响,而像支付交易等才更需要TCP。
(3)缓冲区和sk_buff
①缓冲区
UDP封装在代码上就是创建相应的结构体对象,在创建时会同时申请一块缓冲区,就是UDP协议的缓冲区。UDP有接收缓冲区,但这个缓冲区接收的顺序不一定和对方发送的顺序一致,可能报文2比报文1先到,这也是不可靠的一种。相比而言TCP就没有这个情况,它会有序号保证数据的顺序性。当UDP接收缓冲区被写满后,再收到的数据会直接丢。
UDP没有也不需要发送缓冲区,UDP在收到应用层的数据后,会接添加报头直接往下传,而不需要做处理,但这不影响其全双工的特性。
②sk_buff
sk_buff是负责管理报文的结构体,它属于整个网络协议栈,即从链路层到应用层,始终是同一个sk_buff在管理这个报文。从面向对象的思想来看,我们可以认为对报文的管理就是对sk_buff的管理,这在后面理解缓冲区是sk_buff链表这件事上至关重要。
struct sk_buff
{// 协议层头部指针__u16 transport_header; // 传输层头部位置__u16 network_header; // 网络层头部位置__u16 mac_header; // 链路层头部位置//其余部分省略
};
当一个报文的sk_buff被创建后会由指针管理数据。通过修改指针指向就可以填充UDP报头字段,也就完成了UDP报头封装,其它协议也是这样。
单个UDP报文包含首部能传输的最大数据约为64k字节(MTU限制)。因此要传输更多数据,就要拆分报文,多次发送,多个报文由sk_buff组成的链表管理。UDP的接收缓冲区就是报文的sk_buff链表组成的。
2.TCP
(1)发送和接受缓冲区
TCP也有自己的缓冲区,且同时有发送接受缓冲区。当要发送时,数据会直接拷贝进缓冲区;当读取时,也会直接从接受缓冲区拷贝数据。而缓冲区就由OS自主决定什么时候发,什么时候收,和上层解耦。
我们前面已经提过,管理报文就是管理sk_buff,缓冲区是本应是char数组,但这样并不好处理。所以考虑以面向对象的思想来处理,TCP的发送/接受缓冲区和UDP的接受缓冲区都是sk_buff链表,从缓冲区读数据、写数据本质就是对sk_buff进行增删查改。
(2)报头结构
TCP的报头结构相对比较复杂,但就像在UDP说的,TCP之所以复杂是因为它需要实现一些特性,所以接下来我会根据TCP要实现的独特性质分类讲解报头结构,具体性质后续会拓展。
①按序到达
前面说过UDP不保证接收的顺序,而TCP需要。因此TCP需要给自己的每一个报文添加一个字段——32位序号。这样接收方就知道收到的数据属于哪个序号了。进一步,接收方也就知道现在还有哪些序号的数据没有收到了,这也能进一步实现可靠传输了。
需要注意的是,TCP中要传输的报文的每一个字节都有自己的序号,多个字节组成一个TCP报文,报头的序号就是起始报文对应的序号。
在这里又引入了一个问题:既然一个TCP报文有多个字节,而报头只记录第一个字节的序号,那么接收方怎么知道应该读多少字节呢?报文边界在哪呢?
首先我们要清楚,发送方从应用层接收数据,再创建对象封装,是肯定知道边界的,每个TCP数据段往下传的时候网络层也能分清,也就是说我们的疑惑来自于接收方。对于接收方来说,网络层解包后就会把整个数据交给TCP,这个时候TCP难道还不知道边界吗?网络层 -> 传输层交付的过程就已经有边界信息的交付了。
TCP报头中没有记录报文长度的字段,TCP可以借助其它层确认边界
但是这样还不够,接收方能够确认边界了,报文的边界呢?对于UDP来说,它的报头是固定长度8字节,而TCP有选项部分,可以多添加一些选项,所以TCP的报文长度相对更灵活,那么报文的起始位置就需要单独存储了,这就是——4位数据偏移,且偏移以4字节为单位。最小值5对应报文数据从20开始,报头占0 - 19共20字节;最大值15对应60字节的报头(TCP允许最多40字节的选项)
②.可靠传输
下面的数据就是多个TCP报文,报头序号分别是1,1001,2001,3001,后续的例子都将建立在它之上。
有了序号之后就有机会实现可靠传输,具体过程是怎样的?
当发送序号为1的报文时,1 - 1000序号(共1000字节)的数据被发送和接收,接收报文之后接收方会发出一个回应,回应包含一个数字1001,告诉发送方1001之前的数据都收到了,这时发送方再从1001开始发送第二个报文,再回应,以此类推。这个回应中包含的确认的数字就是——32位确认序号。 如果此时报文2丢了,回应的就还是1001,那么这个时候发送方就会重传第2个报文。 但这样效率并不高,因此发送方会一直发送报文,而接收方收到一个报文就回应,回应中就能体现出丢没丢包,这样发送和回应就并行了。
有了确认序号之后,就能实现捎带应答机制了。从刚才的情况出发,发送方给接收方发报文,接收方通过确认序号回应,但如果回应时接收方发现自己想要给发送方再带一些消息,怎么办呢? 这个时候接收方就可以把要传的数据放在报文中,同时填充序号。这样发送方得到回应后,从序号处可以得到接收方想要给自己的数据,从确认序号那里也能得到确认信息,一举两得,这就是捎带应答机制。
捎带应答是一个可有可无的功能,比如接收方收到一个报文,它怎么知道这是一个单纯的应答,还是说有捎带应答?用序号和确认序号区分确实是一种方法,但是,TCP中这种可有可无的行为可不止捎带应答,还有一些后续会提及,所以 TCP报头需要集中管理,控制TCP连接的状态和数据传输行为,即6位控制位。
我们可以看到,当ACK == 1时,表示确认序号有效,这个时候接收方才会去解读确认序号。通过这张图可看出,控制位在TCP中非常重要,标识不同字段的有效性,算是给TCP报头进行一种分类了。
③流量控制
和UDP一样,TCP发送和接收缓冲区都是有大小的。UDP接收缓冲区满了就直接丢弃多的数据,TCP也是。但TCP要保证可靠传输,发送方发了多少数据要保证接收方能接收多少数据,这就意味着它需要尽可能避免这种情况,除了重传机制以外还要有流量控制,这就需要——16位窗口大小(0 - 65535字节)。
接收方和发送方进行TCP通信时,每个报头都要带上自己的窗口大小,就是自己的接受缓冲区的剩余的大小。 前面我说过发送方会连续发送多个报文,接收方再对接收的每个报文做回应,但是发送方在一定时间内连续发送报文数量的上限是什么呢?这就要引入滑动窗口了。 滑动窗口就是建立在TCP发送缓冲区之上的,是TCP决定什么时候从缓冲区向外发数据的关键结构。 滑动窗口大小的决定因素之一就是对方回应的16位窗口大小,滑动窗口大小不会超过16位窗口大小对应的值,这样就能保证发送方不会发送过多数据,以至于溢出接收方的接受缓冲区导致数据丢弃。
④紧急指针
TCP传输中,总会产生特殊情况需要紧急传输的数据,因此TCP还支持16位紧急指针(需要在控制位URG == 1时生效)。当紧急指针生效时,TCP按序到达就会被打破,OS优先读取处理紧急数据(如使用百度云盘,上传数据时用户申请终止上传,这个终止的指令就比较紧急,不需要等到前面大量数据被按序收到)。 紧急指针就是一个标记有效载荷中紧急数据的偏移量。紧急数据的长度呢?紧急数据只占1个字节,不能被大量使用。
其余部分,如端口、检验和等就不多赘述了,和UDP一致。