阅读 112

再学一次TCP协议

前言

TCP是从大学开始学,学了无数遍还是感觉很抽象的协议,最近在拜读小林大佬的图解网络,有了不一样的感觉,下文是学习笔记

初见TCP

什么是TCP

TCP是面向连接的,可靠的,支持字节流传输的协议

面向连接的:如果将UDP协议提供的服务比作古代的飞鸽传书的话,那么TCP协议所能提供的服务相当于打电话,应用程序在使用TCP协议传送数据之前,必须在源端口和目的端口之间建立一条TCP传输协议

支持字节流传输:流相当于于一个自来水管,从一端放入什么内容,从另一端可以照原样取出什么内容,这是一个不出现丢失、重复和乱序的传输过程

可靠的:TCP的确认、重传机制保证报文一定能到达接收端

TCP报文格式

image.png

  1. 源端口号、目的端口号:相当于地址,没有地址,数据不知到发给哪个应用
  2. 序号:在建立连接时由计算机生成的随机数作为其初始值,每发送一次数据,就累加一次该数据字节数的大小,用来解决包乱序问题
  3. 确认号:表示接收端已经正确接收序号为N的字节,要求发送端下一个应该发送序号为N+1字节的报文段,解决不丢包问题
  4. 头部长度:TCP报头长度是以4B为一个单元来计算,固定长度部分为4 * 5 = 20B,可变长度部分为4 * 10 = 40B,因此这个字段的值是在5~15之间
  5. ACK:除了最初建立连接时的SYN包,连接建立之后发送的所有报文段该位都为1
  6. RST:该位为1时,表示TCP连接中出现异常必须强制断开连接
  7. SYN:该位为1时,表示希望建立连接,同步序号
  8. FIN:该位为1时,表示发送端报文发送完毕,希望释放一个TCP连接
  9. 窗口大小:滑动窗口机制,下文有详细介绍
  10. 校验和:用来校验整个数据包在传输过程中是否出现差错
如何唯一确定一个TCP连接?

四元组:源地址、目的地址、源端口、目的端口

源地址和目的地址都在IP首部,用来定位主机

源端口和目的端口在TCP首部,用来定位进程

TCP三次握手

image.png

  1. 一开始客户端和服务端都处于CLOSE状态,服务端主动监听某个端口,变成LISTEN状态
  2. 客户端向处于LISTEN状态的服务端进程发送一个请求连接报文,此报文SYN位为1,seq值随机生成,随后进入SYN-SEND状态
  3. 服务端收到客户端的请求连接报文后,如果同意连接,则向客户端发送确认报文,此报文SYN、ACK位为1,seq为服务端自己生成的随机值,确认号为客户端seq + 1,随后进入SYN-RECEIVED状态
  4. 客户端收到服务端确认报文后,客户端还要向服务端发送一个确认报文,此报文ACK位为1,确认号为服务端seq + 1,随后进入ESTABLISHED状态,需要注意的是,这个报文是可以携带客户端到服务端的数据的
  5. 服务端收到报文后,也进入ESTABLISHED状态
为什么是三次握手?
  1. 三次握手才可以阻止历史连接的初始化

image.png

客户端发出的第一个请求连接报文因为网络拥堵的原因长时间未到达服务端,客户端没有收到确认报文,便又发出了一个请求连接报文,结果旧SYN比新SYN先到达了服务端,此时服务端返回了一个确认报文给客户端,客户端根据自身的上下文,判断这是一个历史连接,便发送一个RST报文给服务端,表示中止这次连接,节省了服务端的资源(不中止连接,服务端白白浪费了一个文件描述符的资源)。两次握手显然是无法做到的

  1. 同步双方初始化序列号

在TCP连接建立后,序号可以解决包乱序,数据重复等问题

第一次握手的时候,客户端会发送自己的初始化序列号给服务端,第二次连接的时候,服务端会发送自己的序列号给客户端,假如没有第三次握手,服务端怎么知道序列号已经同步给客户端了呢?万一第二次握手的包丢失了呢?

image.png

三次握手可以建立一个TCP连接,四次握手当然也可以,但是三次握手已经可以可靠地建立一次TCP连接,何必再浪费一次通信呢?

  1. 三次握手才能保证客户端与服务端都具有收发数据的能力

客户端第一次请求连接证明自己的说话能力没有问题,服务收到请求连接报文,后发送确认报文,证明自己的听力,说话能力都没有问题,客户端收到服务端的确认报文后建立连接证明自己的听力也没有问题,双方可以正常交流,假如没有第三次握手,万一客户端不具备接收数据的能力呢?

TCP四次挥手

image.png

  1. 客户端发送释放连接报文,此报文FIN位为1,seq为客户端发送的最后一个字节的序号加1,假定为u,随后进入FIN-WAIT-1状态
  2. 服务端收到客户端释放连接报文后,会向客户端发送确认报文,此报文ACK位为1,seq为服务端发送端最后一个字节序号加1,确认号为客户端seq + 1,随后进入CLOSE-WAIT状态
  3. 客户端收到服务端的确认报文后,进入FIN-WAIT-2状态,此时客户端到服务端的TCP连接断开,但是服务端到客户端的TCP还未断开,处于半连接状态
  4. 服务端发送完数据后,会发送释放连接报文,此报文FIN、ACK位为1,seq假定为w(在半关闭状态又发送了一些数据),确认号为u+1,随后服务端进入LAST-ACK状态
  5. 客户端收到服务释放连接报文后,会给服务端发送确认报文,此报文ACK位为1,seq为u+1,确认号为服务端seq + 1,随后客户端进入TIME-WAIT状态,
  6. 服务端收到客户端确认报文后,进入CLOSE状态,客户端经过2MSL时间后,也进入CLOSE状态,四次挥手结束
为什么TIME-WAIT的等待时间是2MSAL

MSL(Maximum Segment Lifetime)的意思是报文最大生存时间,有两个原因

  1. 保证A发送的最后一个ACK报文段能够到达B,这个ACK报文段可能丢失,假如ACK报文丢失,B会重传释放连接的报文,A刚好能在2MSL时间内接收到重传的报文,接着A重传ACK报文,重新启动2MSL计时器,最后,A和B都能正常进入CLOSE状态。假如A没有TIME-WAIT时间,ACK报文丢失后,B就无法按照正常步骤进入CLOSE状态
  2. 防止旧连接数据包影响新连接,2MSL可以保证旧连接的所有报文段都从网络中消失,如果网络中存在旧连接的数据包,有可能出现下图的问题

image.png

SYN攻击

什么是SYN攻击

在TCP三次握手的时候,Linux内核会维护两个队列,分别是

  • 半连接队列,又称SYN队列
  • 全连接队列,又称accept队列

服务端收到客户端发起的SYN请求后,内核会把该连接存储到半连接队列,并向客户端发送SYN+ACK,接着客户端会返回ACK,服务端收到第三次握手的ACK后,内核会把该连接从半连接队列中移除,然后创建新的完全连接,并将其添加到accept队列,等待进程调用accept函数时把连接取出来

image.png

服务器返回确认报文后,会将连接存储在半连接队列中,等待ACK数据包,根据这样的原理,攻击者可以不断发送SYN包,每个请求连接会在半连接队列中存储一段时间,将半连接队列空间占满,导致服务器无法接受正常的请求

如何防御SYN攻击
  1. 增大半连接队列
  2. 开启tcp——syncookies功能
  3. 减少SYN+ACK重传次数,服务端收到SYN攻击时,有大量处于SYN_RECV状态的TCP连接,处于这个状态的TCP会重传SYN+ACK,重传到达上限后,才会断开连接,减少重传次数就可以加快连接断开的速度

DDOS攻击

阮一峰大佬的文章一直很通俗易懂,值得一读

www.ruanyifeng.com/blog/2018/0…

滑动窗口与确认、重传机制

TCP协议使用以字节为单位但滑动窗口协议来控制字节流的发送、接收、确认和重传过程

  1. TCP使用两个缓存和一个窗口来控制字节流的发送,发送端的TCP有一个缓存,用来存储应用程序准备发送的数据,发送端对这个缓存设置一个发送窗口,只要这个窗口值不为0就可以发送报文段,TCP的接收端也有一个缓存,接收端将正确接收的字节流写入缓存,等待应用程序读取。接收端设置一个接收窗口,窗口值等于接收缓存可以继续接收多少字节流的大小
  2. 接收端通过TCP报头通知发送端,已经正确接收的字节号,以及发送端还能够连续发送的字节数

我们先来看看发送端的窗口

image.png

为了对正确传输的字节流进行确认,就必须对字节流的传输状态进行跟踪,根据发送状态,可以将发送的字节分为以下4种类型

  1. 发送且已被确认,例如,图中蓝色部分的19个字节是已经被接收端接收,并且收到了接收端的确认信息的
  2. 已发送但未收到确认
  3. 尚未发送,但接收端准备好接收,接收方还有空间
  4. 尚未发送,且接收方没有做好准备,接收方没有空间

image.png 第三类窗口又叫可用窗口

第二类窗口和第三类窗口相加叫做发送窗口,发送窗口是由接收方决定的,要是接收方没有空间,发送方发送再多也无济于事

上图中,当发送方收到20、21的确认号后,如果发送窗口同时也发生了变化,则滑动窗口向左移动两个字节的位置

image.png

上图中,发送窗口全部用完,表示接收方暂时无力接收新的数据

接下来来看看接收端的窗口

image.png 接收端的窗口比较简单,接收窗口的大小会发送给发送端

发送窗口一定等于接收窗口吗?

这是不一定的,接收端应用程序的读取速度并不是一成不变的,并且接收端通知发送端的报文也是存在时延的,所以两者是约等于的关系

重传策略

在以上的讨论中,没有考虑报文段丢失的情况,但是的现实的网络中,报文段丢失是不可避免的,在报文段丢失的时候,TCP是怎么做的呢?

选择重传

选择重传方式允许接收端在收到字节流序号不连续时,如果这些字节的序号都在接收窗口之内,则首先完成接收窗口内字节的接收,然后将丢失的字节序号发送给发送端,发送端只需要重传丢失的报文段,而不需要重传已经接收的报文段。这种策略需要接收端在TCP头部 选项字段中加一个SACK的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,就可以只重传丢失的数据

超时重传

image.png

首先解释什么是RTT和RTO

RTT(round-trip time):一个数据包从发送端到接收端往返的时间

RTO(Retransmission Time-out):超时重传时间

超时重传时间的设定是很重要的,先看看以下两个场景

image.png

如果设定值过低,有可能出现已被接收端正确接收的报文被重传,造成接收报文重复的现象 如果设定值过高,就造成一个报文已经丢失,而发送端长时间等待,造成效率降低的现象

网络环境是不断变化的,所以RTO并不是一成不变的,RTO的计算还是蛮复杂的,有兴趣的同学可以上网详细学习

流量控制

为什么要进行流量控制?

进行流量控制的目的是控制发送端发送速率,使之不超过接收端接送速率,防止由于接收端来不及接收送达的的字节流,而出现报文丢失的情况

如何进行流量控制?

接收端根据接收能力选择一个合适的接收窗口(rwnd)值,将它写到TCP的报头中,将当前接收端的接收状态通知发送端。发送端的发送窗口不能超过接收窗口的值。TCP报头的窗口数值的单位是字节

  1. 当接收端应用进程从缓存中读取字节的速度大于或等于字节到达的速度时,接收端需要在每个确认中发送一个 非零的窗口通知
  2. 当发送端发送的速度比接收端要快时,接收端缓冲区将被全部占用,此时接收端必须发出一个 零窗口的通知。发送端接收到零窗口通知时,停止发送,直到接收到非零窗口通知为止

如下图所示,TCP采取了滑动窗口控制的机制,使得发送端的发送速度和接收端的接收速度相协调,从而实现了流量控制的作用

image.png

拥塞控制

为什么有了流量控制,还要有拥塞控制?

流量控制的重点是放在发送端-接收端的局部控制上,拥塞控制的重点是放在进入网络报文总量的全局控制上,设想一下,你家门前的大马路就这么大,周一早高峰,邻居的车辆已经把大马路塞死了,你现在出门不是也塞在路上了吗?可怕的是,TCP的确认重传机制收不到确认报文会不断重传,加剧原本就很拥挤的大马路

于是,就有了拥塞控制

如何进行拥塞控制?

主要使用了四个算法

  1. 慢开始
  2. 拥塞避免
  3. 快重传
  4. 快恢复

在一个TCP连接中,发送端需要维持一个拥塞窗口(cwnd)的状态参数。拥塞窗口的大小根据网络的拥塞情况来动态调整,只要网络没有出现拥塞,发送端就逐步增大拥塞窗口,当出现拥塞,拥塞窗口就立即减小,那么发送端怎么知道网络拥塞了呢?

当发送端开始发送数据时,它对网络的负载状态不了解,这时可以使用慢开始算法,由小到大逐步增加拥塞窗口

假设拥塞窗口初始值为1,向接收端发送一个报文,接收端在定时器设定的时间范围内返回确认,表示网络没有出现拥塞。 拥塞窗口增加一倍,变为2,向接收端发送两个报文,接收端在定时器设定的时间范围内返回确认,表示网络没有出现拥塞。 拥塞窗口增加一倍,变为4,向接收端发送四个报文,接收端在定时器设定的时间范围内返回确认,表示网络没有出现拥塞。 一直这样指数增长下去

image.png 但是,为了避免拥塞窗口增长过快引起网络拥塞,还需要定义一个参数-慢开始阈值(ssthresh)

  1. cwnd<ssthresh,使用慢开始算法
  2. cwnd>=ssthresh,使用拥塞避免算法

拥塞避免算法阶段,拥塞窗口的增长并不是使用加倍的方式,而是每次加1的方式,变成了线性增长

image.png

当确认包没有在规定时间内到达时,就是出现网络拥塞了,这时慢开始阈值(ssthresh)设置为出现超时的cwnd最大值的1/2,

cwnd重新设置为1,重新开始慢开始算法

image.png

一旦出现拥塞,重新开始慢开始算法的方式太激进了,有这样的一种情况,当发送端连续发送报文M1~M7,只有M3在传输过程中丢失,而M4~M7都能正确接收,这时不能根据一个M3的超时就简单地判断出现拥塞,在这种情况下,需要采用快重传和快恢复算法

image.png

接收端在没有接收到M3时,不能对M4进行确认,应该及时向发送连续三次发出对M2对重复确认,要求发送端尽快重传未被确认的报文

与快重传配合的事快恢复算法,快恢复算法规定:

当发送端连续三次收到对M2的重复确认时,发送端立即将拥塞窗口设置为最大拥塞窗口值对1/2。执行拥塞避免算法,拥塞窗口按现行方式增长

image.png

抓抓包看看

讲了这么多,我们来看看一个TCP的包长什么样子

先使用tcpdump抓取请求某度的包

image.png

执行对某度的请求

image.png

将tcpdump下来的文件拉近Wireshark中,可以清楚看到三次握手、http请求、四次挥手

image.png

下图是第一次握手的包,Wireshark默认显示的序号和确认号是相对的,所以可以看到从0开始

image.png

大家看看这个包,可以看到Window size 下方还有一个Calculated window size,这是什么东西?

TCP头部的窗口大小只有16位,也就是最大窗口大小是65535字节,对于现代的网络传输需求来说,这个明显是不够用的,所以TCP引入了TCP窗口缩放选项作为窗口缩放的比例因子,范围是0-14,表示可以将窗口扩大到原来的2的n次方,像图中4096*64=262144,表示比例因子是6

image.png

比例因子可以在第一次握手的选项中看到

image.png

在选项中我们可以看到很多熟悉的面孔,比如MSS、SACK,可选项的格式如下

image.png

image.png

常用命令

(tcp.flags.syn == 1) && (tcp.analysis.retransmission) 上文讲到在接收端没有接收到包或者接收方的确认包丢失了,会发生重传,使用这条命令可以筛选出第一次握手重传到包,我们可以看到,重传的时间规律是,一开始都是1s,接着变成2s、4s、8s、16s,然后结束重传,由于本人使用的是mac电脑,我找不到重传次数是如何设置的(水平有限,见笑了)

image.png

(tcp.flags.reset == 1)&&(tcp.seq == 1)这个命令可以筛选出握手被拒绝的包,以下抓到的都是TLS握手失败的包

image.png

tcp.analysis.zero_window:抓取零窗口包

参考资料

小林coding的《图解网络》:写得非常好,推荐大家去看看,受益匪浅

文章分类
后端
文章标签