- 这是我参与「第三届青训营 -后端场」笔记创作活动的的第三篇笔记
一,概述
运输层的分用与复用:通过端口号实现的,网络层通过ip找到目标主机,运输层通过端口号找到目标主机的目标进程
TCP/UDP 属于运输层的协议,TCP是面向连接的协议,UDP是无连接的协议,TCP属于可靠传输,UDP是不可靠传输
二,TCP
1,报文格式
- 源端口,目标端口:各占两个字节,分别表示发送方和接收方的端口
- 序号:占4个字节,表示一个TCP连接中传送的字节的编号,该序号为传递的报文的第一个字节的序号,序号是循环使用的,从0开始,该字段也叫报文段序号
- 确认号:占4个字节,是期望收到对方下个报文段的第一个数据字节的序号,也接收到的正确的数据的序号+1,就是确认号
- 数据偏移:占4位,指出TCP报文段的数据起始处离TCP报文段的起始处有多远,也就是指示TCP报文段的首部长度,由于只有4位,所以单位是4字节,4位二进制最大表示15,所以TCP首部最长为4*15=60字节,而固定首部为20字节,所以选项最大为40字节
- 保留:占6位,保留为今后使用,目前置0
- 紧急URG(URGRent):当URG = 1时,表明紧急指针字段有效。它告诉系统此报文段有紧急数据,应该尽快传送,一般和紧急指针配合,发送方会将紧急数据放到本报文段的最前面,而紧急数据后面仍是普通数据
- 确认ACK(ACKnowledgment):仅当ACK = 1时,确认字段才有效,TCP规定,在建立连接后,所有的报文段都必须把 ACK 置1
- 推送PSH(puSH):当两个应用进程进行交互式的通信时,在一端应用程序希望在键入一个命令后立即就能收到对方的响应,那么发送方可以把PSH置1,接收方接收到PSH=1的报文段,就会尽快地交付到接收应用程序,而不是等整个缓存都填满了再向上交付(一般很少使用)
- 复位RST(ReSeT):当RST = 1时,表示TCP连接出现了严重差错,必须释放连接,然后重新建立运输连接。RST 置1还可以用来拒绝一个非法的报文段或拒绝打开一个连接
- 同步SYN(SYNchronization):在连接建立时用来同步的序号。当SYN = 1 而ACK = 0时,表明这是一个连接请求报文段(详细会在后面讲)
- 终止FIN(FINis):用来释放一个连接。当FIN = 1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接
- 窗口:占两字节。指的是发送本报文段一方的接收窗口(不是发送窗口),窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。窗口值作为接收方让发送方设置其发送窗口的依据。(发送窗口是一直动态变化的)
- 校验和:占两字节,校验包括首部和数据两部分的数据,和UDP一样需要加入伪首部
- 紧急指针:占两字节,紧急指针只在URG = 1时才有意义,它指出本报文段中的紧急数据的字节数,即使窗口为零也可以发送紧急数据
- 选项:长度可变,最大可达40字节
-
- MSS:最大报文段长度:减去首部的长度,默认536字节长
- 扩大窗口:占3字节,其中有一个字节表示移位值S,新窗口值等于首部中的窗口位数从16增大到(16+S),S最大为14,相当于窗口的最大值可以到
- 时间戳:占10字节,主要字段时间戳值字段(4字节)和时间戳回送回答字段(4字节),有两个功能,一个是由于计算RTT,另一个是为了防止序号超过
的情况造成的序号回绕(PAWS)的情况
2,自动重传(ARQ)
我们都知道IP层都是不可靠传输的,只能检查数据包是否是正确的,但是不能确保数据包一定能传输到目标地址,但是TCP是可靠传输,那么TCP是如何在基于IP不可靠传输的情况下实现可靠传输的呢?
TCP实现可靠传输靠的是自动重传协议
停止等待协议
停止等待协议,主要的内容就是,TCP在发送方发送一个数据报之后,都会停止一段时间等待收到接收方发出的确认收到消息,如果没用接收到确认,在一定时间之后发送方就会再次传输该数据,这样每个数据报就能被准确的接收
停止等待协议中应该注意三点:
- 在发送方发送一个数据报的时候,需要暂时保留已经发送了的副本,用于超时重传时使用,只有在收到确认之后才能删除副本
- 每个分组和确认分组都必须进行编号,这样才能明确哪个发送出去的分组收到了确认
- 超时计时器设置的重传时间应该比数据在分组传输的平均往返时间更长一些
如果接收方已经接收到数据,但是发送方没用收到确认:
- 确认丢失:接收方发送的确认消息丢失了
- 确认迟到,接收方发送的确认消息延迟了
这两种情况下接收方会接收到两个相同的数据报,对于重复接收的数据报,接收方应该这样处理:
- 丢弃重复的分组,不向上层交付
- 再次向发送方发起确认,不能认为已经发送过确认就不再发送,因为发送方就是因为没用收到确认才重传的
连续ARQ协议
使用停止等待协议的话虽然可以确保传输的可靠性,可是每次发送报文都要等待确认报文段,这样的效率十分低下,所以就提出了连续ARQ协议和滑动窗口协议
连续ARQ协议
该协议具体的操作就是:发送方维持一个发送窗口,发送窗口内的分组可以连续的发送,而不用等待对方的确认
发送方每收到一个确认,就把发送窗口向前移动一个分组的位置。
接收方一般采用累计确认的方式,也就是说,接收方不必逐个分组发送确认,而是在收到几个分组后再对按序到达的最后一个分组发送确认
优点:容易实现,即使确认丢失也不必重传
缺点:不能向发送方反映接收方已经正确收到的所有分组的信息(如中间某一个分组丢失了,但是前后的分组都收到了,但是发送方还是需要从丢失的分组开始进行重传,这叫做 Go-back-N(回退N)),当网络不好的时候连续ARQ协议会带来负面影响
TCP的可靠传输实现
TCP的活动窗口是以字节为单位的,并且发送端的发送窗口的大小是根据接收端的接收窗口的大小来调整的
发送窗口内的数据可以直接发送,而只有收到了确认,发送窗口才可以改变位置
(基本和上面写的连续ARQ一样,不想写了看上面的就好了)
选择确认SACK
TCP接收方在接收到发来的数据字节流不连续,这就形成了不连续的字节块
TCP允许先存储下这些数据,然后再通过SACK准确的告诉发送方已经发送了哪些字节块,这样就不需要重复传输已经收到的数据
由于TCP首部选项部分的最多只能40字节,而一个边界就要4个字节,所以最多可以指明4个字节块的边界信息(边界信息是收到的字节块的第一个序号和最后一个序号+1)
SACK需要在连接的时候进行商定
大多数实现都还是采用重传全部未被确认的数据块
3,三次握手
TCP建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个TCP报文段
最开始A和B都属于CLOSE状态,之后A主动向B发起连接
- A向B发送请求报文段,该报文段SYN = 1,同时选择一个初始序号seq = x,TCP规定,SYN报文段(即SYN=1的报文段)不能携带数据,但是要消耗掉一个序号
- B在接收到请求报文段后,如同意建立连接,就会向A发送确认。在确认报文中,应该把SYN位和ACK位都置1,确认号 ack = x+1,同时还要选择一个初始序号seq = y,(这个报文段段同样不能携带数据但是要消耗一个序号)
- 在收到B的确认后还需要向B发送确认,确认报文段的ACK = 1,确认号ACK=y+1,自己的序号seq = x+1。(此次报文段可以携带数据,如果不携带数据则不消耗序号)
在经过三次握手后,两台主机就都进入了ESTABLISHED状态
为什么需要3次握手?两次不行吗?
为什么需要最后一次的确认呢?
主要是为了防止已经失效的连接请求报文突然又传送到B,因而产生错误
所谓的“已失效的连接请求报文段”是这样参数的,A在第一次发起握手的时候出现了超时,于是A超时重传,这样B就发起了两次连接请求,而第一次的请求可能并没有丢失,它有可能在AB关闭连接后才到达B,这个时候B以为A再次发起了连接请求
如果只有两次握手,B就会建立连接一直在等A发送消息,可是A并没有发起连接请求(这样B只会拜拜浪费资源)
使用三次握手,可以解决该问题,B在发送确认后得不到A的确认就不会建立连接
三次握手丢包问题
- 第一次握手丢失:会进行三次重传,间隔时间分别是 5.8s、24s、48s,三次时间大约是 76s 左右
- 第二次握手丢失:对于客户来说,依然是connect超时,所以处理方式和第一次握手丢包是一样的。对于服务器来说,由于收不到第三次握手请求,会进行等待重传,直到多次重传失败后,关闭半连接。服务器会将该半连接从队列中删除。
- 第三次握手丢包:由于客户在发送第三次握手包后,不再等待确认,就直接进入了ESTABLISHED状态,所以一旦第三次握手失败,客户和服务器的状态就不同步了。当然,此时服务器会进行多次重发,一旦客户再次收到SYN+ACK(第二次握手请求),会再次确认。不过,如果第三次握手一直失败,则会出现,客户已经建立连接,而服务器关闭连接的情况。随后,一旦客户向服务器发送数据,则会收到一条RST回应,告诉用户连接已经重置,需要重新进行三次握手(这个要求过于严格了,实际上收到数据的话表明连接通畅,通过序号确认,就会建立连接的) 。
如果客户端在第三次握手中的SYN报文段一直故意不发给服务端,会导致服务端一直重试,导致服务器压力增大,这就是SYN flood 攻击
防护方式有:
- 缩短超时(SYN Timeout)时间
- 增加最大半连接数
- 过滤网关防护
- SYN cookies技术
4,四次挥手
在数据传输结束后,通信的双方都可以释放连接,这个时候A,B都处于ESTABLISHED状态
- A发起释放连接的报文段,该报文段FIN = 1,seq = u( u为前面传输数据序号+1),TCP规定,即使FIN报文不携带数据,也要消耗掉一个序号,这个时候A进入FIN-WAIT-1(终止等待1)状态
- B收到释放报文段后,需要发起确认,确认号ack = u +1,seq = v(v为前面传输数据序号+1),这个时候B进入了CLOSE-WAIT(关闭等待)状态。TCP服务器进程会通知上层应用进程,此时A向B方向的连接关闭了,但是B向A的方向还没有关闭,此时TCP连接处于半关闭状态,B向A发送数据,A仍要接收
- A在收到B的确认后,就进入了FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文
- B已经没有要发送给A的数据了,那么B就要发送一个释放报文,该释放报文FIN = 1,seq = w(最后发送数据序号+1),确认号需要重复上一次的确认号ACK = u+1,这个时候B就进入了LAST-ACK(最后确认)状态了,等待A的确认
- A在收到B的释放报文之后,必须发出确认,确认报文中ACK = 1,ack = w+,seq = u+1,然后进入TIME - WAIT(时间等待)状态,这个时候TCP连接还没有释放掉,需要经过时间等待器设置的时间2MSL后,A才会进入CLOSED状态
- B收到A的确认后就进入了CLOSED状态
为什么需要需要2MSL后才能进入CLOSE状态呢?
MSL叫做最长报文段寿命(Maximum Segment Lifetime),一般为2分钟(允许设置为更小值)
原因:
1,为了防止最后一个ACK报文段丢失了,这个时候B收不到i确认会发起重传,这样A因为在2MSL期间内,所以还可以接收到重传,可以再次发起确认,并重置2MSL计数器,以确保B能顺利进入CLOSED状态
2,防止出现“已失效的连接请求报文段”,在经过2MSL时间后,本连接产生的所有报文段就可以从网络中消失了,这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段
保活计时器
保活计时器一般设置为2小时,其作用是,如果在保活计时器内都没有收到报文(也就是两个小时内没收到报文),就会发送一个探测报文段,以后每隔75秒发送一个,如果10个探测报文都没有得到响应,就认为出现了故障,就会关闭该连接
四次挥手丢包的问题
- 第一次挥手丢包:客户端(主动关闭方)会进行重传,如果多次重传失败,则客户端关闭连接,服务器保持ESTABLISHED状态,如果服务器主动发送数据会收到RST报文段,重置连接(如果服务器不发送数据那么将一直保留这个假连接)
- 第二次挥手丢包:第二次挥手丢包不会重发,即使丢包也不管,这个时候直接第三次挥手发送FIN+ACK报文段,等同于发生了“同时关闭”的流程,客户端如果收到第三次挥手的FIN+ACK也可以从FIN-WAIT-1状态直接进入TIME-WAIT状态,也是一个正常的流程
- 第三次挥手丢包:服务端会在超时后重传,此时客户端有两种情况:
-
- 处于FIN-WAIT-1(之前的ACK报文段丢了)
- 处于FIN-WAIT-2
上面两种情况只要是重传之后成功都会顺利进入正常的状态,而如果多次重传失败后,双方就只能等待超时关闭连接了
- 第四次挥手丢包:服务端因为收不到确认会进行重试,再次发送第三次挥手的报文段,如果多次重试失败后会超时主动断开连接(2MSL内) ,或者超过2MSL时间后,新的客户端接入,收到服务端重试的FIN消息后,回复RTS进行重置
客户端收到ACK(第二次挥手)后,服务端跑路了
客户端在收到ACK后,进入FIN-WAIT-2状态,等待服务端发来的FIN包,但是如果服务端跑路了,就永远等不到这个包,这个时候会由操作系统接管,linux下可以通过tcp_fin_timeout 参数,设定超时时间,如果超时了,直接进入CLOSED状态
客户端收到ACK后,客户端跑路了
服务端在发送数据或者发送FIN+ACK包的时候没有接收端,就会不到超时,直到超时,或者收到RST后,主动断开连接
5,流量控制
TCP是利用活动窗口来实现流量控制的,流量控制作用于接收方,由于硬件的约束接收方的缓存区和CPU处理能力并不是无穷大的,所以需要进行流量控制,让发送方的发送速率不要太快,要让接收方来得及接收。
在发送数据的时候接收方可以通过接收窗口(rwnd receiver window)的动态变化来控制发送方的发送速率
6,拥塞控制
拥塞控制是作用于网络的,它防止过多的数据注入网络中,避免出现网络负载过大的情况,防止整个网络崩溃
TCP传输效率的提高
TCP是有缓存区的,那么什么时候该把缓冲区的数据发送出去呢?如果是缓冲区一有数据就发送出去,那么该数据可能只有1字节,而TCP头部就要20字节,这效率是低下的。选择TCP发送报文段的时机是一个较为复杂的问题
Nagle算法
TCP的实现中广泛使用了Nagle算法,该算法的主要内容是:
若应用进程要发送的数据逐字节的发送到TCP缓冲区,那么发送方就将第一个数据字节先发送出去,把后面的数据字节都缓存起来,在收到第一个数据字节确认后,再把缓存区中所有的数据组装成一个报文段发送出去,同时对后续数据进行缓存,在得到前一个报文段的确认之后才继续发送下一个报文段
当网络速率较慢的时候,这样的方法可以明显的减少所使用的网络带宽
Nagle算法还规定,当数据到达发送窗口的一半或已到达报文段的最大长度的时候,立即发送一个报文段,这样可以有效提高网络的吞吐量
糊涂窗口综合征
在TCP接收方缓存已经满了的情况下,交互式进程每次都从缓存中读取一个字节的数据,这个时候缓存空间只能腾出一字节,然后向发送方确认,将窗口设置为1字节,这样发送方会每次都发送1字节,这样效率就会很低下
解决的方法有:
- 让接收方等待一段时间,等待缓存区有足够的空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲空间的时候再发送确认报文
- 发送方不要发送太小的报文段,可以把报文段积累足够大,或达到接收方缓存空间的一半大小再发送
拥塞控制
又上图可以看到,如果出现了拥塞,如果不加处理,就有可能出现死锁等问题,导致吞吐量为0,极大的损害了整个网络的传输能力
TCP的拥塞控制算法有4种,即慢开始(slow-starrt) 、拥塞避免(congestion avoidance) 、快重传(fast retransmit) 和快恢复(fast recovery)
慢开始
拥塞控制是基于窗口的,发送方会维持一个拥塞窗口cwnd(congestion window) 的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化,发送方让自己的发送窗口等于拥塞窗口。如再考虑到接收方的接收能力,则发送窗口还可能小于拥塞窗口
在刚开始发送数据的时候,如果突然向网路发起大量数据,可能会导致网路发送拥塞,所以慢开始就是从小到大逐渐增大拥塞窗口的数值
旧规定里限制是初始拥塞窗口为1-2个发送方的最大报文段SMSS(Sender Maximum Segment Size) 的数值,但之后改为不超过2-4个SMSS,具体如下
- SMSS>2190字节:cwnd = 2*SMSS
- 1095字节<SMSS<=2190字节:cwnd = 3*SMSS
- SMSS<=1095字节:cwnd = 4* SMSS
慢开始规定,收到一个新的报文段确认之后,拥塞窗口最多可以增加一个SMSS数值
拥塞窗口cwnd每次增加量 = min(N,SMSS)
N为原先未被确认,但现在刚收到确认的报文段所确认的字节数
可以看出,慢开始每次增长不会超过SMSS,同时如果N没有超过SMSS,那么每次的增长都是翻倍的****
(上图是以报文段为单位的,真正的拥塞窗口是以字节为单位的,只是为了方便理解)
慢开始不是指cwnd增长慢,而是指,相比于直接发送一大堆数据,刚开始发送一点数据试探网络,要“慢得多”
拥塞避免
为了防止cwnd增长过大引起网路拥塞,还需要设置一个慢开始门限 ssthresh状态变量
- 当cwnd < ssthresh时,使用上述的慢开始算法
- 当cwnd > ssthresh时,停止使用慢开始,改用拥塞避免算法
- 当cwnd = ssthresh时,既可以使用慢开始算法,也可以使用拥塞避免算法
拥塞避免算法的思路是让cwnd缓慢的增长,即每次经过一次往返时间就让拥塞窗口加1(1个MSS单位是字节,这里方便理解),也就是线性增长
当拥塞避免增长到一定程度,发生了超时,这个时候发送方会判断为网络发生了拥塞,于是会调整门限值ssthresh = cwnd/2,同时设置拥塞窗口cwnd = 1,进入慢开始阶段,如此循环
快重传和快恢复
有时候发生了超时不一定是网络发生了拥塞,有可能只是偶然的丢失,我们直接重新慢开始会减低网络传输的效率
所以这就需要快重传算法了
快重传算法要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使是收到了失序的报文段,也要重复发送对已收到报文段的确认
慢恢复
通过快重传,发送方可以知道只是个别报文段丢失而已,而不是发生了拥塞,所以可以不用慢启动,而是使用慢恢复算法
慢恢复算法具体的过程是:
发送方调整门限值ssthresh = cwnd/2,同时设置拥塞窗口cwnd = ssthresh,并开始执行拥塞避免算法
主动队列管理AQM
上面讨论的是TCP自身对拥塞的控制情况,而主动队列管理是网络层对于拥塞控制的影响。
网络层对TCP拥塞控制最大的影响就是路由器的分组丢弃策略,通常来说路由器队列是按照“先进先出”FIFO的规则处理的,如果队列满了,就会执行尾部丢弃策略。
尾部丢弃一连串的分组会导致发送方出现重传,使得TCP进入慢开始状态,TCP突然把发送速率降低到很小的数值,并且丢弃的数据可能不止是一个TCP连接的数据的,这会导致大量TCP连接开始慢开始,这被称为全局同步(global syncronization)
为了避免这种全局同步的现象,提出了主动队列管理AQM(Active Queue Management)
主动就是不要等到路由器队列达到最大值时才丢弃分组,要提前主动丢弃分组,AMQ的实现有多种,曾经流行的就是随机早期检测RED(Random Early Detection)
其实现主要是维持两个参数:队列最小门限和最大门限,每一个分组达到之后就计算平均队列长度
- 若平均队列长度小于最小门限:新到达的分组放入队列排队
- 若平均队列长度超过最大门限:新到的分组丢弃
- 若平均队列长度在最小门限和最大门限之间,按照一定概率p把新到达的分组丢弃
RED中难操作的是p的计算,每次都需要计算,而且RED使用效果不太理想,所以RED被列为“陈旧的”,但是目前没有那种算法成为新的标准,都在实验阶段
三,UDP
UDP的特点:
- UDP是无连接的,发送数据前不需要建立连接
- UDP使用尽最大努力交付,不保证可靠交付
- UDP是面向报文的,对于应用程序交下来的报文,加上UDP首部后就交付IP层,既不合并,也不拆分(所以应用层得选择合适的大小来提高效率)
- UDP没有拥塞控制,因此网络拥塞不会影响发送效率,对某些实时性要求高的程序比较重要,但可能造成网络拥塞
- UDP支持一对一,一对多,多对一和多对多的交互通信
- UDP的首部开销小,只有8字节,比TCP的20字节首部要短
UDP的首部格式
- 源端口:源端口号,需要对方回信时选用,不需要时可全0
- 目标端口:目的端口号,这在终点交付时必须使用
- 长度:UDP用户数据报的长度,最小值为8字节(仅有首部)
- 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃
伪首部:用于计算校验和的,既不向下传递也不向上递交(TCP也有类似的伪首部)
参考资料: