计算机网络学习笔记(3)—— TCP

Posted by 皮皮潘 on 02-17,2022

TCP简介

TCP在网络分层结构中处于运输层这一分层,但是实际上真正做数据运输操作的其实是IP层,TCP它本质上更多的做的是对于这些传输的数据的控制,而这也正是它的名字所代表的——Transport Control Protocol

TCP的特点无非是四点:

  • 面向连接的:面向连接的协议要求正式发送数据之前需要通过「握手」建立一个逻辑连接,结束通信时也是通过有序的四次挥手来断开连接
  • 全双工的: 在 TCP 中发送端和接收端可以是客户端/服务端,也可以是服务器/客户端,通信的双方在任意时刻既可以是接收数据也可以是发送数据,每个方向的数据流都独立管理序列号、滑动窗口大小、MSS 等信息
  • 基于字节流的: TCP 是一种字节流(byte-stream)协议,流的含义是没有固定的报文边界
  • 可靠的: TCP 在 IP 基础上加入了校验和、序列号、超时重传、拥塞控制等功能提供了可靠的传输层协议
    1620_1.webp

在讲了TCP的四大主要特点以后,接下来来讲解一些TCP比较细节的一些东西

MTU,IP分段与MSS

MTU(Maximum Transmission Unit)是在数据链路层的概念,数据链路层传输的帧大小是有限制的,不能把一个太大的包直接塞给链路层,这个限制被称为MTU。

IP分段是IP层的概念,IP数据报的最大大小为65535字节,这个大小远远超过了MTU的大小,因此当IP数据报大于MTU时,IP层会把数据报文进行切割为多个小的片段(小于 MTU),使得这些小的报文可以通过链路层进行传输,IP 头部中有一个表示分片偏移量的字段,用来表示该分段在原始数据报文中的位置,同时也有一个标记是否还有分片的标记,来表示是否是最后一个分片。

MSS(Max Segment Size)是在TCP层的概念,IP分段是非常耗性能的,而且IP层本身就是一个不靠谱的家伙,让它来保证分段以后再合并无疑很容易产生坏掉了的数据报从而导致重传,所以TCP层通过MTU和包头计算出MSS,并主动将数据分割成最大为MSS的数据包再交给网络层从而避免了IP的分段,同时TCP在建立连接的时候双方会互发MSS信息,并取其中的较小值作为各自的MSS以尽量避免数据报在传输过程中再次IP分段。

TCP状态图

直接盗了人家的一张图,大家看看就行
1622_1.webp

三次握手

面向连接的协议要求正式发送数据之前需要通过握手来建立一个逻辑连接,结束通信时也是通过有序的四次挥手来断开连接,握手的主要目的是给连接准备对应的内存空间,如序列号、滑动窗口大小、MSS等,从而保证之后的数据传输有对应的上下文从而可以完成连接的复用,而不至于是一次性连接,同时也交换一些辅助信息,如MSS,WS,窗口大小等,其中服务端返回的SYN + ACK包其实可以看作是两个包,ACK包用来响应之前客户端发来的SYN包代表从客户端到服务端的连接建立起来了,SYN包代表想要建立从服务端到客户端的连接,当收到客户端的ACK包后则双向的连接建立完毕
1636_1.png

四次挥手

挥手其实和握手也差不多,就是客户端和服务端分别断开彼此的连接,但是与握手时不同的是,当一端想要断开连接时,另一端可能还不想断开,因此不能将FIN包和ACK包合在一起发,因此需要四次挥手了
1638_1.png

另外需要注意的一点是,在连接断开发起方回复最后一个ACK包之后(第四次挥手),会进入TIME_WAIT状态,并等待2MSL的时间,
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间, TIME_WAIT存在的意义有两个:

  • 可靠地实现 TCP 全双工的连接终止(处理最后 ACK 丢失的情况)
  • 避免当前关闭连接与后续连接混淆(让旧连接的包在网络中消逝)

全连接队列,半连接队列与backlog

当服务端调用 listen 函数时,TCP 的状态被从 CLOSE 状态变为 LISTEN,于此同时内核创建了两个队列:

  • 半连接队列: 当客户端发起 SYN 到服务端,服务端收到以后会回 ACK 和自己的 SYN。这时服务端这边的 TCP 从 listen 状态变为 SYN_RCVD (SYN Received),此时会将这个连接信息放入「半连接队列」
  • 全连接队列: 「全连接队列」包含了服务端所有完成了三次握手,但是还未被应用取走的连接队列。此时的 socket 处于 ESTABLISHED 状态。每次应用调用 accept() 函数会移除队列头的连接。如果队列为空,accept() 通常会阻塞。
    1624_1.jpg

两个队列都不是无限大小的,listen 函数的第二个参数 backlog 用来设置全连接队列大小,如果全连接队列满(一直没有被accept),server 会舍弃掉 client 发过来的 ack(server 会认为此时连接还未完全建立)

可以举个典型的 backlog 值供大家参考,Nginx 和 Redis 默认的 backlog 值等于 511,Linux 默认的 backlog 为 128,Java 默认的 backlog 等于 50

超时重传、快速重传与SACK

  • 超时重传: 当一个包被发出去但是在一定时间内没有收到ACK的时候,发送端会进行重传,超时重传非常消耗性能,一点是因为在等待时间内发送方都不会再重传,另一点是因为在超时重传以后发送方会将自己的拥塞窗口降低到1MSS,重新开始慢启动

  • 快速重传: 因为超时重传太耗性能了,所以工程师又发明了一种新的重传方式,首先我们这里给ACK一个新的定义,ACK除了是确认自己已经收到了ACK的值之前所有的包之外,也代表了接收方接下来想要的包的序列号,所以当发送方收到3个重复的ACK的时候,它就可以知道有包丢了,这时候就可以直接重传了,而不是等到超时再重传,同时拥塞窗口降低到当前的一半并修改拥塞窗口临界值

  • SACK: 重传的时候又有一个问题,就是发送方应该重传哪个包呢,是只传一个还是全部重传呢,这时候SACK就可以排上用场了,SACK全称为Selectable ACK,也即接收方在每次ACK的时候在Option字段中带上对应的范围值,从而作为发送方重传的指导

流量控制与滑动窗口

流量控制做的事情就是,如果接收缓冲区已满,发送端应该停止发送数据。那发送端怎么知道接收端缓冲区是否已满呢?

为了控制发送端的速率,接收端会告知客户端自己接收窗口(rwnd),也就是接收缓冲区中空闲的部分。

一个非常容易混淆的概念是「发送窗口」和「接收窗口」,很多人会认为接收窗口就是发送窗口,但其实并不是,发送窗口其实是接收窗口和拥塞窗口中的较小值。

从 TCP 角度而言,数据包的状态可以分为如下图的四种:
1626_1.webp

TCP 包中win=表示接收窗口的大小,表示接收端还有多少缓冲区可以接收数据,当窗口变成 0 时,表示接收端不能暂时不能再接收数据了,这时候发送端就需要定时发送零窗口探测包去探测接收方什么时候可以再接收数据。

RST

在 TCP 协议中 RST 表示复位,用来异常的关闭连接,发送 RST 关闭连接时,不必等缓冲区的数据都发送出去,直接丢弃缓冲区中的数据,连接释放进入CLOSED状态。而接收端收到 RST 段后,也不需要发送 ACK 确认。

RST常见的情况:

  • 端口未监听
  • 一方突然断电重启,之前建立的连接信息丢失,另一方并不知道
  • 调用 close 函数,设置了 SO_LINGER 为 true

典型场景——连接池:

连接池是由客户端和服务端共同维护的,如JDBC和MySQL数据库,如果数据库因为Keep-alive超时或者其他原因主动断开连接,客户端OS知道这件事但是应用却不知道,它会傻夫夫地在连接池中用已经断开的连接去尝试通信,然后收获一个悲惨的RST

定时器

TCP 为每条连接建立了 7 个定时器:

  • 连接建立定时器

    当发送端发送 SYN 报文想建立一条新连接时,会开启连接建立定时器,如果没有收到对端的 ACK 包将进行重传
    
  • 重传定时器

    在发送数据包的时候会开启重传定时器,如果没有收到 ACK 将进行重传
    
  • 延迟 ACK 定时器

    在 TCP 收到数据包以后在没有数据包要回复时,不马上回复 ACK。这时开启一个定时器,等待一段时间看是否有数据需要回复。如果期间有数据要回复,则在回复的数据中捎带 ACK,如果时间到了也没有数据要发送,则也发送 ACK。
    
  • PERSIST 定时器

    Persist 定时器是专门为零窗口探测而准备的
    
  • KEEPALIVE 定时器

    如果通信以后一段时间有再也没有传输过数据,怎么知道对方是不是已经挂掉或者重启了呢?于是 TCP 提出了一个做法就是在连接的空闲时间超过 2 小时,会发送一个探测报文,如果对方有回复则表示连接还活着,对方还在,如果经过几次探测对方都没有回复则表示连接已失效,客户端会丢弃这个连接。
    
  • FIN_WAIT_2 定时器

    四次挥手过程中,主动关闭的一方收到 ACK 以后从 FIN_WAIT_1 进入 FIN_WAIT_2 状态等待对端的 FIN 包的到来,FIN_WAIT_2 定时器的作用是防止对方一直不发送 FIN 包,防止自己一直傻等。
    
  • TIME_WAIT 定时器

    主动关闭连接的一方在 TIME_WAIT 持续 2 个 MSL 的时间,超时后端口号可被安全的重用。
    

WireShark

WireShark是一个可视化抓包工具,非常非常好用,使用它的时候注意以下几点就行:

  • DupAck是数据接收方才会有的,它代表的是数据接收方发给数据发送方的ACK包重复了(WireShark分析得出的)
  • 重传是针对数据发送方而言的,只有在发送方抓包才能看到重发的多次数据
  • WireShark的感知都是基于抓包所在的机器而言的,它只能分析抓包所在机器发送和接收到的包
  • Out-of-order也会导致DupAck从而导致重传
  • 想要根据某个字段来过滤消息又不知道怎么表示该字段时,可以随便找一个有该字段的包,然后鼠标选中想过滤的字段,最下面的状态码就会出现当前 wireshark 对应的查看条件
  • 性能问题三板斧:1. Statistics --> Summary 2. Statistics --> Servcie Response Time 3. Analyze --> Expert Info Composite
  • 可以使用TcpTrace图查看对应的拥塞点,并根据拥塞点调整接收窗口最大值来避免拥塞,减少重传

常用的查询条件如下:

tcp 相关过滤器

  • tcp.flags.syn==1:过滤 SYN 包
  • tcp.flags.reset==1:过滤 RST 包
  • tcp.analysis.retransmission:过滤重传包
  • tcp.analysis.zero_window:零窗口

http 相关过滤器

  • http.host==t.tt:过滤指定域名的 http 包
  • http.response.code==302:过滤http响应状态码为302的数据包
  • http.request.method==POST:过滤所有请求方式为 POST 的 http 请求包
  • http.transfer_encoding == "chunked" 根据transfer_encoding过滤
  • http.request.uri contains "/appstock/app/minute/query":过滤 http 请求 url 中包含指定路径的请求

通信延迟常用的过滤器

  • http.time>0.5:请求发出到收到第一个响应包的时间间隔,可以用这个条件来过滤 http 的时延
  • tcp.time_delta>0.3:tcp 某连接中两次包的数据间隔,可以用这个来分析 TCP 的时延
  • dns.time>0.5:dns 的查询耗时

参考

  1. 《计算机网络:自顶向下方法》
  2. 掘金小册-《深入理解 TCP 协议:从原理到实战》
  3. 《Wireshark网络就是这么简单》