传输层协议——UDP和TCP
一、UDP协议
(一)UDP协议的特点
- 无连接
无连接指的就是使用UDP协议传输数据的时候,通信双方是不会自动保存对方的信息的(比如IP和端口号),除非在编程的时候自己添加代码使其保存对端信息,在之前的套接字编程中体现出了这一点。
- 不可靠传输
不可靠传输在代码中不容易体现出来,其形象一点儿的说法就是,发送方在发送完数据报之后,就不管了,不在乎数据报在传输过程中是否发生丢包。
- 面向数据报
使用UDP发送的数据是以UDP数据报为单位的,发送过程中不能进行拆分,
- 全双工
使用UDP协议的通信双方都可以发送和接收数据。
(二)UDP协议段格式
UDP数据报分为两个部分:报头和载荷
报头部分的长度为64位,也就是8字节。
- 16位源端口号和16位目的端口号:
16bit位表示的范围是0~2^16-1=65535,也就是说端口号的范围是0~65535。实际上一般把1024以下的端口号保留,直接写代码的时候都是用1024~65535范围内的。
- 16位UDP长度
16bit位表示的范围是0~2^16-1=65535,意味着UDP数据报长度的最大值是64KB,在传输大数据的时候要进行拆分组合,很麻烦,而TCP能解决这个问题。
- 16位UDP校验和
引入校验和是验证数据是否发生修改的手段。在数据传输过程中,电信号/光信号可能受到干扰,会使数据发生“比特反转”(0变成1,1变成0)。
发送方在发送数据之前,对载荷部分计算出一个校验和1,并将校验和1一起发送给接收方,接收方在收到数据报后,计算出一个校验和2,判断两个校验和是否相等,如果不相等,就丢弃掉这个数据报。
二、TCP协议
(一)TCP协议的特点
- 有连接
- 面向字节流
- 可靠传输
- 全双工
这些特点将会在TCP协议的核心机制中体现出来。
(二)TCP协议段格式
TCP协议段分为两个部分:报头和载荷。
- 16位源端口号和16位目的端口号
是传输层中的核心内容。
- 4位首部长度
4个比特位表示的范围是0~15,也就是说首部的最大长度是15个字节,但是首部不包含选项的情况下都有20个字节了啊,其实首部的长度是以4个字节为单位的,因此首部的长度是15*4=60个字节。
- 保留(6位)
基于UDP协议中数据长度不够,又不能扩展,因此TCP报头中就预留了一些“保留位”。
- 6个标志位
具体内容会在TCP的核心机制中讲到。
- 16位校验和
用于检验数据是否发生错误。
三、TCP的核心机制
(一)确认应答
TCP的一个特点是可靠性,保证可靠性的一个前提就是,发送方能知道发送的数据是否被接收方收到。
接收方收到数据后,给发送方发送应答报文(acknowledge,ack),发送方就知道对方收到了。
打比方就像社交软件上聊天的时候,发送一句消息,我们这里显示“已到达”,就知道对方已经收到了。
当标志位中的ACK=1,就代表这个是应答报文,只起到告知对方我已经收到数据的作用,这是操作系统内核立刻自动执行的。
如果发送方一次性发送多个请求时,先发送的请求在传输过程中也可能后到达接收方,后发送的请求反而先到达,出现了“后发先至”的情况。
对于这种情况,发送方怎么知道,接收方的ack应答是针对于哪一个请求的呢?
⭐️在TCP协议段格式中,有32位的序号和32位的确认序号。
序号就是将发送方发送的多次请求进行编号得到的序号,确认序号在应答报文中才生效,表示该应答是针对于某个序号的请求的。
TCP是面向字节流的,在编号的时候按照字节来编号,每一个字节分配一个序号,是连续递增的。
⭐️发送数据时,序号字段填写载荷部分的第一个字节的序号。
⭐️接收数据时,确认序号字段填写载荷部分的最后一个字节的序号+1。
如果发送方发送了1~1000,1001~2000,2001~3000……
接收方在收到1~1000后返回一个确认应答,确认序号是1001。其中1001~2000在传输过程中丢失了,即使后面发送方发送了2001~3000,确认应答的确认序号依然是1001,而不是3001,用于提醒发送方1~1000没有收到。
⭐️因此确认序号的作用就是:
- 告知对方<确认序号的数据都已经收到了。
- 告知发送方下次要从确认序号开始发送数据。
- 如果中途漏了一段数据,意在提醒对方有一段数据没收到。
❓要是出现“后发先至”的情况,接收方如何能按照发送方发送的顺序来读取数据呢?
TCP在接收方这里,会安排一个“接收缓冲区”,通过网卡读取到的数据,会先存放到接收缓冲区里,后续代码调用read,就从接收缓冲区里面读取数据。
在接收缓冲区内,数据会按序号进行排序,小的在前面大的在后面。
只有当前面的数据都收到了之后,才会解除阻塞,否则会阻塞(后面的数据都收到了,前面的却没收到),并发送确认应答告知接收方哪一段数据没有收到。这样就能防止后发先至的情况造成数据混乱。
(二)超时重传
TCP的可靠性,是确认应答和超时重传来维持的。
发送方发送数据后,在传输过程中可能发送“丢包”的情况,这种情况下,接收方是收不到的。
发送方在接收方收到数据后,会收到ack应答报文,引入超时时间,如果超过这个时间仍然没有收到ack报文,就可以判定为丢包了,此时发送方就会重新发送该数据。
这里有两种情况:
- 接收方发送的数据丢失了
- 接收方发送的ack报文丢失了
对于这两种情况,解决办法都是超时重传,如果是第二种情况,接收方就会收到两个一模一样的数据。其实接收方的接收缓冲区有去重功能的,根据数据的序号,先在缓冲区里查找一下,如果存在就丢失这个数据,不存在就接收。
⭐️接收缓冲区的特性
- 自动排序
- 去重
- 如果没有收到连续的数据段,会进行阻塞,使接收方发送ack报文提醒发送方
此时缺少了1001~2000,接收方就会发送1001的ack报文,当1001~2000接收到之后,接收方就会发送4001的ack报文。
(三)连接管理
连接管理要从两个方面来:
- 建立连接
- 断开连接
⭐️建立连接,TCP是靠“三次握手”来实现的。
发送方发送一个不带业务的数据,通过这个数据与接收方“打个招呼”。
发送方告诉接收方:接下来我要和你建立连接,你要把我的关键信息(IP、端口号等)保存好,同时你也把你的信息发给我。
当标志位中的syn=1时,表示这是一个同步报文。
❓在三次握手的过程中,发挥了哪些作用呢
- 投石问路,确认通信路径是否通畅
- 验证发送方和接收方的发送和接受能力是否正常
- 协商一些关键数据,比如保存对方的IP和端口号,协商第一次通信的初始序号是多少(并且两次连接的初始序号往往差异很大,防止第二次连接处理了第一次发送的数据)
⭐️三次握手过程中,服务器的状态变化
CLOSED:是一种假想的情况,表示TCP还没有连接。
LISTEN:表示服务器已启动,执行accept方法,阻塞等待用户连接。
SYN_SENT:发送了syn报文。
SYN_RCVD:接收了syn报文。
ESTABLISHEED:表示客户端与服务器已经建立好连接了,可以开始传输数据了。
⭐️断开连接,TCP是靠“四次挥手”来实现的。
四次挥手,就是一方想要断开连接,发送fin报文告知对方,对方准备好断开连接时,也发送fin报文,从而正常地断开连接。
当标志位中的fin=1时,表示这是一个断开报文。
⭐️四次挥手过程中,服务器的状态变化
CLOSE_WAIT:接收方在收到fin请求后,就会出现这个状态,表示正在等待执行close操作。
TIME_WAIT:在四次挥手的过程中,也是可能发生丢包的,如果A收到fin后返回ack就立马关闭,ack在传输过程中可能会丢失,B就会超时重传。为了避免B额外的重传操作,让A延迟一段时间关闭,这段时间长度为2*MSL(网络上任何两个节点传输过程中消耗的最大时间)。
(四)滑动窗口
TCP在保证可靠性的前提下,会损失效率。
因此TCP会使用到批量传输:
⭐️批量传输的原理就是滑动窗口
这张图中,窗口中有四段数据,主机A将窗口内的数据一起发送出去。
主机A收到了B的2001应答报文,此时窗口向后滑动一个单位。
窗口越大,效率就越高,但是窗口的大小不能无限大,会影响可靠性。
- 这里不考虑丢包的情况:
如果主机A先收到的是3001的ack报文呢,那此时窗口就往后滑动两位,因为返回的是3001,说明<3001的数据都已经接收到了,返不返回2001已经没关系了。
- 考虑丢包的情况:
⭐️接收方返回的ack报文丢失
这种情况下,部分ack丢失了不要紧,因为可以通过后续的ack来确认。
⭐️发送方的数据包丢了
快重传机制:
- 当某一段报文段丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"一样;
- 如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001 - 2000重新发送;
- 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
快重传机制与超时重传并不矛盾:
当要发送的数据量大的时候——>使用滑动窗口——>快重传
数据量小的时候——>超时重传
(五)流量控制
滑动窗口能提高数据发送的效率,但是如果发送方的发送速度远远大于了接收方的接收速度,就会导致数据大量丢失。
想象接收方的接收缓冲区是一个蓄水池,发送方发送数据相当于往水池里面加水,接收方从缓冲区里读取数据,相当于给水池放水,当接水速度远大于放水速度,水就会溢出。
⭐️流量控制就是给对方踩刹车,让它发的慢点。
流量控制就是让接收方,根据自身处理数据的速度,反馈给发送方,来限制发送方的速度。
在ack报文中,有16位的窗口大小,接收方将缓冲区的剩余空间大小填入这个属性中,发送方就按照这个数字来重新设定滑动窗口大小。
16比特位表示的最大范围是64KB,但是滑动窗口的最大长度不是64KB,在报文中还有“选项”这个属性,里面可以调整窗口扩展因子,来扩大窗口最大长度。
⭐️窗口探测包
(六)拥塞控制
流量控制是根据接收方接收数据的能力来控制的。
拥塞控制是根据传输链路的转发能力来控制的,如果发送方发送的速率大于传输链路的转发速率,那么就会导致丢包。
⭐️少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞,这时候就执行拥塞控制。
⭐️如果不好衡量链路中某个设备的转发速率,可以把整个链路看做成一个整体,通过做实验的方式(慢启动机制)来调节窗口大小:
- 先以小窗口来发送数据,如果很顺利、不丢包那就增大窗口的大小。
- 如果出现了丢包,那就减小窗口大小。
流量控制和拥塞控制都能限制发送方发送数据的速率,哪个值小,窗口大小就以哪个值为准。
⭐️执行拥塞控制的具体情况:
此处引入一个概念称为拥塞窗口
- 发送开始的时候,定义拥塞窗口大小为1;
- 每次收到一个ACK应答,拥塞窗口加1;
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口;这么一来,相当于拥塞控制是包含了流量控制的。
像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动”只是指初始时慢,但是增长速度非常快。
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值:
- 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
- 当TCP开始启动的时候,慢启动阈值等于接收方返回的窗口最大值;
- 在大量收到3个重复的ack报文后,就执行超时重传,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;(另一个策略是拥塞窗口从新的慢启动阈值开始)
(七)延迟应答
默认情况下,接收方收到数据后会立即返回一个ack报文。为了提高效率,返回ack的时机可以进行延迟。
⭐️延迟应答在于尽可能放大滑动窗口的大小,从而提高传输效率:
- 如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。
- 假设接收端缓冲区为1M。一次收到了500K的数据;如果立刻应答,返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
- 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M;
那也并不是每一个包都要执行延迟应答:
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过最大延迟时间就应答一次;
(八)捎带应答
TCP已经有了延迟应答,基于延迟应答,引入了“捎带应答”,“捎带应答”就是将返回业务数据的时候,顺便把ack给带过去。
将两个报文合并成一个报文,提高了效率。
如果没有延迟应答,那么返回ack和响应的时机就是不一样的。
捎带应答将ack也代入到响应报文中,在响应报文的报头中ack设置为1、窗口大小设置为接收缓冲区剩余值、确认序号选择合适的序号。设置这些并不影响响应的数据。
(九)面向字节流
TCP在传输过程中,是以字节为单位的,这是具体的传输过程:
⭐️创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时,数据会先写入发送缓冲区中;
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据.这个概念叫做 全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;
⭐️上面TCP传输的流程,会出现“粘包问题”:
- 首先要明确,粘包问题中的 "包", 是指的应用层的数据包.
- 在 TCP 的协议头中,没有如同 UDP 一样的 "报文长度" 这样的字段,但是有一个序号这样的字段.
- 站在传输层的角度,TCP 是一个一个报文过来的。按照序号排好序放在缓冲区中.
- 站在应用层的角度,看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包.
❓那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界.
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的 Request 结构,是固定大小的,那么就从缓冲区从头开始按 sizeof (Request) 依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符 (应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
HTTP是基于TCP实现的,解决粘包问题,在HTTP中也有体现到:
- GET请求:没有body,报头以空行符作为结束标记。
- POST请求:有body,但报头中有Content_Length来标明body长度。
(十)异常情况的处理
TCP进行数据传输的时候,通信双方可能都会出现异常,分为大致两种情况:
- 进程崩溃或主机关机重启
进程崩溃和主动退出连接没有本质区别,进程结束会释放文件描述符表,表中的资源都会调用socket中的close方法,完成四次挥手。
❓要是挥手过程中,接收方发来的fin太迟了,发送方已经提前关闭了呢?
- 主机掉电或者网线断开
⭐️接收方掉电
此时发送方不会收到接收方的ack报文,就会触发超时重传。
这种情况超时重传并不能解决问题,当发送方超时重传一定次数后,会发送一次“复位报文”触发“重置TCP连接”。
当rst=1时,表示这个报文是复位报文。
如果发送的复位报文依旧没有收到ack应答,发送方就单方面删除对方的数据,断开连接了。
⭐️发送方掉电
接收方会发现,发送方不再发送数据了,不知道对方是掉电了还是在休息。
等待一段时间后,接收方会给发送方发送一个“心跳包”,心跳包不携带业务数据,就是为了测试是否能得到对方的ack应答。
- 如果对方返回ack报文,说明有心跳,就继续等待。
- 如果对方没有心跳,就发送一次rst复位报文,还是不行就单方面断开连接。
四、TCP协议段格式中的其他字段
- URG:紧急指针位,TCP正常来说是按照序号顺序来发送和接收的,紧急指针相当于“插队”。
- 16位紧急指针:跳过前面的数据,直接从某个指定的序号开始read。
- PSH:催促标志位,发送方给接收方发的数据中带有这个标志位,接收方就能尽快将这个数据read到应用程序中。
五、TCP和UDP对比
我们说了 TCP 是可靠连接,那么是不是 TCP 一定就优于 UDP 呢?TCP 和 UDP 之间的优点和缺点,不能简单,绝对的进行比较:
- TCP 用于可靠传输的情况,应用于文件传输,重要状态更新等场景;
- UDP 用于对高速传输和实时性要求较高的通信领域,例如,早期的 QQ, 视频传输等。另外 UDP 可以用于广播;
归根结底,TCP 和 UDP 都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。