Transmission Control Protocol(TCP) gaunthan Posted on May 14 2016 ? Transport Layer ? ? Computer Networking ? ## 概述 **TCP**(Transmission Control Protocol,传输控制协议)是一个**面向连接(connection-oriented)、可靠数据传输(reliable data transfer)**的协议,它显式定义了连接建立、数据传输以及连接拆除阶段来提供面向连接服务。 <!--more--> TCP使用*GBN(回退N帧)*和*SR(选择性重复)*协议的组合来提供可靠性,使用序号、累积确认、差错检测以及超时重传操作。TCP是因特网中最常用的传输层协议。[^Transport Protocol] [^Transport Protocol]:[传输层通用协议](http://gaunthan.leanote.com/post/%E4%BC%A0%E8%BE%93%E5%B1%82%E9%80%9A%E7%94%A8%E5%8D%8F%E8%AE%AE). ## TCP报文段 TCP中的分组称为**报文段**(segment),简称段。段包含20字节到60字节的头部,接着是来自应用程序的数据。如果没有选项,那么头部是20字节;如果有选项,最多是60字节。 报文段的格式如下图所示:  字段|说明 --|-- 源端口地址|一个16位的字段,定义了在主机中发送该段的应用程序的端口号。与UDP头部的源端口地址的作用一样。 目的端口地址|一个16位的字段,定义了在主机中接收该段的应用程序的端口号。与UDP头部的目的端口地址的作用一样。 序号|一个32位的字段,定义了一个数,该数分配给段中数据的第一个字节。TCP是一种**字节流传输协议**,为了确保连通性,它对发送的每一个字节都进行编号。序号告诉目的端,在这个序列中哪一个字节是该段的第一个字节。在连接建立时,每一方都使用随机数生成器来产生一个**初始序号**(initial sequence number,ISN),通常每一个方向的ISN都不同。其他段的序号是之前段的序号加之前携带的字节数。随机产生的初始序号减少了将那些仍在网络中存在的来自两台主机之间先前已经终止的连接的报文段,被误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性(它碰巧与旧连接使用了相同的端口号)。 确认号|一个32位的字段,定义了段的接收方期望从对方接收的字节号。如果段的接收方成功接收了对方发来的字节号x,它就将确认号定义为x+1,确认和数据可**捎带**一起发送。 头部长度|一个4位的字段,指明了TCP头部中共有多少个4字节长的字。头部的长度可以在20字节到60字节之间。因此,这个字段的值在5到15之间。 控制字段|一个6位的字段,定义了6种不同的控制位或标记。在同一时间可以设置一位或多位。这些位用在TCP的流量控制、连接建立和终止、连接失败和数据传送方式等方面。6个字段及其含义如下:<br /> URG 紧急指针有效 <br /> ACK 确认有效 <br /> PSH 请求推送 <br /> RST 连接复位 <br /> SYN 同步序号 <br /> FIN 终止连接 窗口大小|一个16位的字段,定义了本数据包发送者的**接收窗口**(rwnd)的大小(以字节为单位),最大值为65535。数据发送方根据这个数据来计算自己最多能发送多长的数据。 校验和|一个16位的字段,包含了校验和。对TCP来说,将校验和包含进去是强制的,起相同作用的伪头部也被加到段上。对于TCP伪头部而言,协议字段的值为**6**。 紧急指针|一个16位的字段,当紧急标识URG置位时才有效。它定义了一个偏移量,将该量加到序号上就得出该段数据部分中最后一个紧急字节。 选项|用于发送方与接收方协商最大报文段长度(MSS),或在高速网络环境下用作窗口调节因子。首部字段中还定义了一个时间戳选项,可参见RFC 854和RFC1323了解其他细节。 在实践中,PSH、URG和紧急指针都没有使用。 ## TCP报文段的寿命 由于TCP底下是一个网络,网络信道可被看成基本上是在缓存分组,并在将来任意时刻自然地释放出这些分组。由于序号可以被重新使用,可能会导致冗余分组。实际应用中采用的方法是,为了确保一个序号不被重新使用,直到发送方“确信”任何先前发送的序号为x的分组都不再存活于网络中为止。还存在一种使用序号的方法,它也可以完全避免这个问题。 与一个与TCP报文段的寿命相关的名词——**MSL**(Maximum Segment Life,最大段寿命)。[^MSL]最大段寿命是一个TCP分段可以存在于网络中的最大时间。它被任意地定义为两分钟长。而在高速的TCP扩展中,它被假定为大约3分钟。 RFC 793 中强调 TIME_WAIT 状态必须是两倍的 MSL 时间(max segment lifetime),在 linux 上,这个限制时间无法调整,写死为 1 分钟了,定义在 include/net/tcp.h[^TCP_TIME_OUT]:  只能通过重新编译内核来修改这个值。 [^TCP_TIME_OUT]:[不要在 linux 上启用 net.ipv4.tcp_tw_recycle 参数](http://www.cnxct.com/coping-with-the-tcp-time_wait-state-on-busy-linux-servers-in-chinese-and-dont-enable-tcp_tw_recycle/) ## 最大报文段长度 TCP可从发送缓存中取出并放入报文段中的数据数量受限于**最大报文段长度**(Maximun Segment Size, MSS)。MSS通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的**最大传输单元**(Maximum Transmission Unit, MTU))来设置。设置该MSS要保证一个TCP报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。以太网和PPP链路层协议协议都具有1500字节的MTU,因此MSS的典型值为**1460字节**。 注意MSS是指在TCP报文段里应用层数据的最大长度,而不是整个TCP报文段的最大长度。 ## TCP服务 ### 进程到进程通信 像UDP一样,TCP通过使用端口号来提供进程到进程通信。 ### 流传输服务 与UDP不同,TCP是一个**面向流**的协议,它允许发送进程以字节流形式传递数据,并且接收进程也以字节流形式接收数据。 因为发送进程和接收进程可能以不同的速度写入和读出数据,所以TCP需要用于存储的**缓冲区**。每一个方向都存在一个缓冲区:发送缓冲区和接收缓冲区。 IP层作为TCP服务的支持者,需要以分组的方式而不是字节流的方式发送数据。在传输层,TCP将多个字节组合在一起成为一个分组,这个分组称为**段**。TCP给每个段添加头部,并将该段传递给IP层。段被封装到IP数据报中,然后再进行传输。 ### 全双工通信 TCP提供**全双工通信**(full-dupler service),即数据可以在同一时间双向流动。全双工通信使用了**捎带**(piggybacking)技术。[^piggybacking] [^piggybacking]:[双向协议:捎带](http://gaunthan.leanote.com/post/传输层通用协议#双向协议:捎带). ### 面向连接的服务 与UDP不同,TCP是一种面向连接的协议。通信步骤为:连接建立、交换数据、连接拆除。面向连接意味着通信过程是同步的,是双方互相协调的,而不像UDP那样是单方面的通信。 ### 可靠数据传输 TCP使用确认机制来检查数据是否安全完整地到达接收方。 ### 流量控制 一条TCP连接每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、按序的字节后,它就将数据存入该接收缓存。相关联的应用进程会从该缓存中读取数据,但不一定是数据一到达就去读取它。如果某应用进程读取数据时相对缓慢,而发送方发送得太多、太快,发送的数据就会很容易使接收方的接收缓存溢出。这其实是一个生产者——消费者问题。这意味着我们需要一种同步技术来协调生产速度。 TCP通过让发送方维护一个称为**接收窗口**(receive window)的变量为它的应用程序提供**流量控制服务**(flow-control service),以消除发送方使接收缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用进程的读取速率相匹配。 接收窗口用于给发送方一个指示——该接收方还有多少可用的缓存空间。因为TCP是全双工通信,因此在连接的两端都各自维护一个接收窗口。 ### 拥塞控制 **拥塞控制**(congestion control)和流量控制采取的动作非常相似(对发送方的遏制),但是它们其实是针对完全不同的原因而采取的措施。 当网络变得拥塞时,往往由于路由器缓存溢出引起丢包。由于分组丢失,TCP协议会在超时后重传分组。分组重传因此可以作为网络拥塞的征兆来对待(注意,在高带宽但错误率高的链路上发生丢包,显然不该作为网络拥塞的征兆,Linux 4.9引入的BBP拥塞避免算法正是基于这一考虑的),但是却无法处理导致网络拥塞的原因,因为有太多的发送源已过高的速率发送数据。为了处理网络拥塞原因,需要一些机制以在面临网络拥塞时遏制发送方: - **端到端拥塞控制**。网络层没有为传输层拥塞控制提供显示支持。即使网络中存在拥塞,端系统也必须通过对网络行为的观察(如分组丢失与时延)来推断之。 - **网络辅助的拥塞控制**。在网络辅助的拥塞控制中,网络层构件(即路由器)向发送方提供关于网络中拥塞状态的显示反馈信息。这种方式下,拥塞信息从网络反馈到发送方有两种方式:网络路由器直接把反馈信息发送给发送方;路由器标记或更新分组中的某些字段以指示拥塞的发生,随后把分组交给接收方,由接收方向发送方反馈拥塞信息。 TCP运行在IP协议之上,因为IP层不会向端系统提供有关网络拥塞的反馈信息,因此TCP必须通过端到端的方法解决拥塞控制。TCP为发送方维护一个额外变量——**cwnd**(congestion windows),代表拥塞窗口的大小。同时规定在一个发送方中未被确认的数据量不会超过cwnd与rwnd(接收窗口)中的最小值,即: $$LastByteSent - LastByteAcked \le min \{cwnd, rwnd\}$$ 通过限制发送方中未被确认的数据量,TCP间接地限制了发生方的发送速率。 **TCP拥塞控制算法**(TCP congestion control algorithm)包含三个主要部分:慢启动、拥塞避免和快速恢复。慢启动和拥塞避免是TCP的强制部分,两者的差异在于对收到的ACK做出反应时增加cwnd长度的方式。快速恢复是推荐部分,对TCP发送方而言并非是必需的。 TCP拥塞控制可以被总结为:每个**RTT**(Round Trip Time,往返时间)内cwnd线性(加性)增加1MSS,然后出现3个冗余ACK事件时cwnd减半(乘性减)。因此,TCP拥塞控制常常被称为**加性增、乘性减**(Additive-Increase, Multiplicative-Decrease, AIMD)拥塞控制方式。AIMD拥塞控制引发了下面中所示的“锯齿”行为:  *(图片来源:[Dynamics of AIMD For A Single Flow](https://www.keyboardbanger.com/aimd-for-a-single-flow/))* #### 慢启动 在**慢启动**(slow-start)状态,cwnd的值以1个MSS(最大报文段长度)开始并且每当传输的报文段首次被确认就增加一个MSS。因此,TCP发送方速率起始慢,但却以指数增长:每经过一个**传输轮次**(transmission round),cwnd就加倍。 有几种方式可以确认指数增长的结束时机: - 存在一个由超时指示的丢包事件(即拥塞),TCP将cwnd设置为1并重新开始慢启动过程。同时设置变量**ssthresh**(slow-start thresh, 慢启动阈值)的值为cwnd/2,即当拥塞发生时将ssthresh置为拥塞窗口的一半。 - 当cwnd的值等于ssthresh时,TCP结束慢启动并且转移到拥塞避免模式。在这种模式下,cwnd以线性缓慢增长。 - 如果检测到3个冗余ACK,TCP执行快速重传并进入快速恢复状态。 ##### TCP 分岔:优化云服务性能 对于诸如搜索、电子邮件和社交网络等云服务,非常希望提供高水平的响应性,给用户一种完美的印象,即这些服务运行在它们自己的端系统中。因为用户经常位于远离数据中心的地方,而这些数据中心负责为云服务关联的动态内容提供服务。实际上,如果端系统远离数据中心,则RTT将会很大,TCP慢启动将潜在地导致低劣的响应时间性能。 缓解这个问题和改善用户感受到的性能的一个途径是: 1. 部署临近用户的前端服务器; 2. 在该前端服务器利用TCP分岔(TCP splitting)来分裂TCP连接。  *(图片来源:[Satellite Communications in the Global Internet: Issues, Pitfalls, and Potential](https://www.isoc.org/inet97/proceedings/F5/F5_1.HTM))* 借助于TCP分岔,客户向临近前端连接一条TCP连接,并且该前端以非常大的窗口向数据中心维护一条TCP连接。TCP分岔大约能够将网络时延从4倍RTT减少到RTT(如果服务器在慢启动期间交付响应要求三个TCP窗口),同时也有助于减少因接入网丢包引起的TCP重传时延。今天,Google和Akamai在接入网中广泛利用了它们的CDN服务器,为它们支持的云服务来执行TCP分岔。 #### 拥塞避免 一旦进入拥塞避免状态,cwnd的值大约是上次遇到拥塞时的值的一半(cwnd当前等于ssthresh,当ssthres的值是上次拥塞时的拥塞窗口大小的一半),即距离拥塞可能并不遥远。因此,TCP无法每过一个RTT就将cwnd翻倍,而是采用了一种较为保守的方法,每个RTT只将cwnd的值增加一个MSS(MSS/cwnd)字节。因此,如果在1个RTT内发送了10个报文段,则在收到对所有10个报文段的确认后,拥塞窗口的值才增加1个MSS。 那么何时应当结束拥塞避免的线性增长呢?当出现超时事件时,TCP的拥塞避免算法执行与慢启动算法一样的行为:置ssthresh为cwnd值的一半,然后将cwnd的值设置为1个MSS,重新开始慢启动过程。 此外,如果收到三个冗余ACK,拥塞避免执行与慢启动一样的操作:TCP执行快速重传并进入快速恢复状态。 #### 快速重传 快速重传算法首先**要求接收方每收到一个失序的报文段后就立即发出重复确认**(使发送方及早知道有报文段没有到达对方)而不是等待自己发送数据时才进行稍带确认。 快速重传算法规定,发送方只要一连收到三个重复确认就立即重传对方尚未收到的报文段,而不是等待为该报文段设置的计时器超时。由于发送方能够尽早重传未被确认的报文段,因此采用快速重传后可以使整个网络的吞吐量提高约20%。 #### 快速恢复 快速恢复是与快速重传配套使用的算法。当发送方连续收到三个重复确认时,就执行“乘性减”算法,将cwnd的值减半,然后设置ssthresh为当前cwnd的值(即此时ssthresh具有和cwnd一样的值)。对于引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余的ACK,cwnd的值增加一个MSS。 注意,有的快速恢复算法实现是把开始时的拥塞窗口的值再增大一些,即等于$ssthresh+3*MSS$。这样做的理由是:既然发送方收到这些重复确认,则表明这些冗余分组已经离开了网络,因为它们已经到达了接收方的接收缓存。因此我们可以适当地增大拥塞窗口的值。 最终,当对丢失报文段的一个ACK到达时,TCP在降低cwnd(设置cwnd为当前ssthresh的值的一半)后进入拥塞避免状态。如果出现超时事件,快速恢复执行如同在慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:将ssthresh设置为cwnd的一半,然后将cwnd设置为1个RSS。 ##### TCP Tahoe vs Reno **快速恢复是TCP推荐的而非必需的构件**(RFC 5681)。有趣的是,一种称为**TCP Tahoe**的TCP早期版本,不管是发生超时指示的丢包事件,还是发生三个冗余ACK指示的丢包事件,都无条件地将其拥塞窗口减至1个MSS,并进入慢启动阶段。TCP的较新版本**TCP Reno**,则综合了快速恢复。 下面展示这两者的区别。在该图中,阈值初始等于8个MSS,在第8次传输时收到3个冗余ACK:  *(图片来源:[GKF168 Blog - TCP Effective Bandwidth (2)](https://gkf168.wordpress.com/2012/05/20/tcp-effective-bandwidth-2/))* #### 状态图 TCP拥塞控制的状态图见下:  *(图片来源:[Internet Technology - Exam 2 study guide](https://www.cs.rutgers.edu/~pxk/352/exam/study-guide-2.html))* ## TCP连接 在TCP中,面向连接的传输需要三个过程:连接建立、数据传输和连接终止。 ### 连接建立 TCP以全双工方式传输数据,当两个机器的TCP建立连接后,它们就能同时发送段。这表示,在传输数据之前,每一方都必须对通信进行初始化,并得到对方的认可。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。 TCP连接的组成包括:一台主机上的缓存、变量和与进程连接的套接字,以及另一台主机上的另一组缓存、变量和与进程连接的套接字。 在TCP中的连接建立称为**三次握手**(three-way handshaking),因为这一过程在两台主机之间交替发送了3个报文段。该过程从服务器开始。服务器程序告诉它的TCP,它已准备好接收一个连接。这称为**被动打开**(passive open)。客户端程序发出请求进行**主动打开**(active open)。想要与服务器进行连接的客户端告诉它的TCP,它需要连接到特定的服务器。示意图如下:  这个阶段的三个步骤如下: 1. 客户端发送**SYN段**。 这个段仅有SYN标识被置位,它用于序号同步。客户端随机选择一个数作为**初始序号**(ISN)。这个段不包含确认号,也没有定义窗口大小。窗口大小的定义只有当段包含确认号时才有意义。SYN段是一个不携带数据但占用序号的控制段。 2. 服务器收到客户端发送的SYN段后,发送**SYN+ACK段**。 这个段有两个目的:首先,是向另一方请求建立连接的SYN段;第二,通过给ACK置位确认接收到来自客户端的SYN段。这个段包含了确认,因此它需要定义接收窗口,即rwnd(供客户端使用)。SYN+ACK段也是一个不携带数据但占用序号的控制段。 3. 客户端收到服务器发送的SYN+ACK段后,发送**ACK段**。 客户端使用ACK标识和确认序号字段来确认收到了第二个段。如果不携带数据,ACK段不占用序号。一些实现是允许该段从客户端携带第一块数据。在这种情况下,段消耗的序号和数据字节数相同。 #### SYN泛洪攻击 在TCP中,连接建立的过程易遭受**SYN泛洪攻击**(SYN flooding attack)。攻击者将大量的SYN段发送到一个服务器,在数据报中通过伪装源IP地址假装这些SYN段是来自不同的客户端。 假定客户端发出主动打开,服务器分配必要的资源,如生成**传输控制块**(Transmission Control Block,TCB)和设置计时器等。然后服务器发送SYN+ACK段给这些虚假客户,但这些段最终都会丢失。当服务器等待第三段握手过程时,服务器上有许多资源被占用但没有被使用。如果在短时间内,SYN段的数量很大,则服务器最终会因为耗尽资源而崩溃。发送SYN+ACK后的等待时间通常为1分多种,具体由服务器设定。 SYN泛洪攻击属于一种称为**拒绝服务攻击**(denial of service attack,DoS)的安全攻击类型。其中,一个攻击者独占系统如此多的服务请求使得系统崩溃,拒绝对每个请求提供服务。 TCP的某些实现拥有减轻SYN攻击影响的策略。有些实现在特定时间周期内对连接请求进行限制,另一些实现过滤掉来自不需要的源地址的数据报。一个新的策略使用cookie推迟资源分配直到一个完整的连接建立。新的传输层协议SCTP使用这种策略。 #### SYN cookie SYN cookie被部署在大多数主流操作系统中,它以下列方式工作: - 当服务器接收到一个SYN报文段时,它并不知道该报文段是来自一个合法的用户,还是SYN泛洪攻击的一部分。因此服务器不会为该报文段生成一个半开连接(占用资源但还未完整建立的连接)。相反,服务器生成一个初始TCP序列号,该序列号是SYN报文段的源和目的IP地址与端口号以及仅有服务器知道的秘密数经一个复杂函数(散列函数)生成的。这个序列号被称为"cookie"。服务器发送具有这种特殊序列号的SYN+ACK分组。**重要的是,服务器并不记忆该cookie或任何对应于SYN的其他状态信息**。 - 如果客户是合法的,则它返回一个ACK报文段。当服务器收到该ACK,需要验证该ACK是与前面发送的某些SYN对应的。如果验证通过,服务器就生成一个具有套接字的全开的连接。(感觉恶意用户发送ACK代价也不会很大) - 在另一方面,如果客户没有返回一个ACK报文段,则初始的SYN并没有对服务器产生危害,因为服务器没有为它分配任何资源。 ### 数据传输 连接建立后,可进行双向数据传输,客户端与服务器双方都可发送数据和确认。在同一段内携带确认时,在同一方向上也可以传递数据。 确认号是主机期望从对方主机收到的下一个字节的序号。假设主机A已经收到了来自主机B的编号为0~535的所有字节,同时假设它打算发送一个报文段给主机B。由于主机A等待主机B的数据流中字节536及之后的所有字节,因此主机A就会在它发往主机B的报文段的确认号字段中填上536。 再举一个例子,假设主机A已经收到来自主机B的包含字节0~535的报文段,以及另一个包含字节900~1000的报文段。由于某种原因,主机A还没有收到字节536~899的报文段。在这个例子中,主机A为保字节流固有的顺序性,仍在等待字节536(和其后的字节)。因此,A到B的下一个报文段将在确认号字段中包含536。因为TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供**累积确认**(cumulative acknowledgment)。 同时注意到上面的例子中,报文段900~1000是失序到达的。这引起了一个问题:当主机在一条TCP连接中收到失序报文段时该怎么办?有趣的是,TCP RFC对此没有明确规定,而是把这个问题留给了实现TCP的程序员去处理。他们有两个选择: - 接收方立即丢弃失序报文段。这种方式可以简化接收方的设计。 - 接收方保留失序的字节,并等待缺少的字节以填补该间隔。这种方式对网络带宽而言更为有效,是实践中采用的方法。 假设接收方的接收缓存是可以收缩的,则可能会发生下图中展示的问题——数据包大于接收缓存:  *(图片来源:[TCP Window Management Issues](http://www.tcpipguide.com/free/t_TCPWindowManagementIssues.htm))* 这种情况下如果要求发送方重新发送的话,未免太过低效。为了防止这种情况发生,TCP向基本的滑动窗口机制添加了一个简单的规则:不允许设备收缩窗口。但是如果我们一定需要收缩窗口呢?我们可以把客户端的发送窗口也收缩到相应的大小,至于这份过大的报文段,服务器可以简单地丢弃它。 ### 连接终止 交换数据双方的任一方都可关闭连接,通常情况下是由客户端发起的。终止连接的大多数实现有两种方法:**三次挥手**与**带半关闭选项的四次挥手**。 #### 三次挥手 当前对连接终止的绝大多数实现是**三次挥手**。与连接建立时的三次握手的区别在于:SYN标志换为FIN标志;客户端发送的第一个段携带ACK标志,并且可以携带最后要发送的数据块。 示意图如下,注意服务器接收到FIN段时,可以发送一个FIN+ACK段。这意味着图中服务器端的两个段在接收到客户端的关闭请求时,是可以合并发出的。  #### 带半关闭选项的四次挥手 在TCP中,一端可以停止发送数据后,还可以持续接收数据。这就是所谓的**半关闭**(half-close)。虽然任一端都可发出半关闭,但通常是由客户端发起。  从客户端到服务器的数据传输停止。客户端通过发送FIN段实现半关闭连接。服务器通过发送ACK段确认。然而,服务器还可以发送数据。当服务器已经发送完被处理的数据时,它发送一个FIN段。该FIN段由客户端的ACK来确认。 注意连接终止时的等待时间是MSL的两倍。如果MSL是60秒,则等待时间为2分钟。 ### 连接重置 在一端的TCP可能拒绝连接请求,可能终止已存在的连接,也可能结束空闲连接。这些都通过RST(Reset)标志完成。 一般来说,服务器端在没有客户端请求的端口或者其他连接信息不符时,系统的TCP协议栈就会给客户端回复一个RST包。可见连接重置功能本来用于应对例如服务器意外重启等异常情况。为什么不直接丢弃数据包算了?因为直接丢弃数据包的话,客户端将不知道网络的具体状况。基于TCP协议的超时/重传机制,客户端就会不停地超时重传数据包。 #### 原因 连接重置的原因有以下几种: - 端口未打开。 服务器程序端口未打开而客户端请求连接。这种情况是最为常见和好理解的一种了。`telnet`一个未打开的 TCP 的端口就可能会出现这种错误。这个和操作系统的实现有关。在某些情况下,操作系统可能完全不理会这些发到未打开端口的连接请求。 - 请求超时。 - 提前关闭。 - 在一个已关闭的 socket 上收到数据。如果某个 socket 已经关闭,但继续接收数据,则会产生 RST。 具体内容可以参阅这篇文章:[几种TCP连接中出现RST的情况](https://my.oschina.net/costaxu/blog/127394)。 #### GFW封锁技术 天朝人民访问国外著名网站时,往往会遇到连接重置问题,这是GFW(Great Firewall of China,中国国家防火墙,防火长城)[^GFW]引起的。实现方式大概就是修改服务器返回的SYN+ACK包,使客户端受骗以为服务器重置了连接而主动放弃向服务器发送请求。现在GFW也进化到了欺骗服务器的程度。[^中国防火长城的封锁技术]示意图如下:  *(图片来源:[道高一尺,墙高一丈:互联网封锁是如何升级的](https://theinitium.com/article/20150904-mainland-greatfirewall/))* 网上有一些关于避开这种欺骗的讨论,有兴趣可以去了解一下。[^KillGFW] [^GFW]:[防火长城](https://zh.wikipedia.org/wiki/防火长城). [^中国防火长城的封锁技术]:[中国防火长城的封锁技术](https://zh.wikipedia.org/wiki/%E8%BF%9E%E6%8E%A5%E9%87%8D%E7%BD%AE#中国防火长城的封锁技术). [^KillGFW]:[TCP连接被重置的探究](http://blog.qiusuo.im/blog/2010/05/27/tcp-reset/). ### 连接保活 双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会,那么在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启等各种意外,当这些意外发生之后,这些 TCP 连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的**保活**(keepalive)报文来实现。[^TCP Keepalive] [^TCP Keepalive]:[TCP 保活(TCP keepalive)](http://www.vants.org/?post=162). #### 作用 - 探测连接的对端是否存活。 在应用交互的过程中,可能存在以下几种情况:客户端或服务器端意外断电、死机、崩溃、重启;中间网络已经中断,而客户端与服务器端并不知道。利用保活探测功能,可以探知这种对端的意外情况,从而保证在意外发生时,可以释放半打开的 TCP 连接。 - 防止中间设备因超时删除连接相关的连接表。 中间设备如防火墙等,会为经过它的数据报文建立相关的连接信息表,并为其设置一个超时时间的定时器,如果超出预定时间,某连接无任何报文交互的话,中间设备会将该连接信息从表中删除,在删除后,再有应用报文过来时,中间设备将丢弃该报文,从而导致应用出现异常。 这种情况在有防火墙的应用环境下非常常见,这会给某些长时间无数据交互但是又要长时间维持连接的应用(如数据库)带来很大的影响,为了解决这个问题,应用本身或 TCP 可以通过保活报文来维持中间设备中该连接的信息,(也可以在中间设备上开启长连接属性或调高连接表的释放时间来解决)。 #### 工作过程 *(下面关于keepalive机制工作过程的相关内容来自知乎,作者郭无心,[链接](https://www.zhihu.com/question/35013918/answer/63664974))* 在此描述中,我们称使用存活选项的那一段为服务器,另一端为客户端。也可以在客户端设置该选项,且没有不允许这样做的理由,但通常设置在服务器。如果连接两端都需要探测对方是否消失,那么就可以在两端同时设置(比如 NFS)。若在一个给定连接上,两小时之内无任何活动,服务器便向客户端发送一个探测段。(我们将在下面的例子中看到探测段的样子。) 客户端主机必须是下列四种状态之一: 1. 客户端主机依旧活跃(up)运行,并且从服务器可到达。从客户端 TCP 的正常响应,服务器知道对方仍然活跃。服务器的 TCP 为接下来的两小时复位存活定时器,如果在这两个小时到期之前,连接上发生应用程序的通信,则定时器重新为往下的两小时复位,并且接着交换数据。 2. 客户端已经崩溃,或者已经关闭(down),或者正在重启过程中。在这两种情况下,它的 TCP 都不会响应。服务器没有收到对其发出探测的响应,并且在 75 秒之后超时。服务器将总共发送 10 个这样的探测,每个探测 75 秒。如果没有收到一个响应,它就认为客户端主机已经关闭并终止连接。 3. 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。 4. 客户端主机活跃运行,但从服务器不可到达。这与状态 2 类似,因为 TCP 无法区别它们两个。它所能表明的仅是未收到对其探测的回复。 #### 保活报文 TCP 保活探测报文是将之前 TCP 报文的序列号减 1,并设置 1 个字节,内容为 “00” 的应用层数据。保活的交互过程如下图所示:  *(图片来源:http://www.vants.org/?post=162)* #### 保活可能带来的问题 - 中间设备因大量保活连接,导致其连接表满。 网关设备由于保活问题,导致其连接表满,无法新建连接或性能下降严重。 - 在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)。 当连接一端在发送保活探测报文时,中间网络正好由于各种异常(如链路中断、中间设备重启等)而无法将该保活探测报文正确转发至对端时,可能会导致探测的一方释放本来正常的连接,但是这种可能情况发生的概率较小,另外,一般也可以增加保活探测报文发生的次数来减小这种情况发生的概率和影响。 - 消费了不必要的宽带。 - 在以数据包计费的互联网上它们(额外)花费金钱。 TCP 协议的 keep alive 机制是不建议开启的,即使开启了默认的时间间隔是 2h。[^Keepalive] [^Keepalive]:[在以 TCP 为连接方式的服务器中,为什么在服务端设计当中需要考虑心跳?](https://www.zhihu.com/question/35013918). ### 内核对TCP连接的管理 内核为任何一个给定的监听套接字维护两个队列: - **未完成队列**(incomplete connection queue),每个这样的 SYN 段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于 SYN_RECEIVED 状态。 - **已完才队列**(completed connection queue),每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态。 当来自客户的 SYN 到达时,TCP 在未完成队列中创建一个新项,然后响应以三次握手的第二个段:服务器的 SYN 响应,其中捎带对客户 SYN 的 ACK。这一项一直保留在未完成队列中,直到三次握手的第三个段(客户对服务器 SYN 的 ACK)到达或者该项超时为止。如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。 当进程调用 `accept` 时,已完成连接队列中的队头项被返回给服务器进程。如果该队列为空,那么服务器进程就会被投入睡眠,直到 TCP 在该队列中放入一项时才唤醒它。 ## TCP状态图 TCP连接的整体状态图如下:  ## 与GBN和SR的比较 TCP是一个GBN协议还是一个SR协议? 我们知道,TCP确认是累积式的,正确接收但失序的报文段是不会被接收方逐个确认的。因此,TCP发送方仅需维持已经发送但未确认的字节的最小序号(SND.UNA)和下一个要发送的字节的序号(SND.NXT)。在这种意义下,TCP看起来更像一个GBN风格的协议。但是TCP和GBN协议之间其实有着许多显著的区别: 差异|TCP|GBN --|-- 对失序分组的操作|许多TCP实现会缓存正确接收但失序的分组|直接丢弃失序分组 超时重传的分组|TCP重传发生时至多重传一个报文段,该段对应于超时的定时器。此外,如果对报文段n+1的确认报文在报文段n超时之前到达,TCP甚至不会重传报文段n|每当一个分组丢失或被破坏,发送方(在超时后)重新发送所有未确认的分组,即使有些失序分组已经被安全完整地接收了。这意味着即使数据已经全部完整地被接收方接收,但确认丢失的情况下还会重传大量分组 对TCP提出的一种修改意见是所谓的**选择确认**(selective acknowledgement)。它允许TCP接收方有选择地确认失序报文段,而不是累积地确认最后一个正确接收的有序报文段。当将该机制与选择重传协议机制结合起来使用时(即跳过重传那些已被接收方确认过的报文段),TCP看起来就很像SR协议。因此,TCP的差错恢复机制也许最好被分类为GBN协议和SR协议的混合体。 ## TCP Socket编程 下面是使用Python 2.7编写的示例程序,客户端向服务器发送一串字符串,服务器将其转化为大写后返回。 ### server.py ```python #!/bin/python2 from socket import * serverPort = 12000 serverSocket = socket(AF_INET, SOCK_STREAM) serverSocket.bind(('', serverPort)) serverSocket.listen(1024) print 'The server is ready to receive.' while True: connectionSocket, _ = serverSocket.accept() sentence = connectionSocket.recv(1024) capitalizedSentence = sentence.upper() connectionSocket.send(capitalizedSentence) connectionSocket.close() ``` ### client.py ```python #!/bin/python2 from socket import * serverName = 'localhost' serverPort = 12000 serverAddress = (serverName, serverPort) while True: message = raw_input('>> ') clientSocket = socket(AF_INET, SOCK_STREAM) clientSocket.connect(serverAddress) clientSocket.send(message) modifiedMessage = clientSocket.recv(1024) clientSocket.close() print modifiedMessage ``` ## References - JamesF.Kurose, KeithW.Ross, 库罗斯,等. 计算机网络:自顶向下方法[M]. 高等教育出版社, 2009. - 谢希仁. 计算机网络.第6版[M]. 电子工业出版社, 2013. 赏 Wechat Pay Alipay Internet Protocol v4(IPv4) Transport Layer