TCP头部字段剖析

四元组

  四元组是由源IP、目标IP、源端口、目标端口;但是在TCP协议包中只能看到源端口和目标端口,这是因为解析IP是网络层做的事,并不是传输层,所以源IP和目标IP会出现在IP包的最后方

序列号

  1. 序列号是一个32位的无符号整数,达到2^32-1后循环到0
  2. 通过TCP传输的每个字节流都分配了序列号,序列号指的是本报文的第一个字节的序号
  3. 序列号加上报文长度,就可以确定传输的是哪一段数据
  4. 在建立连接之初双方都会随机一个序列号通过SYN报文发送到对端,这个随机序列号称之为初始序列号(ISN)
  5. 初始序列号是由四元组加随机因子通过MD5算法生成

Q1:序列号有什么用?
A1:我们知道序列号加报文长度可以唯一确定一段报文,因为报文不一定是按顺序发送,当其乱序到达对端时可以通过序列号拼接完整报文

Q2:为什么要随机序列号?
A2:如果序列号是固定的,那么很容易被猜到后续包的序列号,这样别有用心的人可以伪造一个RST包中断连接

Q3:序列号既然有长度限制,那么如果发生序列号回绕怎么确定哪段在前面?
A3:可以将前后报文序列号做差值运算,转为一个有符号整数,可以通过符号判断哪个在前哪个在后面,且在Options中会有每个报文的时间戳,时间戳可以反映报文的先后顺序

确认号

  1. 通过确认号可以告知发送端,已经收到前面序列号的报文,期望下个报文的序列号为ACK的值
  2. 只有需要被确认的包才会回复ACK
  3. ACK包有可能发生延迟
  4. ACK包本身不用被确认

flags

  该字段主要是标识该报文的属性在其前面还有4位的首部长度和6位的保留,比如SYN包为首次连接,将SYN标记为置为1;ACK包为确认包,将ACK标记为置为1

  1. SYN(Synchronize):用于发起连接数据包同步双方的初始序列号
  2. ACK(Acknowledge):确认数据包
  3. RST(Reset):这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无能为力处理
  4. FIN(Finish):通知对方我发完了所有数据,准备断开连接,后面我不会再发数据包给你了。
  5. PSH(Push):告知对方这些数据包收到以后应该马上交给上层应用,不能缓存起来
  6. URG : 紧急指针标志,为1时表示紧急指针有效,该报文应该优先传送,为0则忽略紧急指针。

窗口大小

  窗口大小只有16位标识,即最大窗口大小为65535但实际包大都远大于此,因为便将其作为窗口缩放大小,那么实际窗口大小 =缩放前大小 * 2^n

校验和

  校验和是用来校验报文是否缺失,校验和机制在很多数据交互设计中都会用到,发送报文时会将发送前的报文大小通过CRC算法计算后通过校验和字段发送过去,对端接收完报文之后,再自己计算报文大小,如果二者相等则可以认为报文没有丢失

紧急指针

  当flags为URG时,才会有紧急指针的概念

options

  该选项中通常会携带MSS、窗口缩放选项、时间戳和SACK等

三次握手

1.客户端发送SYN报文

  在介绍头部字段时有说到,SYN包会发送ISN,即随机一个序列号发送到服务端;当然SYN包在握手阶段,是不会携带数据过去,但其消耗一个序列号,同时携带最大段大小(MSS)、窗口大小(Win)、窗口缩放因子(WS)、是否支持选择确认(SACK_PERM)等信息;同时会开启一个SYN定时器,来控制是否重传重传次数由tcp_syn_retries值控制

  1. 客户端会首先设置目标IP和端口,
  2. 初始化MSS上限
  3. 设置状态为SYN-SENT
  4. 动态分配端口
  5. 计算ISN
  6. 调用connect函数发送SYN报文给服务端
    1. connect函数中会构造报文头部字段
    2. 将报文添加到发送队列
    3. 发送报文
    4. 启动一个重传定时器

Q1:不携带数据为啥要消耗序列号?
A1:因为该段报文需要被确认,所有带序列号的报文都需要被确认

Q2:第一个SYN包可否携带数据?
A2:其实也不是不可以,如果利用TFO(TCP FAST OPEN)机制是可以做到第一握手就携带数据包,TFO机制是当客户端第一次请求服务端时,在options中携带Fast Open选项,服务端接收到SYN后会生成一个Cookie值放在options中,同SYN+ACK包一起回复给客户端,然后客户端缓存服务端IP和cookie值;后续客户端访问服务器,打开fast open选项,且携带了缓存的cookie值发给服务端,服务端校验其合法性,然后处理相应数据

2.服务端接收SYN并发送SYN+ACK报文

  服务端在调用bind、listen之后进入状态从CLOSEDLISTEN,于此同时内核会创建两个队列,一个是半连接队列(Incomplete connection queue),又称 SYN 队列;另一个是全连接队列(Completed connection queue),又称 Accept 队列。
  服务端接收到客户端的SYN包时,会发送SYN和ACK包给对端(同一个包将flags中SYN和ACK标记为都置为1),同时状态从LISTENSYN-RCVD;这时会将这个连接信息放入半连接队列中;并且会开启一个定时器,等待客户端回复ACK,如果在等待时间内没有收到会重新发送SYN+ACK包

Q1:如何处理大量的SYN包?
A1:
  首先要分析这个“大量”是怎么回事,是正常的业务流量线性增长,还是恶意的SYN攻击;如果是业务流量的正常增长可以考虑适当的增加半连接队列大小,通过tcp_max_syn_backlog参数控制,或者降低SYN+ACK包的重试次数,通过tcp_synack_retries控制;如果是非正常连接,比如瞬时有大量的SYN包发过来,我们可以通过SYN-Cookie机制处理
  SYN Cookie 的原理是基于「无状态」的机制,服务端收到 SYN 包以后不马上为 Inbound SYN分配内存资源,而是根据这个 SYN 包计算出一个 Cookie 值,作为握手第二步的序列号回复 SYN+ACK,等对方回应 ACK 包时校验回复的 ACK 值是否合法,如果合法才三次握手成功,分配连接资源。

3.客户端接收SYN并发送ACK报文

  客户端收到服务端的SYN+ACK包之后,会回复ACK给服务端,状态从SYN-SENTESTABLISHED
  服务端收到ACK包之后,状态从SYN-RCVDTABLISHED时将这个连接加入全连接队列中,如果此时客户端携带了数据过来,就处理其中数据

Q1:既然ACK不用被确认,那么这里发出的ACK包如何保证服务端收到?
A1:其实服务端有相应的机制确保SYN+ACK包发送成功,这里的发送成功标志就是收到负载的ACK(消耗一个序列号),如果是非负载的ACK包就会被服务器丢掉;服务端在发送SYN+ACK时会启动一个SYN+ACK定时器用来确保该包发送成功,定时器每200ms执行一次,这里的执行时间缩短(原本是1s)可以及时清理太老的连接请求;SYN+ACK包的重试次数是由tcp_synack_retries参数控制,默认值为5次(centos)

Q2:全连接队列满了怎么办?
A2:有两种解决方案,即通过tcp_abort_on_overflow参数控制,当其值为1时代表发送RST包给客户端,当其值为0时丢掉客户端发过来的ACK包,然后重发SYN+ACK包

四次挥手

1.客户端发送FIN报文给服务端

  客户端(服务端)调用close方法,发送FIN报文给服务端(客户端),状态从ESTABLISHED → FIN-WAIT-1

Q1:FIN包能否携带数据?
A1:FIN包是可以携带数据的,也可以在最后一次数据传输包中开启FIN标识位

Q2:FIN是否需要消耗一个序列号?
A2:FIN包是要消耗一个序列号的,因为当最后一个数据包和FIN包同时到达对端,这时候对端回复ACK无法确保其是数据包还是FIN包的ACK

2.服务端收到FIN并回复ACK

  服务端(客户端)收到客户端(服务端)发送的FIN包,状态从ESTABLISHED → CLOSE-WAIT,并想客户端回复ACK包
  客户端(服务端)收到服务端(客户端)的ACK,状态从FIN-WAIT-1 → FIN-WAIT-2

Q1:如果同时关闭那么状态会如何变化?
A1:如果双方同时关闭,那么整个状态都会同客户端类似,但是将FIN-WAIT-2去掉,取而代之的是CLOSING状态

3.服务端发送FIN报文

   服务端(客户端)发送FIN包到客户端(服务端)收到,状态从CLOSE-WAIT → LAST-ACK

Q1:能否将挥手变为3次?
A1:可以的,有个东西叫做延迟确认,也就是当双端有双向数据传输时(pingpong),则开启延时确认,linux系统中延迟时间是40ms,刚开始不会立刻回复ACK,但如果在该时间段内没有数据包要回复,则将一起捎带给ACK给对端;有了该机制的存在就可以将服务端的FIN和ACK包合并发送到客户端,这个时候挥手变为了三次,服务端变为半关闭状态

4.客户端收到FIN并回复ACK

  客户端(服务端)收到服务端(客户端)的FIN包,然后恢复ACK给服务器端(客户端),状态从FIN-WAIT-2 → TIME-WAIT,同时开启一个定时器等待2MSL后关闭
  服务端(客户端)收到ACK包后,状态从LAST-WAIT → CLOSED

Q1:为什么要有TIME-WAIT状态?
A1:

  1. 因为数据报文可能在发送途中延迟但最终会到达,为了避免端口复用时途中“迷路”的数据包,造成新连接的数据包错乱
  2. 最后的ACK是主动关闭方发送,如果这个ACK丢失被动关闭方将会重发FIN包,如果马上就关闭连接则主动关闭方无法重新回复ACK

Q2:等待时间为什么是2MSL?
A2: MSL -> Max Segment Lifetime,linux中2MSL = 60s

  • 1个MSL是确保ACK报文能最终到达对端
  • 1个MSL是确保对端没有收到ACK时,重传的FIN报文可以到达主动关闭方
  • 2MSL = 去向ACK消息最大存活时间 + 来向FIN消息的最大存活时间

Q3:如何解决TIME-WAIT状态过多?
A3:大量的TIME-WAIT会造成端口无法复用,可以通过以下配置解决问题

  1. 开启tcp_tw_reuse,其原理是根据来向报文时间戳跟当前时间戳对比,如果小于当前时间戳说明是旧包直接丢弃掉
  2. 开启tcp_tw_recyle,其原理是缓存每个机器最新链接的时间戳,对于新来的同个主机连接如果SYN包中时间戳小于缓存的时间戳,则直接丢弃,否则可以复用TIME-WAIT连接

Q4:TCP状态机