TCP数据包一般也被叫做数据报文,只要使用了TCP协议在设备之间传输数据就会一定遵循TCP数据包格式,这种通用的格式可以保证数据在不同系统、不同设备上以约定好的格式来传输数据。同时TCP处于OSI七层网络模型中的第四层,所以应用层的的协议都是通过四层协议来传输数据的。如果说OSI的三层实现了通过互联网跨机器传输数据,那么可以认为四层实现了传输数据统一格式(通过TCP、UDP等协议)。本篇文章主要分析TCP数据包格式,目的如下:

  1. 了解并且数据TCP数据包格式
  2. 通过抓包方式分析TCP数据包

用到工具:
  1. WireShark: 一般在本地环境分析数据, 支持Mac、Windows、Linux Desktop(Linux Server可以用tshark)
  2. TCPdump: 一般用在服务端分析数据,用于定位问题,支持Mac、Linux

TCP数据包头

TCP数据包格式图

TCP协议数据头部格式

info 代码定义

1
2
3
4
5
6
7
8
9
10
11
12
/* TCP头定义,共20个字节 */
typedef struct _TCP_HEADER
{
short m_sSourPort; // 源端口号16bit
short m_sDestPort; // 目的端口号16bit
unsigned int m_uiSequNum;    // 序列号32bit
unsigned int m_uiAcknowledgeNum; // 确认号32bit
short m_sHeaderLenAndFlag; // 前4位:TCP头长度;中6位:保留;后6位:标志位
short m_sWindowSize;       // 窗口大小16bit
short m_sCheckSum;       // 检验和16bit
short m_surgentPointer;      // 紧急数据偏移量16bit
}

头部格式说明

下面是是头部数据结构总览,后面会对重要字段详细解释。

在TCP头部前20个字节大小固定,为TCP数据包的固定首部。

  • 源端口: 位于TCP数据包头部0-15的位置,占用两个字节,记录了发送数据端套接字端口号。

  • 目的端口: 位于TCP数据包头部16-31的位置,占用两个字节,记录了数据接受套接字端口号。

  • 报文序号: 位于TCP数据包头部,占用四个字节,也就是32位。使用报文中第一个数据字节的序列号表示报文序列号。

  • 确认号: 发送数据方期望收到的数据段序号,用于确认发送方确认报文送达。

  • 数据偏移: 也被称为首部长度,占用4位,用来标记TCP协议头部长度。

  • 保留字段: 占用6位,为TCP发展预留空间,目前必须全部为0,老版tcp头部预留6位,新版预留3位。

  • 标志位: 占用6位,用来指示TCP会话中发送发或接受方根据标志位正确处理会话。

  • 窗口: 也被称为滑动窗口,占用16位,最大位65535, 告知发送端接收端缓存大小,控制发送端发送速率,控制流量。

  • 校验和: 占用两个字节,16位,对TCP报文(包括头部)校验,用于验证数据的正确性。

  • 紧急指针: 占用16位,配置URG=1时使用,紧急指针声明了紧急数据结束位置,用于发送紧急数据。

  • 选项填充: 用于声明TCP报文中的更多信息,长度可变,最大为40字节,在设置完选项后,填充保证头部大小为32的倍数。

报文序号详解

序号是为了接收方能够按照正确的顺序接受数据。

位于TCP数据包头部,占用四个字节,也就是32位。序号用来标记数据传输顺序,因为不管任何数据类型都会在传输层转换成字节流,举个例子,如果要发送一个字符串Hello,在传输层这个字符串有可能被拆分成两部分字节数据分别传输, 比如上面的字符串被拆分成了Hello(为了方便解释,这个地方用字符串代替字节),在实际的传输过程因为用户终端或者是网络设备、环境等因素是无法保证传输顺序正确性,此时数据接收方就会使用这个32位的序号来判断数据传输的顺序(一般在开发中不会关注这些信息,因为操作系统网络栈中实现了数据正确性的校验)。

序号的取值范围为2的32次方。在建立的TCP连接中,会为传送的字节流中的每一个字节按照顺序编号,也就是说从建立TCP连接开始到TCP连接端接这个生命周期中,只要涉及到传输数据,数据中的每一个字节都会被编号,这个序号被称为字节序号。

当TCP连接被建立以后,第一个字节数据的序号被称为ISN(Initial Sequence Number), 也就是初始化序号, 初始化的ISN并不一定为1,在RFC中规定,ISN的是根据时间分配的(具体的实现要以操作系统为准),当操作系统初始化时,存在一个全局变量假设为:g_number会被初始化为1(或者为0),每个4us加1, 当g_number达到最大值时会被重新初始化为0,当一旦有新的TCP连接创建时,g_number的值就会被赋值给ISN。

在整个TCP生命周期中,初始化序号是一个非常重要步骤,双方互相告第一个报文段是谁,TCP之所以被称为是安全的连接,就是通过序号保证了数据传输的安全,TCP三次握手就是为了初始化序号。报文序号只有在下面两种情况中的任意才会存在意义:

  • 报文数据字段至少包含一个字节
  • 报文是SYN段或FIN段或RST段

TCP序列号复用问题

在TCP中定义的序号位数只有32位,也就是如果在建立的一个会话中传输的数据量大于2的32次方字节,TCP会话会如何使用序号呢,答案就是复用序号:也就是TCP会话会重置序号,来保证序号仍然保持在2的32次方范围内,但是此时会带来一个问题: 一个高速传输数据的会话中(比如10/40G网络中数据传输速度很快,序号很快就会达到最大值限制),是如何告知数据接收方该序号是客户端失败重发的序号还是序号重置之后用于发送新数据的序号呢? 在RFC 1323提出了PAWS(Protect Against Wrapped Sequence Number)协议,简单的来说就是通过增加了时间戳的方式来保证序号的唯一性(因为时间戳是线性增长的),后面我会单独写一篇文章来了解一些PAWS协议!

报文确认号详解

确认号是为了让发送方知道接收方已经接收到了数据。

在TCP发送端发送数据到接收方之后,发送端每发送一个TCP段,服务端都要回复确认号来表示数据已经收到了来自于发送方的数据。比如发送方发送了报文,其中序列号为:101, 传输的数据为:100字节,如果接收方成功返回确认号,那么接收方就会返回确认号为:201, 表示服务端接下来期望接收字节序号为201以及以后的数据。

同时TCP协议支持累计确认的方法: 对于连续传输的报文,可以只对报文中的最后一个TCP段进行确认, 表示确认号之前的数据已经成功接收,比如发送方发送了三个数据包,包1包含字节:0-10, 包2包含字节:11-20, 包3包含的字节:21-20, 那么只要接收方回复了确认号21,就表示包1和包2已经接收成功。 之所以这样设计,是因为数据报文可能会丢失,应答的报文也可能产生丢失,累计确认的目的就是为了避免确认号回复在网络链路中丢失导致数据重新传输。

数据偏移详解

数据偏移用来表示报文中传输数据的真实位置。

TCP数据包中的数据偏移表示TCP中真正传输的数据距离TCP头部的距离,数据偏移在头部占用4位(0-15)位,单位为32位,也就是四个字节,每一个偏移都可以表示4个字节的偏移。之所以TCP头部存在数据偏移,是因为TCP选项中的内容是根据实际使用情况确定的, 所以TCP头部必须存在数据偏移。数据偏移最大表示为15,每一个偏移都表示为4个字节,所以在一个TCP报文中,最大的偏移量为:15*4=60字节(其中前20个偏移量是固定的)。TCP首部长度范围在:20-60字节之间。同时选项中数据长度不固定并且偏移量是以4字节为一个单位的,所以在填充中必须在在填充之后和选项内容为32的倍数(这里是位为单位,也就是4字节的倍数)。

报文标志位

报文标志位是非常重要的内容,一个TCP会话建立连接到数据传输到最终连接关闭都离不开标志位参与。

在TCP传输中每个TCP报文都会有一个目的,至于是什么目的就需要借助TCP标志位来确定,比如TCP在握手、挥手、传输数据过程中每个标志位也是不一样的。

  • URG: 紧急指针,该Flag需要配合16位紧急指针一起使用,其中指针数据用来指示数据包中紧急数据中最后一个字节的下一个字节,也就说当启用了URG之后,会通知接受方数据包中从0开始到指针指向位置结束的内容为紧急数据, 接收方接受到紧急数据会将该数据保存到单独的缓冲区,并且告知应用程序有紧急数据可用,应用程序可以通过设置recv(int sockfd, void *buf, size_t len, int flags)方法中的flags标志位来读取这些紧急数据。在FTP协议中用到了URGFTP即可以执行命令也可以传输数据,如果FTP一直忙于传输数据就会导致命令阻塞,通过设置URG状态,系统会中断数据传输,通知FTP服务接受到了紧急数据。除此之外,URG使用的情况很少,大部分应用程序是不支持的。

  • ACK: 该Flag用于通知发送方接收方已经接收到了发送方发送的数据,在大多数情况下,对于发送或接受的每个数据包,都做进行确认。

  • PSH: 在了解`PSH` Flag之前必须简单了解一下`MTU`、`MSS`、`窗口`和`TCP中缓冲区`运行机制,区分`MTU`、`MSS`、`窗口作用`:

    MTU(Maximum Transmission Unit)是指网络层(第三层)中传输的最大数据报单元,MTU一般由数据链路层(二层)设备决定。简单的来说就是物理接口提供给上层最大一次传输数据的大小。比如在以太网中,默认MTU的值为1500字节,如果在三层要发送的IP数据包大于1500字节,则必须把IP数据包进行分片传输,如果发送的IP数据包小于等于1500字节,那么就只需要一个数据包就可以完成发送。这里需要特别注意的有三点:

    • 传输层提供的是一种不可靠的传输方式,也就是在传输过程中丢失了一个包,传输层无法发现丢失的这个包,应用层即使发现传输失败,也只能重传整个IP包。
    • MTU可以被被修改,但是无论设备是处于二层还是处于三层,都需要配置端到端沿途所有的设备,IP包只要大于沿途中任何一个设备的MTU,那么设备就会把IP包做分片处理MTU分片

    MSS(Maximum Segment Size)是指数据传输层(第四层)数据报文中payload的长度,在MTU=1500字节的网络上传输数据是,MSS=1500-IP头部长度-TCP头部长度。该功能实现了传输层分片功能。当TCP层要传输的数据大于MSS时,传输的数据就会被分片传输。之所以存在MSS,目的就是实现四层对数据分片,避免数据在三层分片。举个例子: 如果一个数据包为10000,在不使用MSS的情况下,该数据会在三层拆分多个包发送,如果传输过程中一个包丢失,长度为10000的整个数据包就会重新传输。如果使用了MSS,该数据就会在四层分片,因为四层中TCP协议实现了安全传输,丢失其中任意分片的数据,四层都可以重新传输,从而大大降低因为丢包导致整个数据需要重新传输的代价。

    TCP滑动窗口用来告知发送方接收端缓存大小,控制发送端发送的速度,可以简单的理解为: 接收端告诉发送端我还有多少缓冲空间可以使用,你发送的数据量不要超过我的缓冲空间。必须明白的是窗口和MUT、MSS没有任何关系,MTU和MSS用来控制每次传输单元长度,滑动窗口用来协商双方发送数据量问题。

    在上面的偏移量数据偏移详解中可以得知,一个TCP头部最小为20个字节,同样的一个IP头部也占用20个字节,如果每次传输的数据量太小,比如每次传输只有1个字节, IP头部长度20个字节,假设TCP头部长度也是20字节,那么在一次传输中一共要传输41个字节,这对于网络来说是一种巨大的浪费。如果在传输数据时以大量小包方式传输,整个网络宽带就会被挤占, 同时接受和发送端所在的操作系统也不得不处理每一个数据包(这种攻击手法也是众多DDOS攻击手法中的一种常见攻击方式)。 为了增加网络吞吐量,一般操作系统都有相应的算法优化小包发送方式, 以Linux中的Nagle为例, 优化小包规则如下:

    • 如果数据包长度达到了MSS, 则允许发送缓冲区数据;
    • 如果数据包中含有FIN标志,则允许发送缓冲区数据;
    • 如果设置了TCP_NODELAY选项,则允许发送缓冲区数据;
    • 没有设置TCP_CORK选项,并且所有发出去的包都被确认,则允许发送缓冲区数据;
    • 数据已经缓冲区等待了一定的时间(一般是200ms),则允许发送缓冲区数据;
    • 数据被标记为紧急数据,则允许发送缓冲区数据;

    综上来说,PSH标记是为了告知TCP模块不要再等待缓冲区数据写入或者继续等待发送周期,而是直接将数据发送(发送方)或者将数据推送给上层应用程序(接受方)。
    对于发送方来说,一般每一次使用write方法写入数据到缓冲区,都会将缓冲区的数据打包成一个或者多个TCP报文,并且设置最后一个TCP报文的PSH标志位,从而告知TCP立即发送缓冲区的数据。 对于接受方来说,如果接受到的数据报文中包含PSH标志,则会立即将数据从缓冲区推送给上层应用。

  • RST: 复位标志, 在正常断开一个已连接的TCP会话的情况下,过程需要四次挥手。 但是在异常情况下双方通讯一定存在问题,导致正常的四次挥手这个过程无法正常执行,RST Flag的出现提供了强制关闭连接的一种机制。正常情况下,无论是发送方的数据包还是接收方的数据包中包含RST标记,该TCP会话申请的内存和端口等资源都会被系统释放,对于上层的应用来说,就是TCP连接被关闭了,一般会显示connection reset或者connection refused的错误信息。在目前的操作系统中,为了保证系统内核的安全性,一般是把应用和内核分开,每个应用的内存分为 用户空间内核空间。在网络通讯中,客户端和服务端一般都属于应用层,应用层只能通过send/recv的系统调用与内核交互,才能感知到内核的网络会话域是否接受到了RST。 当一端收到了另外一端发送的RST后,内核会认为会话连接已经关闭。如果应用层的应用再次尝试使用recv去读取数据,应用层就会收到Connection reset by peer的错误,表示另外一端已经把连接关闭,如果尝试使用send把数据写入内核缓冲区,应用层就会收到Broken pipe的错误,表示会话已经断开了连接。 下面是几种经常出现RST的情况:

端口不可用一般可能是因为端口没有监听过,也有可能是监听端口的应用已经退出,操作系统系统回收掉了应用程序申请的资源。无论是上面的哪种情况,操作系统接收到请求后就会根据请求目的IP和端口遍历操作系统中会话资源, 如果根据IP和端口没有找到指定的对象,一般情况下就会给请求方返回一个包含RST的数据包。但是需要注意的是如果在TCP传输的过程中数据包被篡改,也就是原始数据包发送的校验和和接受到的数据中的校验和不一致,那么TCP报文会被操作系统直接丢弃,不会返回包含RST的数据包。这是一个连校验和检测都没有通过的数据包,多半是有问题的,该数据包大概率是在传输过程中被篡改了,或者是一个伪造的数据包,因为在正常的传输层,一般应用的两种协议中: TCP丢弃检验和有问题的包后,对方长时间收不到ACK回复的情况下会自动尝试重传,另外一种协议UDP本身就是一个不可靠传输协议,如果检测到数据包有问题,就会直接丢弃。

TCP协议中提供了超时选项,如果接收方所在的IP地址能够ping通过,并且端口正常监听,排除防火墙问题,如果在接收到包含RST的数据包,有大概率的问题就是设置了TCP超时选项,当设置了TCP超时选项后并且没有在指定的超时时间内收到数据,此时设置超时的一方就会发送一个RST标志的数据包给对方,表示拒绝对方连接。

正常关闭TCP连接的过程中要经历四次挥手的操作,在应用层调用close()关闭连接时,会发送FIN标志给对方,默认情况下,close()会立马返回,此时内核中的TCP模块负责将缓存区中的数据发送给对方,但是对于应用层来说,无法感知到缓冲区的数据是否发送完成,l_linger选项可以控制应用调用close()之后的行为。

  1. 使用默认的参数情况下,也就是l_onoff为0,l_linger值被忽略,调用close()之后会立即返回给调用者,TCP模块负责发送残留的的缓冲区数据给对方。
  2. l_onoff为1, l_linger为0的情况下,TCP回直接丢弃缓冲区数据,并发送RST给对方,此时不会再按照四次挥手关闭连接,同时对方调用recv()也会出现WSAECONNRESET错误。
  3. l_onoff为1,l_linger>0,并且socket为阻塞,则调用close()将阻塞l_linger秒时间,如果在l_linger秒内缓冲区数据成功发送,close()将会返回0,如果未能成功发送缓冲区数据,close()返回-1,表示失败, 并将errno设置为EWOILDBLCOK。
  4. 当socket为非阻塞时, close()将立即返回,需要根据close()返回值以及errno来判断TCP缓冲区是否发送成功。

需要注意的是,传输过程中如果出现数据包错误错误是不会发送RST标志的,也就是原始数据包发送的校验和和接受到的数据中的校验和不一致,那么TCP报文会被操作系统直接丢弃,不会返回包含RST的数据包。这是一个连校验和检测都没有通过的数据包,多半是有问题的,该数据包大概率是在传输过程中被篡改了,或者是一个伪造的数据包,因为在正常的传输层,一般应用的两种协议中: TCP丢弃检验和有问题的包后,对方长时间收不到ACK回复的情况下会自动尝试重传,另外一种协议UDP本身就是一个不可靠传输协议,如果检测到数据包有问题,就会直接丢弃。

  • SYN: SYN(Synchronize Sequence Numbers)被程序同步序号编号,该Flag用于初始化TCP连接,TCP连接可以理解为于远程的机器建立一个虚拟电路(类似于二层中设备通过物理电路连接), 当客户端发起对服务端连接时,会在数据包中设置SYN标记,服务端回复的包中也会设置SYN标记,最后客户端回复给服务端中的不再设置SYN标记,此时三次握手结束(后面我会单独写一篇文章介绍TCP中的三次握手和四次挥手)。

与该标记相关的一种攻击手法就是SYN Flood攻击,也被称为SYN洪水攻击,原理就是描述就是: 客服端向服务端连接请求,此时数据报文中的SYN标记被设置,服务端返回一个包含SYN+ACK的报文给客户端,并等待客户端再次发送包含ACK标记的报文等待完成三次握手这个过程,此时如果客户端不回复ACK报文,则服务端会认为之前发出的报文丢失,并且会重新发送一遍包含SYN+ACK的报文返回客户端并等待一端时间,如果等待后仍然没有收到客户端发来包含ACK标记的报文,服务端就会丢弃这个未完成的连接,中间等待的这段时间被称为SYN Timeout,一般来说等待的这段时间是分钟数量级别的,如果一个系统存在这种大量的半连接,就不得不花大量的资源维护在操作系统中,加上服务端不断的对半链接进行回复重试,服务端的负载最终将会变得异常巨大,此时由于服务器忙于维持这种半连接会无暇理睬正常用户的请求或者响应客户请求很慢,从正常请求的的用户来看,服务端就是响应缓慢或者无响应。

另外一种对SYN常见的用法就是SYN扫描, 正常扫描端口是客户端和服务端之间建立一个连接(完成三次握手)来判断目标是否开启了指定端口,正常三次握手之后,服务端应用能够获取到客户端的信息,如果服务端发现某个客户端进行大量的不同端口的连接就可以非常简单的判断是否有扫描器在扫描自己。但是客户端在发送SYN请求到服务端之后,可以直接根据服务端返回的响应信息是RST还是SYN+ACK就可以判断出目标端口是否存活,不需要完成三次握手过程。此时客户端在服务端响应后直接发送RST标记的数据包返回给服务端结束连接就可以了,此过程中上层应用无法感知到是否存在客户端连接, 比如Nmap扫描器就支持SYN扫描

  • FIN: FIN Flag表示连接终止符,用于四次挥手这个过程,建立一个连接需要三次握手,断开一个连接需要四次挥手。和上面三次握手中,TCP提供了半连接(half-open)的特性一样,TCP关闭连接同样提供了半关闭(half-close)的特性, 半关闭这个特性主要是为了解决在TCP中的一端关闭掉发送数据之后还能继续接收对端发送数据的能力,下面是简单的四次挥手过程(后面我会写一篇详细的文章介绍三次握手和四次挥手):
  1. 客户端发送一个FIN报文, 并且停止再次发送数据,并且客户端此时处于FIN-WAIT1的状态,等待服务端来确认。
  2. 服务端收到FIN报文后,会响应ACK报文,表示已经收到客户端要关闭连接的的报文了,服务端此时处于CLOSE-WAIT状态。
  3. 客户端收到服务端ACK报文后,进入到FIN-WAIT2的状态,等待服务端最终确认关闭连接。
  4. 如果服务端想要断开连接(不再向客户端发送数据),此时服务端也向客户端发送一个FIN报文, 此时服务端处于一个LAST-ACK的状态,等待客户端的确认。
  5. 客户端收到服务端的FIN报文后,也会一样发送一个ACK报文作为应答,此时客户端处于TIMNE-WAIT状态,并在计时器指定时间周期后关闭连接,服务端收到ACK报文后也将关闭连接。

扩展标标志位

在2001年9月的RFC3168的文档中启用了3位TCP保留位,实现了显示拥塞通知。

还记得前面图中TCP头部保留字段保留了6位么?TCP头部。在2001年9月的RFC3168中定义了新版的TCP头部(该头部兼容老版头部定义)。新版本的头部使用了保留字段中的三位用来扩展TCP协议。新增加了NS, CWR, ECE标志位。这三个标志位需要一起配合使用,简称ECN,三个标志位定义位置如图所示:TCP头部扩展
ECN(Explicit Congestion Notification)简称显示拥塞通知,支持端到端的网络拥塞通知。
在通常情况下,一旦网络中出现拥塞,TCP/IP会主动丢弃数据包,源端检测到丢包后就会减小拥塞窗口,降低传输速率,如果端到端能够成功协商ECN的话。支持ECN的路由器就可以在发生拥塞时在IP报头中设置一个标记,发出一个即将拥塞的信号,而不是直接丢弃数据包。ENC减少了TCP的丢包数量,通过避免重传,较少了延迟(尤其是抖动), 提升了网络传输效率。由于ENC定义比较复杂,我后面会写一篇新的文章来介绍ECN

滑动窗口

滑动窗口告知发送方接收方还能接受最大的数据量,后面我会单独写一篇文章介绍介绍TCP中的滑动窗口。

每个TCP会话中,无论接收方还是发送方,系统都初始化了一块内存区域来保存接收或者发送的数据。我们一般叫这块内存区域为内存缓冲区。

在TCP会话中,发送方接收到另外一方发送的数据之后,会回复ACK响应,然后发送方继续发送一下一个数据包。但是这种效率明显会很低效,第一个原因是考虑到发送端需要在每次在发送数据之后等待接收端的ACK确认,确认也是需要网络延时成本的。第二个原因就是如果接收端接收到数据后所在主机如果忙于处理其他事情有可能会导致ACK不能及时回复。

鉴于这种情况,TCP引入了窗口的概念,窗口大小就是指:无需等待接受端ACK恢复,可以继续发送数据量的最大值。前面说到了,在每个TCP会话中,系统都会初始化一块内存用来保存接收和发送的数据,窗口大小就是系统中这块内存剩余的空间,对于发送方来说,通过得知对方的窗口大小就可以知道还能发送给接收方的的最大数据量是多少。如果尝试发送数据量大于接收方的内存缓冲区,则接收方会丢弃掉该来自于接收方的数据。 对于发送方来说,发送缓存也是一块内存区域,该内存也有大小。 如果尝试写入的数据量大于该内存空间大小, 会导致上层应用的send()方法一直处于阻塞状态。

另外需要注意的是,发送方发送的数据虽然可以在ACK确认之前多次发送,但是已发送的数据在收到对应ACK确认之前仍然会保留在内存缓冲区,在按期收到ACK确认之后,数据就可以缓冲区清除。

另外TCP支持累计确认,比如发送方发送了数据为: 100-199, 200-299, 300-399 这三段数据,只要接受方回复了ACK为400的结果,就认为 100-199, 200-299, 300-399这三段数据已经全部发送成功。

TCP校验和

TCP校验和属于端到端的一种校验方式,发送的每个数据包都由发送端计算出校验和,然后放在TCP头部,然后接收端接收到数据之后再次计算校验和是否一致,如果TCP会话中发送的数据包在中途被修改过,接收端会直接丢弃TCP报文。 TCP校验和校验包含IP的首部信息,然后把伪首部、TCP报头、TCP数据化为2个字节长度,如果总字节总长度为奇数,那么就在最后添加一个每位位都为0的字节。需要注意的是TCP报头中包含校验码信息,在计算校验和的过程中,校验码每位都是0,否则会陷入校验码计算的逻辑混乱中。

伪首部需要依赖三层数据报头中的信息,包括: 源IP地址(32位)、目的IP地址(32位)、保留字节(8位)、传输协议(8位)、TCP报文长度(包含报头+数据)。之所以TCP校验和用到三层的内容,主要是为了进一步TCP校验和的检错能力,因为有了伪首部的参与,TCP校验和相当于增加了对源和目的IP地址、传输协议等内容的检测, 这里简单说明一下校验和就算过程:

  1. 计算伪首部数据: 源和目的IP地址 + 保留字节 + 传输协议 + TCP报文长度。
  2. 计算TCP头部数据: TCP头部(TCP头部的checksum要全部置为0计算)。
  3. 计算TCP Playload真实传输的数据。
  4. 数据相加计算: 伪首部计算值 + TCP头部计算值 + TCP数据计算。
  5. 如果相加值大于FFFF,则使用高16位和低16位相加,直到高16位都为0。
  6. 计算结果按位取反。

TCP可选项

TCP提供了可选项来增加TCP报文的描述信息,可选项区域长度是可变长度,范围为:0-40字节, 在数据偏移详解中指定了可选项区域的长度,数据偏移每个位都都可以表示4个字节,所以可选项和填充位只能是32倍数位。TCP可选项
每个可选项下都有一个Kind标志位来标记选项类型

EOL可选项

可选项的二进制为: 00000000

End of Option List标志位简称EOL, 用来表示选项列表的结尾。还记的数据偏移详解么? 数据偏移中每个位都表示4个字节, 也就是32位, 如果TCP可选项中的长度不满足是32位的倍数。可以使用EOL可选项填充。该标志位只包含Kind标识,长度为1个字节, 用来表示TCP可选项结束。当TCP包头可选项区域结束后没有与数据偏移指定的偏移量结束时才会需要使用EOL选项。除此之外,EOL可选项不一定是在TCP可选项的末尾,比如指定TCP头部长度为40个字节,其中第39bytes是EOL标识符,根据RFC793协议规范,可以指定0来填充第40个bytes, 对于第40个bytes已经不再属于TCP可选项的一部分了。

NOP可选项

可选项的二进制为: 00000001

No Operation标志位简称NOP, 该选项可以用在选项之间或者结尾处,比如可选项WSOPT长度为3个字节,也就是24位, 为了使WSOPT更容易对齐,可以在WSOPT之前添加NOP做填充, 保证WSOPT域为4bytes。但是在RCF793协议中规定,发送端不一定会保证会填充NOP来保证选项的对齐,因此接收端必须支持接受非填充的可选项。在TCP选项中,选项顺序除了EOL需要声明在结尾处外,其他顺序并不是固定的,除此之外由于NOP标志位的加入,每个标志位有可能是使用NOP填充之后的长度,也有可能是使用NOP填充之后的对齐长度。

标志位是否会使用NOP填充取决于操作系统对TCP栈的实现,比如在Linux操作系统中, 如果TCP的可选项不满足32位倍数长度,在发送数据时并不会使用EOL填充结尾,而是使用一个或者多个NOP填充并且实现TCP头部可选项32位对齐,另外虽然RFC协议没有规定TCP头部可选项的顺序, 但是Linux在TCP栈实现上会按照一定顺序排列TCP选项,其中的原因就是虽然RCF协议虽然没有规定顺序,但是会有一些设备对TCP可选项顺序是比较敏感的,可选项的顺序有可能导致TCP通讯异常。

MSS可选项

可选项的二进制为: 00000010, 长度标志为00000100, 长度为4。

Max Segment Size简称MSS是TCP希望从对端收到最大报文的长度。这边需要注意的是MSS指示TCP数据长度,不包含关联的TCP头部的长度。在三次握手阶段,在包含SYN标志的握手包中的可选项区域都包含MSS标志来告知对方自己的MSS标志,因为在三次握手阶段无论是客户端还是服务端都会发送带有SYN的数据包, 所以双方在握手阶段就可以知道对方的MSS的大小,但是MSS是可选项,所以RFC1122规定如果没有MSS可选项的话,则会默认使用536字节作为MSS的大小, 但是需要注意的是目前网卡几乎都支持TSOGSO功能,在网卡开启这些功能的前提下,协议栈中的TCP层可能会按照MSS的整数倍发送数据包,然后网卡会再次对TCP数据包分段,这样做的目的是减轻CPU的处理压力。RFC6691协议重新声明了MSS选项的相关说明,该协议明确说明了MSS的值为MTU-IP包头-TCP基本包头(不包含可选项)。发送端在负责发送数据之前扣除TCP可选项长度并得到真实传输数据的长度。

WSOPT可选项

可选项的二进制为: 00000011, 长度标志为00000011, 长度为3。

TCP中有一种连接叫做长肥管道,简称LFN(Long Fat Networks)所谓的长肥管道就是建立的TCP的会话中物理带宽很大,而且RTT也很大的会话,计算公式为:BDP = RTT * BandWidth。由于TCP协议栈的拥塞控制实现算法限制,这种长肥管道最终传输数据效率很低(后面我会详细写一篇文章介绍长肥管道,并且介绍如何最大化利用长肥管道)。默认拥塞控制算法对缓冲区要求比较高(Google于2016年提出BBR拥塞控制算法,对解决长肥管道很有帮助,Youtube应用了BBR算法,Youtube网络全球吞吐量提升了4%)。一般来说操作系统分配的会话缓冲区要大于窗口的大小,但是TCP头部中已经限制了窗口最大值为65535, 基于此RFC1323中为长肥管道提供了两个高性能扩展, 包括WSOPTTSOPT选项,这两个选项可以通知会话双方真正的窗口大小。

当TCP会话一端需要通知对方需要更大的接收窗口时,就会使用WSOPT选项, 当使用WSOPT选项时,接收端实际的接收窗口为Window Size<<shift.cnt, 其中按照RCF协议规定,shift.cnt的最大值为14,当shift.cnt超过14之后,则按照最大14来处理。 使用WSOPT选项来扩展窗口大小, 被称为Window Sclae, WSOPT选项可以把窗口的大小由原来的16位扩展到30位,此时窗口大小大约可以表示容量为1G。提升窗口大小可以有效提升长肥网络的性能传输效率。

需要注意的是,如果要使用Windows Scale特性,需要在发送端的SYN包中使用WSOPT选项,接收端在回复SYN-ACK的数据包中同样需要发送WSOPT选项,需要注意的是在协商Window Scale的过程中,不能对SYNSYN-ACK报文中窗口大小应用WSOPT选项。WSOPT中的shift.cnt可以为0,标识Window Scale Factor为1(2^0=1), 表示TCP头部中Windows Size就是实际接收窗口的大小, 表示不对Windows Size进行扩充。 并且一定需要注意的是,如果只有一方发送了WSOPT选项,但是没有接收到对端返回的WSOPT选项,表示协商无效,此时发送端仍然需要将Window Scale Factor设置为1,表示TCP头部中Windows Size就是实际接收窗口的大小。
"通讯双方WSOPT选项"
在上面的图中,展示了发送端和接收端都有各自的接口窗口发送窗口,因此一个有效的会话中,一共有4个Window Scale Factor。假设发送端接受窗口Window Scale FactorR,发送窗口Window Scale FactorS, 则对应接收端窗口Window Scale FactorR, 发送端窗口Window Scale FactorS。 在TCP通讯双方在协商好Window Scale Factor之后, 之后数据包中窗口大小字段将自动进行Window Scale, 发送的数据包中的窗口大小实际为接收窗口右移Sclae Factor之后的结果。

SACK-Permitted和SACK选项

SACK-Permitted可选项标识为: 00000100,长度为2字节。
SACK可选项标识为:00000101,长度不固定,长度为变量。

从之前的TCP通讯机制中可以得知,接收端是通过ACK标识来通知发送端已经成功接受到数据包,并且TCP支持累计确认机制。但是在实际的网络中,由于网络的不稳定性,导致数据包在传输过程中存在乱序传输或丢包的现象。比如在传输过程中,发送端发送了100-200的数据包,并且接收端成功接受该数据。紧接着发送端发送了201-300301-400的数据包,但是由于网络丢包的问题接收端只收到了301-400的包, 并没有接收到201-300的数据包。也就是说接收端缺少了201-300的数据包,这种接收端区间丢失数据的情况被称为滑动窗口出现了洞(hole), 比如下面的图片展示了TCP滑动窗口中的:TCP滑动窗口洞
接着上面的例子继续说下去,此时服务端返回ACK报文时只能回复201,表示期望发送端发送编号从201开始的数据。对于发送端来说,除了要发送201-300的数据包之外,还需要发送301-400的数据包,这样会造成数据包重复传输,如果使用SACK可选项的话,接收端可以通过SACK通知发送端已经收到了301-400的数据包,告知客户端的301-400的数据包已经收到的元描述信息被称为SACK块(SACK Block), 一个SACK可选项可以包含多个SACK块, 对于发送客户端来说,解析SACK数据并结合ACK number就可以计算出需要重传数据包(这里是:201-300),而不需要全部重传。这样会大大提升TCP传输效率。

需要注意的是通讯双方需要协商确认是否使用SACK选项。而且SACK选项在握手阶段就需要确认,客户端在和服务端发起握手的SYN包中的可选项中声明SACK-Permitted选项,表示客户端支持SACK选项,同样的在服务端回复的SYN+ACK数据包中回复SACK-Permitted数据包。表示双方协商通过,可以使用SACK选项。Linux内核选项/proc/sys/net/ipv4/tcp_sack默认是开启状态。

TSOPT选项

TSOPT可选项标识为: 00001000, 长度为10个字节。格式如下:

+-------+-------+---------------------+---------------------+
|Kind=8 |  10   |   TS Value (TSval)  |TS Echo Reply (TSecr)|
+-------+-------+---------------------+---------------------+
1       1              4                     4

TSOPT选项也叫做timestamp选项,有时候会被简称为TSopt。和上面的WSOPT可选项一样。在RCF1323中为了改善长肥管道同样提出了TSOPT选项。当启用TSOPT选项时,发送方会在TSval字段设置一个时间戳,接受方会在TSecr字段将发送的时间戳返回回来。因为在接收端不会处理这个TSval而是直接从TSecr返回,因此在使用该选项时,通讯的双方并不需要做时间同步。发送的时间戳一般是单调增的值, RCF1323建议每秒增加1。在三次握手阶段SYN包因为无法获取对端的时间戳信息TSecr会默认使用0填充,TSval使用自己的时间戳填充。Linux内核选项/proc/sys/net/ipv4/tcp_timestamps可以设置是否启用TSOPT选项。

TSOPT选项包含两个用途,其中一个用途是根据ACK报文返回的TSOPT选项测量往返延时,简称RTTM(round-trip time measurement)。另外一个重要的用途就是上面前面提到的:防止TCP序列号复用问题(PAWS), 除了这两个重要的用途之外还用于实现SYN-CookieEifel Detection Algorithm等功能。

RCF7323文档中规定: TCP头部中的ACK标志位有效时TSecr才有效, 如果ACK标志位没有设置,发送端应该把TSecr设置为0。当发送的数据包中设置ACK标志位的情况下,发送方必须在TSecr中设置最后一个收到的TSval, 如果ACK标志位没有设置,接收方必须忽略TSecr字段。

在一个TCP会话中,TSOPT选项可以在SYN包中被发送,接收端在接收到SYN报文中解析到TSOPT选项时才会在SYN-ACK报文中回复TSOPT选项。一旦TCP会话中双方通过SYNSYN-ACK报文中协商好TSOPT选项之后,除非是包含RST标志的报文,否则TSOPT必须在会话中发送。 如果一个会话在协商好TSOPT选项后但是接受到的报文中不包含TSOPT选项,文档中规定应该丢弃报文(Linux在TCP协议栈实现上没有丢弃报文)。

TCP连接中有一种情况是通讯的双方都会发送SYN握手包,这种情况叫做同开。虽然这种情况很少,但是也是存在的,比如NAT穿透中这种情况会比较多(后面我会写一篇文章介绍TCP同开同关)。这种情况下可以在两端在回复报文SYN-ACK中包含TSOPT选项。