为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

零基础学透网络编程

原价 ¥ 58.00

立即订阅
07 不辱使命:可靠传输协议 TCP 的连接建立和终止过程
更新时间:2020-08-07 18:59:33
不安于小成,然后足以成大器;不诱于小利,然后可以立远功。——方孝孺

前言

从本文开始,我们要学习 TCP/IP 协议族中另外一个非常重要的传输层协议,叫做 TCP(Transmission Control Protocol),翻译成中文叫传输控制协议。TCP 是一个面向连接的、面向字节流的、可靠的传输层协议,有丢包重传机制、有流控机制、有拥塞控制机制。TCP 保证数据包的顺序,并且对重复包进行过滤。相比之前学过的不可靠传输协议 UDP,TCP 完全是相反的。对于可靠性要求很高的应用场景来说,选择可靠 TCP 作为传输层协议肯定是正确的。例如,著名的 HTTP 协议和 FTP 协议都是采用 TCP 进行传输。当然 TCP 为了保证传输的可靠性,引入了非常复杂的保障机制,接下来的三篇专栏会逐一剖析 TCP 的实现细节。本文作为三篇之首,主要包含如下内容:

  • 如何保证可靠性,主要介绍实现可靠传输的主要手段;
  • TCP Header 介绍,主要介绍 TCP 头的字段及其含义;
  • 关于序列号,主要介绍 TCP 是如何对字节进行编号的;
  • TCP 连接建立过程,主要是分析 TCP 三次握手过程;
  • TCP 连接关闭过程,主要是分析 TCP 连接终止的几种情况;
  • TCP 的状态迁移过程;
  • 关于 TIME_WAIT 的那些事儿;
  • TCP 实验案例,通过 Wireshark 抓包分析 TCP 连接建立和终止过程;
  • TCP 选项参数,主要介绍 TCP 选项格式以及常用的选项参数。

提示:

  • 在 UDP 的学习中,我们选择 iperf 做了 UDP 相关的实验,目的是为了向你介绍 iperf 的基本用法,并且强调他的重要性。iperf 普通的带宽监测、丢包、连通性探测是足够的,但是要用于 TCP 实现原理的研究,还是欠缺一些灵活。
  • 为此,从本文开始的 TCP 相关的三篇专栏我们要对 nwcheker 程序做一个修改,增加部分功能。关于功能细节、代码细节不做介绍,你只要会用即可。

那就让我们先从大家最为熟悉的打电话场景入手,分析一下实现可靠性传输的常用手段。

如何保证可靠性

保证传输的可靠性的例子,在生活中非常多,就拿大家最为熟悉的打电话场景来说,假设 Alice 想邀请好朋友 Bob 下班后共进晚餐、吐吐槽,我们设想双方对话场景大概如下图所示:

图片描述

简单分析一下这段对话。

  • Alice 首先问候 Bob。Bob 听到问候以后,礼貌答复了 Alice。Alice 听到 Bob 的答复以后,可以确定电话通畅,说了编号 3 和 编号 4 两句话;

  • 在打电话的时候,经常是一个人在说,而另外一个人要附和着“好,ok”之类的话语,以表示我正在听,并且都听到了

  • 可是 Alice 说了两段话后,没有听到对方的附和声,所以说出编号 5 的话语,目的是向对方确认电话是否通畅。对于 Bob 来说,自从互相问候以后,再没有听到 Alice 的声音,也是表达了编号 6 的内容,确认电话是否通畅;

  • 事实上可能是信号干扰的原因,编号 3 和 编号 4 的内容最终没有发送给 Bob,所以 Bob 肯定是没有听到声音;

  • 而编号 5 的内容可能经过了一段延迟最终达到了 Bob 这边,此时 Bob 又回复了编号 7 的内容表示我听到了你说的内容。但是,此时 Bob 并不能确定 Alice 还说过编号 3 和 编号 4 的话语。在 TCP 中,通过对每一个发送字节进行编号,这样接收端就可以判定中途是否有丢包发生;

  • 编号 7 的内容到达 Alice 以后,Alice 才知道之前白说了,再重复之前说的话。

尽管电话线路出现了故障,但是 Alice 和 Bob 用了一种可靠机制保证沟通最终还是完成了。仔细总结,包含以下几点:

  • 建立连接,Alice 和 Bob 在电话接通后,互相问候确保线路正常;

  • 超时重传,Alice 说了半天发现对方没有反馈,主动询问对方;当然 Bob 这边半天听不到对方的声音,主动询问,也是一样的道理;

  • 确认机制,说话一方在收到对方的答复以后认为交流是成功的。比如,TCP 中把这种机制叫做 ACK 机制;

  • 丢包重传,当 Alice 得知 Bob 并没有听到自己之前说的内容,再次进行重复。

其实在整个 TCP 的可靠策略中,也采用了类似以上几种机制,当然也包含许多更复杂的保障机制,比如:TCP 对发送的字节进行统一编号,通过接收方的 ACK 机制确保传输的每一个字节都达到了对方。类似的机制有很多,我们会一一介绍,下来首先了解一下 TCP Header 格式,因为 TCP 可靠性保障措施很大一部分体现在 TCP Header 中。

TCP Header 介绍

TCP Header 基本长度是 20 字节,TCP Header 支持选项字段。TCP Header 的基本字段加上选项字段,最大长度是 60 字节,字段格式的细节如下图所示:

图片描述

  • Source Port:占用 16 bit 长度,表示源端口号,用于唯一标识一个发送端应用程序;

  • Destination Port :占用 16 bit 长度,表示目标端口号,用于唯一标识一个接收端应用程序;

  • Sequence Number:占用 32 bit 长度,表示序列号。关于序列号,我们后面小节有详细介绍;

  • Acknowledgmengt Number:占用 32 bit 长度,表示接收端期望接收的下一个字节编号。接收端向发送端发送 ACK 的时候,会在 Acknowledgmengt Number 字段填上期望接收的下一个字节编号。发送端通过分析 ACK 报文,很容易知道接收端已经收到了哪些字节序列;

  • Header Length:占用 4 个 bit,表示 TCP Header 的长度,取值范围是 0 ~ 15,单位是 32 bit 字(word)的个数。由于 TCP Header 的长度是可变的,所以专门设置此字段。TCP 必须包含 20 字节的基本头,所以 Header Length 的最小值是 20 / 4 = 5;Header Length 最大值是 15,即最大长度是 60 字节,这样减去 20 字节长的基本头,最多可以包含 40 字节的选项字段;

  • Reserved:占用 3 个 bit,是保留位。随着 TCP 的不断优化,保留位从最早的 6 个 bit 变成了现在的 3 个 bit,越来越少;

  • NS:占用 1 个 bit,ECN-nonce 机制,是一个实验标准。可以参考 rfc 3540

  • CWR:占用 1 个 bit,是拥塞窗口(Congestion Window Reduced)减少的标志位,表示发送端降低了发送速率;

  • ECE:占用 1 个 bit,ECN Echo 机制,表示发送端之前收到了一个 ECN;

  • URG:占用 1 个 bit,表示 Urgent Pointer 字段有效,很少使用;

  • ACK:占用 1 个 bit,表示 Acknowledgmengt Number 被设置。除了主动发起的 SYN 报文段,所有 TCP 通信的报文段都会设置 ACK 标志位;

  • PSH:占用 1 个 bit,目前发送的带有数据的 TCP 报文段都会设置此标志位;

  • RST:占用 1 个 bit,用于连接异常关闭;

  • SYN:占用 1 个 bit,TCP 在连接建立的过程中需要设置此标志位;

  • FIN:占用 1 个 bit,TCP 在连接关闭的过程中需要设置此标志位;

  • Window Size:占用 16 个 bit,用于向对方通告自己接收窗口的大小,一般都是剩余接收缓冲区的大小;

  • Checksum: 占用 16 个 bit,TCP 检验和是用于差错检测的,检验计算包含 TCP Header 和 TCP Data 两部分;

  • Urgent Pointer:占用 16 个 bit,紧急指针(Urgent Pointer)准确的说应该叫做紧急数据偏移量(Urgent Offset)。Urgent Pointer 与 Sequence Number 相加得到的是紧急数据最后一个字节的序列号,或者是紧急数据最后一个字节的下一个字节的序号。

选项参数是变长的,后面小节详细介绍。

从 TCP Header 格式我们可以看出,TCP 包含了连接建立、流控、拥塞控制等很多机制,本文我们主要介绍 TCP 的连接建立和关闭机制。

关于序列号(Suquence Number)

我们说 TCP 是面向字节流的传输协议,主要是表现在传输的每一个字节都进行了编号。现在我们需要回想一下 nwchecker 程序,他是一个 Client/Server 程序,Client 每隔 3 秒向服务端发送一个 “ping” 消息,服务端应答一个"pong" 消息,传输层协议是 TCP,现在通过一张图展示一下 TCP 是如何对传输的字节进行编号的。

图片描述

Client 每隔 3 秒发送一个 “ping” 消息,图中展示了编号为 (1),(2),(3)的三个消息,应用层的消息都是有边界的。

当应用层的消息发送到 TCP 层以后,TCP 会将这些消息进行统一编号,并且保存在 TCP 发送缓冲区中,TCP 并不会保留应用层的消息边界,如图中对三个消息的编号。TCP 根据发送流量控制,选择要发送多少字节的数据,我们把 TCP 发送的字节流叫做 Segment,也叫 Payload。TCP 会在每一个 Segment 前面附加一个 TCP Header,然后再发送给网络层

我们假设 TCP 预先建立好了一个连接,这是 TCP 在网络层之上建立的一个虚拟网络连接管道,在此管道里传输的是带有 TCP Header 的字节流。

注意:Segment 是不包含 TCP header 的,只是应用层的数据。TCP Header 部分是不进行编号的。

我们把 TCP Header 的序列号(Seq No)字段用蓝色高亮表示,我们知道 TCP Header 的序列号字段的取值是 Payload 的第一个字节的编号,Payload 中后续字节的编号是依次加 1 的。那么:
编号为(1)的“ping”消息,序列号字段填 1,字节编号依次是 1,2,3,4;
编号为(2)的“ping”消息,序列号字段填 5,字节编号依次是 5,6,7,8;
编号为(3)的“ping”消息,序列号字段填 9,但是网络连接管道只发了 “pin”,编号也是 9,10,11,并没有发送字符 ‘g’。

这就是 TCP 是面向流的一个非常典型的场景。TCP 并不保留应用层的消息边界,TCP 根据发送流量的限制,灵活选择发送的 Segment 的大小。对于编号(3)的“ping”消息,会被 TCP 发送两次。

TCP 连接是一个虚拟管道,在两台主机通信之前,需要双方协商建立 TCP 连接,下来我们看一下 TCP 连接的建立过程。

TCP 连接建立过程

你还记得 IP 可以唯一确定一台主机,端口号(Port)可以唯一确定一台主机上的应用程序吗?通常 <IP , Port> 叫做 Endpoint,一个 Endpoint 可以唯一确定一个应用程序。

TCP 连接是在两个互相通信的应用程序之间建立的,通常用四元组 <源 IP,源端口号,目标 IP,目标端口号> 来标识一个 TCP 连接。TCP 通信是主机上的应用程序之间的通信,也叫进程间通信

TCP 连接通常是由客户端(Client)主动发起,服务端(Server)被动做出响应,经过三次握手以后,可以确定连接的建立。具体过程如下图所示:

图片描述

三次握手的过程如下:

  • 一般是由客户端主动发送一个设置了 SYN 标志位的报文段,TCP Header 需要设置的字段包括:

    1. 序列号(Seq),注意不是 0, 而是初始序列号 ISN;
    2. 确认序列号,主动发起方要填 0,因为没有要确认的序列号;
    3. 窗口(Window),需要向对方通告自己的接收窗口大小;
    4. 一些选项参数:最大报文段长度(MSS),是否启用选择性 ACK(SACK)机制志,窗口比例因子(WScale)。
  • 服务端回应一个设置了 SYN 和 ACK 标志位的报文段,关于 TCP Header 的字段设置,除了需要将确认序列号(ACK_Seq) 设置成客户端的序列号+1 以外,其他字段都和客户端的一致;

  • 客户端回应一个设置了 ACK 标志位的报文段,序列号需要设置成客户端 ISN + 1,确认序列号需要设置成服务器 ISN + 1。

通过如上三次交互以后,TCP 连接就建立成功,通常把 TCP 连接建立过程叫做三次握手。客户端经常会发送第一个 SYN 报文段,叫做主动打开;服务器会接收此 SYN 报文段,并且发送下一个 SYN 报文段,叫做被动打开。当 TCP 连接建立好以后就可以进行正常的通信了。

下来我们解释一下三次握手中,ISN 为什么不能为 0?

初始序列号 ISN 介绍

初始序列号(ISN,Initial Sequence Number)是随机生成的,是不能为 0 的,生成算法由协议栈实现来决定,作用如下:

防止网络丢包在同一个连接的不同替身之间混传,造成错误。
比如:由于网络拥塞,导致 TCP 连接断开,但是一部分报文段依然流落在网络中,可能会继续传输。TCP 断线往往需要重新建立连接,新建立的连接很可能和老连接的四元组是相同的,及具有相同的 <源 IP,源端口号,目标 IP,目标端口号> ,我们把具有相同四元组的连接叫做彼此的替身。当老连接的报文段在新连接上传输,会导致新连接的报文处理出现错误。如果前后两个连接的 ISN 不同,那么老连接的报文段对新连接的影响会几乎没有,因为序列号差距很大,会被丢弃掉。

防止网络攻击
比如:恶意破坏行为可以在某一个 TCP 连接上发送数据包,对连接造成破坏。如果 ISN 很难被猜测到,那么这种破坏就一定程度可以避免。

当 TCP 数据传输完成后,需要关闭连接、释放资源,下来我们了解一下 TCP 连接的拆除过程。

TCP 连接拆除

TCP 连接的关闭一般是由客户端主动发起,因为通常的工作流程是客户端主动发起连接,工作完成以后主动关闭连接。当然,服务器也会主动关闭连接,比如服务器检测到连接超时,也要关闭连接的。关闭连接需要通过 close() 函数来完成。在连接关闭过程中,可能会出现三种场景,第一种场景如下图所示:

图片描述

图中交互过程解释如下:

  • 客户端发送了一个设置了 FIN 和 ACK 标志位的报文段,当然服务端也可以主动发起关闭。序列号是服务端所希望收到的下一个字节序号,确认序列号是客户端所希望收到的下一个字节的序号;

  • 服务端收到带有 FIN 标志的报文段以后,首先向对方回应一个 ACK,并且向应用程序通知“连接被对方关闭”的事件;

  • 服务端发送一个设置了 FIN 和 ACK 标志位的报文段,同样执行一个主动关闭的逻辑;

  • 客户端收到带有 FIN 标志的报文段以后,发送一个设置了 ACK 标志位的报文段。

经过以上四次交互,TCP 的连接就被终止了,通常被叫做“四次挥手”。

你可能已经发现,服务端收到 FIN 报文段以后,并没有马上发送 FIN,而是先向对方回应一个 ACK。因为 TCP 必须要确保本端发送缓冲中的数据发送完才能关闭连接。TCP 是全双工通信,所谓全双工通信是指某个 Endpoint 同时可以进行数据收发,当一个方向数据发送完以后,可以发送 FIN 关闭这个方向的通道,而另外一个方向还可以继续进行数据收发,这就是所谓的半关闭

TCP 连接终止的第二种场景如下图所示:

图片描述

从图中可以看出,客户端和服务端同时发起了关闭连接的动作,双方都是发送了带有 FIN + ACK 标志位的报文段,上方都对对方进行了响应,也是经历了“四次挥手”。这种情况也是比较普遍,比如通信双方都检测到了连接超时,同时发起关闭操作。

TCP 连接终止的第三种场景如下图所示:

图片描述
从图中可以看出,客户端主动发送了带有 FIN + ACK 标志位的报文段,服务端收到以后也发送了 FIN + ACK 的报文段,最后客户端对服务端进行了确认,我们发现经历了“三次挥手”。这种情况一般出现在服务端没有数据要发送,基本类似我们之前的 nwchecher 程序,服务器只是做出 echo 的情况。

我们已经了解了 TCP 连接建立和拆除过程,伴随着这些过程的进行,TCP 的内部状态也会发生一些变化,这个状态维护在 TCP 的传输控制块(TCB,Transmission Control Block)中。TCB 就是 C 语言中的一个结构体(struct),此结构体包含了很多字段,其中很重要的一个字段就是连接状态(Connection state),用于维护 TCP 连接状态的。下面我们就看一下 TCP 的状态迁移过程。

TCP 的状态迁移过程

TCP Socket 创建好以后,初始状态是 CLOSED 状态,当 TCP 调用具体的 Socket 函数,或者接收到数据包以后,会根据当前的状态做出不同的动作,同时状态也会发生迁移,我们把这个迁移过程叫做 TCP 的状态机,如下图所示:

图片描述

状态机图中包含了 TCP 主动打开和被动打开所经历的状态变迁过程,图中的黑色虚线表示被动打开和关闭的状态变迁过程,服务器执行的是被动打开过程;图中黑色、红色、蓝色实线表示主动打开和关闭过程的状态迁移过程,客户端执行的是主动打开过程。

结合我们前面小节介绍的三次握手和三种情形的关闭过程,此图应该很好理解吧?

我们先分析一下连接建立的状态迁移过程:

  • 当你调用 socket() 函数建好一个 TCP Socket 以后,初始状态是 CLOSED 状态;
  • 对于服务端来说,需要调用 bind() 、listen() 函数,状态变迁为 LISTEN 状态;对于客户端来说,需要调用 connect() 函数连接服务器,此时会发送 SYN 报文段,状态变迁为 SYN_SENT 状态;
  • 服务端收到 SYN 报文段,会发送 SYN + ACK 报文段,并且将状态变迁为 SYN_RCVD 状态;
  • 客户端收到 SYN + ACK 报文段,发送 ACK 报文段,并且将状态变迁为 ESTABLISHED 状态。对于客户端来说,连接就建立后了,可以通过 send() 函数发送数据了;
  • 服务端收到 ACK 报文段,将状态变迁为 ESTABLISHED 状态,服务端的连接也建立好了,通知应用层通过 accept() 函数获取新连接。

下来在看一下连接拆除的状态迁移过程,TCP 关闭过程的状态机我们画出了三种关闭场景,我们就简单分析一下四次挥手吧。

  • 一般是由客户端调用 close() 函数主动发起连接关闭的请求,此时 TCP 协议栈会发送 FIN 报文段,并且将状态变迁为 FIN_WAIT_1 状态;

  • 服务端接收 FIN 报文段,发送一个 ACK 报文段,并且将状态变迁为 CLOSE_WAIT 状态,然后在通知应用层“对方关闭连接”的事件;

  • 客户端收到 ACK 报文段以后,将状态变迁为 FIN_WAIT_2 状态,等待对方的 FIN 报文段;

  • 服务端的应用程序调用 close() 函数,TCP 协议栈发送 FIN 报文段,并且将状态变迁为LAST_ACK 状态;

  • 客户端收到 FIN 报文段,发送 ACK 报文段,将状态变迁为 TIME_WAIT 状态。此时 Socket 不能马上转入 CLOSED 状态,需要等待 2MSL 时间以后,才能转入 CLOSED

  • 服务端收到 ACK 报文段以后,转入 CLOSED 状态。

经历这样一个过程,TCP 连接就走完了由生到死的历程。关于 TIME_WAIT 状态的故事,几乎是面试的必考题,所以我们下来再了解一下关于 TIME_WAIT 的故事。

注意:

我们说过客户端和服务器都可以进行主动关闭的操作,我们是以为客户端主动关闭为例进行分析的。

提示:

TCP 也支持同时打开,但是在实际工作中很少遇到,所以我们在图中没有提到,本专栏文章也不会介绍同时打开的情况。

关于 TIME_WAIT 的那些事儿

关于 TCP 的 TIME_WAIT 状态,在工作中经常会谈论,尤其是做服务器开发的程序员,经常会遇到 TIME_WAIT 导致连接失败的问题。最典型的场景是早期 HTTP 没有支持 Keep-alive 特性,一次 HTTP 响应完成后,HTTP 服务器就要关闭连接,会导致 TIME_WAIT 状态出现。如果服务器在短时间内关闭大量连接,很可能会导致后续的请求失败。作者曾经的一款产品采用 Apache 服务器作为 Web Server,由于没有使用 Keep-alive,短时间内频繁的 HTTP 请求,导致 HTTP 请求超时。

每当在工作中或者是面试中谈及此问题时,很多人会模棱两可、人云亦云,最终给人的感觉就是谈虎色变,反正是这玩意儿很邪门儿,最好不要遇到。下来,我们就尝试分析一下此问题。

首先,我们从 TCP 状态机图中可以看到,只要是执行主动关闭的一方(不管是客户端还是服务器),最终都会进入 TIME_WAIT 状态,我们可以说:“ TCP 就是这么设计的,主动关闭一方必然会导致 TIME_WAIT 状态的出现,没什么可怀疑的”。

其次,之所以 TIME_WAIT 状态会导致问题出现,是因为 TCP 规定 Socket 进入 TIME_WAIT 状态以后,必须等待 2MSL 时间,才能转入到 CLOSED 状态。MSL 是最大报文段存活时间(Maximum Segment Lifetime),是指报文在网络中的存活时间。

记得我们在学习 IP 报文格式的时候讲过:“IP 报文头中有一个 TTL(Time To Live) 字段,用于指定报文可以被路由器转发的次数,报文每被路由器转发一次,TTL 就会减 1,TTL 减到 0,路由器就会丢弃此报文”。这就是报文在网络中存活的意思。RFC 793 中建议 MSL 设定为 2 分钟,但是不同协议栈实现都有差异。比如,Linux 协议栈硬编码写死 1 分钟。

现在你想想,就拿 Linux 系统来说,MSL 是 1 分钟,那么 2MSL 就是 2 分钟。如果在 2 分钟内有大量的连接建立又被关闭,那就产生了大量的状态为 TIME_WAIT 的 Socket。我们先看一张图:

图片描述

这是互联网上非常典型的一种架构,WEB 服务器往往是处于前端代理的后面,互联网的 HTTP 请求往往是通过前端代理转发到 WEB 服务器的,他们之间也需要建立 TCP 连接。你还记得用四元组<源 IP,源端口号,目标 IP,目标端口号> 表示一条 TCP 连接吗?仔细观察图中的情形,不管是 WEB 服务器主动关闭连接还是前端代理主动关闭连接,都会导致大量 TIME_WAIT 出现,都会导致后续的连接请求失败,能理解吗?

另外,大量的 TIME_WAIT 状态的 Socket 会占用大量的内核资源,对系统的内存耗费很大,会降低服务器性能。

既然此问题无法回避,那只能积极面对了。到底该如何解决此类问题呢?大概有如下几个办法:

  • 调整客户端的本地端口范围,可以绑定更多端口;或者是客户端可以配置更多 IP;

提示:在 Linux 系统可以设置 net.ipv4.ip_local_port_range 改变本地端口范围。

  • 让服务器能够监听更多端口;或者服务器可以配置更多的 IP(就是要多网卡);
  • 如果双方不在意关闭连接后导致数据丢失的问题,可以设置 no linger,这样应用层调用 close() 函数的时候,会向对方发送 RST,连接会被立即释放掉,避免 TIME_WAIT 出现;

提示:在 Linux 系统可以通过设置 SO_LINGER 选项达到此目的。

  • 最后一个不推荐的解决办法就是设置系统参数,回收处于 TIME_WAIT 状态的 Socket。

提示:在 Linux 系统可以通过设置 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_tw_recycle 两个参数达到目标,具体可以参考相关手册。

你已经知道了 TIME_WAIT 是怎么产生的,也知道了 2MSL 等待带来的问题,我们也介绍了解决办法。那么你是否有疑问为什么 TCP 要引入 2MSL 等待这个机制呢?其实,主要原因有两个:

  1. 假如一个客户端通过端口号 1024 和一个监听了 80 端口号的服务器建立了 TCP 连接,并且发送了一个报文段。此时客户端关闭了连接,服务器也关闭了连接。如果此客户端刚好用端口号 1024 向监听了 80 端口号的同一台服务器又发起新的 TCP 连接,那么前后两次连接的四元组是相同的,能理解吗?如果老连接发的报文段在网络中转了一大圈,在新连接中被服务器收到,你想想会是什么场景呢?如果老连接关闭的以后等等一个 2MSL 时间,那么可以一定程度避免此问题;

  2. 当 TCP 进行四次握手关闭的时候,最后一个 ACK 可能会被丢弃,这时候要给对方一个重传 FIN 报文段的时间,所以要等待 2MSL 时间。

到此,我们已经介绍完了 TCP 连接管理的细节,下来就通过实验感受一下吧。

TCP 实验案例

你还记得我们的 nwchecker 程序吗?现在用他来做一个实验,观察 TCP 连接的建立和终止过程。为了实验方便,我对程序进行了一个简单的修改,主要包含如下内容:

  1. 增加 nwchecker.h 和 nwchecker.c 两个文件,通过 getopt_long 函数支持命令行选项的功能;
  2. 将原有 nwchecker_client.c 和 nwchecker_server.c 中的 main 函数统一移到 nwchecker.c 中,也就是说我们以后只需要编译生成一个文件即可。

修改后的代码保存在"imooc-sock-core-tech/02-06_可靠传输协议_TCP_连接建立和终止"目录下面。

本次实验的环境配置如下:

  • 将代码进行编译,生成 nwc 可执行程序;
  • Ubuntu 18.04 运行 nwc server;
  • Windows 10 cygwin 运行 nwc client;
  • 通过 Wireshark 抓包分析连接建立和关闭过程。

nwc Client 主动发起连接请求,发送的带有 SYN 标志位的报文段内容如图中所示:

图片描述

报文段中包含的重要信息如下:

  • 序列号是 ISN 值,不是 0。Wireshark 只是为了显示直观,采用了相对序列号,所以我们看到的是 0,这个可以通过报文内容来确定,我在图中最下面的报文内容部分用红框做了说明;

  • 连接建立过程,需要向对方通告窗口大小。比如图中的 8192,单位是字节

  • 在选项参数中向对方通告了最大报文段(MSS)的大小,是 1460。这个值是不是 1500-20-20 的结果;

  • 窗口比例因子。在 TCP 通信的过程中,双方要向对方实时通知自己的接收窗口大小,窗口大小的计算是用 Window size << Window scale 来最终确定的;

  • 通知对方开启 SACK 机制,现代 TCP 都支持此功能。

nwc Server 被动对连接请求做出响应,发送了带有 SYN 和 ACK 标志位的报文段,报文段的内容和主动发起类似的,不再赘述,细节可以观察下图:

图片描述

Wireshark 支持很多统计功能,比如你想查看 TCP 数据流的统计情况,可以在 Wireshark 上做如下操作:

  • 选择菜单 “Statistics”
  • 在弹出下拉菜单选择 “Flow Graph”
  • 在弹出对话框选择“TCP Flow”
  • 然后点击“OK”按钮。

就可以看出如下图所示的统计信息:

图片描述

这个统计信息还可以保存成文本,非常直观。

我们已经多次提到了 TCP 的选项参数,下来就看看 TCP 所支持的几种选项参数。

TCP 选项参数

TCP 的选项格式可以说是一个 TLV(类型、长度、值) 结构,是一个可变长度的结构,如下图所示:

图片描述

  • Kind 是指定选项的种类,占用 1 个字节长度;
  • Length 指定选项的总长度,取值包含了 Kind,Length,Value 占用的长度。Length 本身是占用 1 个字节长度;
  • Value 是选项值占用的长度,具体由 Length 字段来决定。

TCP 支持的选项参数如下:

种类(Kind) 长度(Length) 描述(Description) 取值(Value)
0 1 EOL 没有值,表示选项结束
1 1 NOP 没有值,用于 padding
2 4 MSS 能接受最大报文段大小
3 3 WSOPT 窗口比例因子
4 2 SACK 标志 是否支持 SACK,有次选项代表支持 SACK
5 变长 SACK 具体的序列号区间
8 10 TSOPT 时间戳选项

我们这里列出常用选项,部分选项在后面专栏会详细介绍。

总结

本文我们通过生活中的打电话场景,总结了实现可靠传输的常用手段。接着我们分析了 TCP Header 格式,介绍了 TCP 连接建立和终止过程。

我们也详细介绍了 TCP 序列号的工作原理,我们说过序列号只是针对 TCP Payload 进行编号的。但是有一个例外,设置了 SYN 、SYN+ACK、FIN 三种标志位的报文段也需要占用 1 个序列号,虽然他们的 Payload 长度是 0。这个一定要谨记!另外,SYN 报文段中的序列号不是 0,是 ISN,这也需要注意。

最后我们把 nwchecker 程序进行了简单的重构,目的是方便做实验,通过 Wireshark 抓包分析了 TCP 连接的建立和终止过程。

最后对 TCP 的选项参数进行了一个介绍。由于 TCP 的实现细节繁杂,我们在后面两篇文章中会继续探讨。

思考时间

  1. 对于仅设置 ACK 标志位,并且 Payload 长度是 0 的报文段,会占用序列号吗?

  2. 在 TCP 三次握手过程中,假设第三次 ACK 报文段在网络中丢失,会出现什么情况?

  3. 尝试做一个 TCP 三次握手的实验,假设连接过程中出现了超时,请通过 Wireshark 抓包分析 TCP 三次握手过程中发生的超时处理机制。

}
立即订阅 ¥ 58.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

零基础学透网络编程
立即订阅 ¥ 58.00

举报

0/150
提交
取消