TCP协议
传输层协议概述
在OSI模型中,传输层是介于网络层和会话层之间的一个中间层次,弥补高层服务和网络层服务之间的差距,并向高层用户(应用程序)屏蔽通信子网的细节,使高层用户看到的只是在两个传输实体间的一条端到端的、用户可控的、可靠的数据通路。在TCP/IP模型中,由于3个高层简化为1个应用层,传输层是介于网络层与应用层之间的一个层次。其中两个主要协议TCP与UDP在TCP/IP协议栈中的位置如图所示。
网络层协议提供网络地址、路由、交付功能,而传输层协议提供了端到端数据传输的必要机制。
传输层协议通常要负责以下几项基本功能:
-
创建进程到进程的通信,进程即正在运行的应用程序。进程之间通过传输层进行通信,发送进程向传输层发送数据,接收进程从传输层接收数据。
-
提供控制机制,如流量控制、差错控制。数据链路层定义相邻节点的流量控制,而传输层定义端到端用户之间的流量控制。
-
提供连接机制。在数据传输开始时,通信双方需要建立连接。在传输过程中,双方还需要继续通过协议来通信以验证数据是否被正确接收。数据传输完成后,任一方都可关闭连接。
TCP协议
TCP(Transmission Control Protocol,传输控制协议)是传输层最重要和最常用的协议。它提供一种面向连接的、可靠的数据传输服务,保证了端到端数据传输的可靠性。它同时也是一个比较复杂的协议,提供了传输层几乎所有的功能,支持多种网络应用。RFC 793“TRANSMISSION CONTROL PROTOCOL DARPA INTERNET PROGRAM PROTOCOL SPECIFICATION”是TCP协议的正式规范文件。
TCP协议的特性
1. 面向连接:
它向应用程序提供面向连接的服务,两个需要通过TCP进行数据传输的进程之间首先必须建立一个TCP连接,并且在数据传输完成后要释放连接。一般将请求连接的应用进程称为客户进程,而响应连接请求的应用进程称为服务器进程,即TCP连接的建立采用的是一种客户/服务器模型。
2. 全双工:
它提供全双工数据传输服务,只要建立了TCP连接,就能在两个进程之间进行双向的数据传输服务,但是这种传输只是端到端的传输,不支持广播和多播。
3. 可靠:
TCP 提供流量控制,解决接收方不能及时处理数据的问题;提供拥塞控制,解决因网络通信拥堵延迟带来的数据丢失问题;提供差错控制,解决数据被破坏、重复、失序和丢失的问题,从而保证数据传输的可靠性。
4. 基于字节流:
提供面向字节流的服务,即TCP数据传输是面向字节流的,两个建立了TCP连接的应用进程之间交换的是字
用途
TCP比较安全、稳定,但是效率不高,占用资源较多。TCP协议的作用主要是在计算机之间可靠地传输数据,将具有一定可靠性的流式通信服务提供给应用程序。目前大多数Internet信息交付服务都使用TCP协议,这样便于开发人员专注于服务本身,而不是处理可靠性和数据交付问题。
TCP段格式
位于传输层的TCP数据分组称为段(Segment),又译为报文段、数据段或分段。TCP将来自应用层的数据分块并封装成TCP段进行发送。TCP段封装在IP数据报中,然后再封装成数据链路层中的帧,如图所示。
TCP段是TCP的基本传送单位,段格式如图7-4所示。TCP段由首部和数据两部分构成。首部长度在20~60字节之间,由定长部分和变长部分构成,定长部分长度为20字节,变长部分是选项和填充,长度在0~40字节之间。数据部分用于装载来自应用层的数据,下面重点介绍首部格式。
首部格式
TCP首部的字段和功能说明如下。
1.源端口(Source Port)和目的端口(Destination Port) 这两个端口各占16位,源端口定义发送该报文段的应用程序的端口号;目的端口定义接收该报文段的应用程序的端口号。每一个TCP首部中都包含有源端口号和目的端口号,用于定位源端点的应用进程和目的端点的应用进程。
2.序列号(Sequence Number) 该字段占32位,定义数据段中的数据部分在发送方数据流中的位置,也就是发送的数据部分第1个字节的序列号。这个字段包含了一个唯一标识TCP数据段的数字。
3.确认号(Acknowledgment Number) 该字段占32位,定义报文段的接收方期望从对方接收的序列号,指明下一次希望得到的、来自另一方的序列号。 序列号和确认号是TCP实现可靠连接的关键。当建立一个TCP连接时,发送方主机发出一个随机的初始化序列号(ISN)给接收方,接收方将其加1后送回发送方,这意味着发送方可以发送下一个字节了。一旦数据开始传送,序列号和确认号将跟踪已发送了的那些数据。
4.首部长度(Header length) 该字段占4位,以字节为单位表示TCP首部的大小。首部长度随TCP选项字段而改变,通过这个字段可以同时判断该TCP段的开始位置和结束位置。某些协议分析器将这个字段标记为偏移(Offset)字段。这个字段的值可以在5(5×4=20)至15(1×4=60)之间,即首部长度可以在20~60字节之间。
5.标志(Flags) 该字段占6位,定义6种不同的控制位或标志位,在同一时间可设置一位或多位标志。这些标志位用在TCP的流量控制、连接建立和终止以及数据传送方式等方面。标志位设为1又称置位(set)。这些标志的具体说明如图所示。
6.窗口大小(Window size) 该字段占16位,指明TCP接收方缓冲区的长度,以字节为单位。最大长度是65535字节,0指明发送方应该停止发送,因为接收方的TCP缓冲区已满。这个值通常作为接收窗口(rwnd),并由接收方来确定,接收方可以使用此字段来改变发送方的窗口大小。在这种情况下,发送端必须服从接收端的决定。
7.校验和(Checksum)
该字段占16位,用于存储TCP段的校验和,包括首部和主体部分。校验和用于传输层差错检测,允许目的主机可以验证TCP段的内容并能够测试可能的破坏。UDP是否使用校验和是可选的,而TCP使用校验和则是强制性的。校验和算法将TCP段的内容转换为一系列16位的整数,并将它们相加,接收方根据校验和判断传输是否正确。
8.紧急指针(Urgent Pointer) 该字段占16位,只有当紧急标志置位(值为1)时,这个字段才有效,这时的报文段中包括紧急数据。紧急指针是一个正的偏移量,定义了一个数,把这个数加到序列号上就得出报文段数据部分中最后一个紧急字节。如果URG指针被设置,接收方必须检查这个字段。TCP为了提高效率,数据不会直接发送到对方,一般都先放在数据缓冲区中,等到数据积累到一定的大小才会一起发送。紧急指针所指向的一段数据不必等待缓冲数据的积累,直接发送到对方。
9.选项(Options) 在TCP首部中可以有多达40字节的选项字段。TCP首部最小为20字节。如果定义了一些选项,首部的大小就会增加(最大为60字节)。RFC 793规定首部必须可以被32位整除,所以如果定义了一个选项,但该选项只用了16位,那么必须使用填充(padding)字段补充16位,填充字段是由0组成的字段。
TCP连接
IP是无连接协议,而使用IP服务的TCP则是面向连接的协议,这里的关键是TCP的连接是虚拟的,不是物理的。TCP使用IP的服务把单个的报文段交付给接收方,但是TCP控制这个连接本身,在源节点和目的节点之间建立一条虚路径,属于一个报文的所有段都沿着这条虚路径传送,这使得确认过程以及对损坏或丢失的报文的重传更加容易。无论哪一方发送数据之前,都必须先在双方之间建立一条连接。为了理解TCP工作原理,必须熟悉它建立和终止连接的过程,以及在连接状态传输数据。
3次握手建立连接
由于TCP要提供可靠的通信机制,在传输数据之前,必须先初始化一条客户端(请求端)到服务器(被请求端)的TCP连接,在连接正式建立后,双方经TCP连接通道进行数据传输。
理论上建立传输连接只需一个请求和一个响应。但是,实际网络通信可能导致请求或响应丢失,可采用超时重传解决此问题,即请求或响应的丢失会造成定时器超时溢出,客户端将被迫再次发起连接请求,通过重传连接请求来建立连接。但这又有可能导致重复连接的问题。为避免这些问题,在建立TCP连接时采用三次握手(Three-way Handshaking,又译三向握手)方法。该方法要求对所有报文段进行编号,每次建立连接时都产生一个新的初始序列号。整个连接建立过程如图7-6所示,具体解释如下。
TCP三次握手建立连接
(1)客户端(作为源主机)通过向服务器(作为目的主机)发送TCP连接请求(又称SYN段),其中标志SYN=1, ACK=0;序列号为客户端初始序列号(简称称ISN);目的端口号为所请求的服务对应的端口;还包括最大段长度(MSS)选项。 这个SYN段不携带任何数据,但是它消耗一个序列号。这一步客户端执行主动打开(Active Open)。
(2)服务器在指定的端口等待连接,收到TCP连接请求后,将回应一个TCP连接应答(又称SYN/ACK段),其中标志SYN=1, ACK=1;序列号为服务器初始序列号;确认号为客户端初始序列号加1;目的端口号为客户端的源端口号。 这个SYN/ACK段不携带数据,但消耗一个序列号。这一步服务器执行被动打开(Passive Open)。
(3)客户端再向服务器发送一个TCP连接确认报文(又称ACK段),其中标志SYN=0, ACK=1;序列号为客户端初始序列号加1;确认号为服务器的初始序列号加1。
- 一般来说,这个ACK段不携带数据,因而不消耗序列号。某些实现中,该段可以携带客户端的第1个数据块,此时必须有一个新的序列号来表示数据中的第1个字节的编号。
经过上述3次握手后,TCP连接正式建立。双方都置ACK标志,交换并确认了对方的初始序列号,可以通过连接互相传输数据。
由于客户端对TCP段进行了编号,它知道哪些序列号是期待的,哪些序列号是过时的。当客户发现段的序列号是一个过时的序列号时,就会拒绝该段,这样就不会造成重复连接。
疑问:
为什么是三次握手,两次不行么?
上面是整个三次握手的过程,现在我们分析一下为什么三次握手可以可靠的确定客户端和服务端都能支持的发送和接收数据。
第一次握手:第一次握手是客户端发送同步报文到服务端,这个时候客户端是知道自己具备发送数据的能力的,但是不知道服务端是否有接收和发送数据的能力;
第二次握手:当服务端接收到同步报文后,回复确认同步报文,此时服务端是知道客户端具有发送报文的能力,并且知道自己具有接收和发送数据的能力,但是并不知道客户端是否有接收数据的能力;
第三次握手:当客户端收到服务端的确认报文后,知道服务端具备接收和发送数据的能力,但是此时服务端并不知道自己具有接收的能力,所以还需要发送一个确认报文,告知服务端自己是具有接收能力的。
当整个三次握手结束过后,客户端和服务端都知道自己和对方具备发送和接收数据的能力,随后整个连接建立就完成了,可以进行后续数据的传输了。
看到这里,如果大家理解了就会知道很明显,两次握手是不行的,因为服务端并不知道客户端是具备接收数据的能力,所以就不能成为面向连接的可靠的传输协议。
半开连接
从协议定义的角度来说,TCP的半开连接是指TCP连接的一端异常崩溃,或者在未通知对端的情况下关闭连接,这种情况下不可以正常收发数据,否则会产生RST(后面内容我们在介绍RST)。比如一个常见的情况是TCP连接的一端异常断电,就会导致TCP的半开连接。如果没有数据传输,对端就不会知道本端的异常而一直处于ESTABLISHED状态(TCP有存活检测机制,后面内容我们会进行介绍)。
另外从linux实现的角度来说,因为linux内部有个半连接队列,TCP半开连接是指发送了TCP连接请求,等待对方应答的状态,此时连接并没有完全建立起来,双方还无法进行通信交互的状态,此时就称为半连接。由于一个完整的TCP连接需要经过三次握手才能完成,这里把三次握手之前的连接都称之为半连接。
SYN Flood攻击
可以针对半连接队列发起SYN Flood攻击——给服务器发一个SYN就立即下线,或者填写错误的源端IP地址,在服务器中创建一个半连接。发SYN的速度是很快的,这样攻击者很容易将服务器的SYN队列资源耗尽,使服务器无法处理正常的新连接。
防御方法
针对该问题,Linux提供了一个tcp_syncookies参数解决这个问题。当SYN队列满了后,如果收到客户端的SYN分节的建立连接请求,服务器不再把连接放入SYN队列,服务器会通过源地址、端口、目标地址、端口和时间戳等构造一个特别的Sequence Number+ACK发回去,称为SYN Cookie。如果是攻击者则不会有响应,如果是正常连接,服务器则会收到客户端的ACK响应,然后可以通过确认号中减去 1 以便还原向客户端发送的原始 SYN Cookie。至于SYN队列中的连接,则不做处理直至超时关闭。请注意,不要用tcp_syncookies参数来处理正常的大负载连接情况,因为SYN Cookie本质上也破坏了建连接的SYN超时机制,是妥协版的TCP协议。
对于正常的连接请求,有另外三个参数可供选择:
tcp_synack_retries参数设置SYN超时重试次数
tcp_max_syn_backlog参数设置最大SYN连接数(SYN队列容量)
tcp_abort_on_overflow参数使SYN请求处理不过来的时候拒绝连接
另外一种防护的思路是使用防火墙限制某些IP
TCP数据传输
数据传输过程
1.数据传输过程 客户端和服务器都可以在两个方向进行数据传输和确认。客户端和服务器分别记录对方的序列号,序列号的作用是为了同步数据。客户端向服务器发送数据报文,服务器收到后会回复一个带有ACK标志的确认报文段。客户端收到该确认报文段,就知道数据已经成功发送,否则,报文将会被重发。接着它继续向服务器发送报文。 实际上TCP并不是每发送一个报文段就要等标有ACK标志的确认报文到达才能发送下一段报文的。通常是连续发送几段报文,然后等待服务器的回应;接着再发送几段报文,再等待回应。至于每次应该发送多少段报文,需要由双方协调。
接收方通知发送方它可以接收多少字节的报文,这个数目的大小就是窗口。窗口越大,发送方一次可以传送的字节就越多,反之越少。窗口大小是会动态调整的。 TCP数据传输的过程如图所示。数据可以双向传送,并且在同一个报文段中可以携带确认,即确认是由数据捎带上的。
图中客户端用两个报文段发送若干字节的数据,服务器接着用一个报文段发送若干字节的数据。这3个报文段既有数据又有确认,但最后一个报文段只有确认而没有数据,这是因为没有数据要发送了。如果发送的数据带有PSH(推送)标志,对方在收到这些数据后要尽可能快地交付给相应的进程。
推送数据
通常TCP向应用程序延迟传送和延迟交付数据以提高效率。发送方TCP使用缓冲区来存储由发送应用程序提交的数据流,并且可以选择报文段的长度。接收方TCP在数据到达时也要将其进行缓存,当应用程序就绪或者接收方TCP方便时,才将这些数据交付。
希望对方立即收到就不能采用这种延迟方式,而需要发送方应用程序请求推送(PUSH)操作。发送方TCP设置推送标志(PSH)以告诉接收方TCP该报文段所包括的数据必须尽快地交付给接收应用程序,而不要等待更多的数据的到来。这样每创建一个报文段就立即发送。不过,目前大多数实现中都忽略推送操作请求,TCP可以选择是否使用推送操作。
紧急数据传输
TCP是一种面向流的协议,这就意味着从应用程序到TCP的数据被表示成一串字节流。数据的每一个字节在字节流中占有一个位置。但是,在某些情况下应用程序需要发送紧急数据,希望该数据由接收应用程序不按顺序依次读出,这可通过发送带有URG标志的报文段来实现。具体方法是发送应用程序通知发送方TCP某些数据需要紧急发送。发送方TCP创建报文段,并将紧急数据放在报文段的开头,其余部分可以包括来自缓冲区的正常数据。TCP首部中的紧急指针字段定义了紧急数据的结束和正常数据的开始位置。当接收方TCP收到URG置位的报文段时,它就利用紧急指针的值从报文段中提取出紧急数据,并且优先将它交付给接收应用程序。
TCP长连接
建立TCP连接之后,可以保持连接以避免每次发送数据都需要重复执行握手过程。这样,即使没有数据在TCP链路上传输,仍旧能够维持连接。通常应用层可以实现连接保持,如FTP。如果应用程序不能保持连接,则可以由服务器进程发起TCP保持连接。
那么我们如何才能保持住这个连接呢?实际上,TCP并没有keepalive机制,但是很多的操作系统内核实现TCP协议时,都加上了这个keepalive机制,那么这个功能默认是关闭的,那这个keepalive机制到底是如何的呢?也就是,如果TCP之间没有任何数据来往了在tcp_keepalive_time(7200s,2h)后,服务器给客户端发送一个探测包,如果对方有回应,说明这个连接还存活,否则继续每隔tcp_keepalive_intvl(默认为75s)给对方发送探测包,如果连续tcp_keepalive_probes(默认为9)次后,依然没有收到对端的回复,那么则认为这个连接已经关闭。
TCP连接关闭
参加交换数据的双方中的任何一方(客户端或服务器)都可以关闭连接。
4次握手关闭连接
建立一个连接需要3次握手,而终止一个连接要经过4次握手。这是由TCP的半关闭(half-close)造成的。由于一个TCP连接是全双工的(即数据在两个方向上能同时传递),因此每个方向必须单独进行关闭,当一方完成它的数据发送任务后就能发送一个FIN段(应用层关闭连接就要求TCP发送FIN段)来终止该方向的连接。当一方收到一个FIN段,它必须通知应用层对方已经终止了该方向的数据传送,此时它自己仍然能够向对方发送数据。首先进行关闭的一方执行主动关闭,而另一方执行被动关闭。
TCP四次握手关闭连接:
第一次挥手客户端发起关闭连接的请求给服务端;
第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文,表示自己知道客户端想要关闭连接了,但是因为数据还没传输完,所以还需要等待;
第三次挥手:当数据传输完了,服务端会主动发送一个 FIN 报文,告诉客户端,表示数据已经发送完了,服务端这边准备关闭连接了。
第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个 ACK 报文,告诉服务端自己知道了,再等待一会就关闭连接。
疑问:
为什么握手要三次,挥手却要四次呢?
因为握手的时候并没有数据传输,所以服务端的 SYN 和 ACK 报文可以一起发送,但是挥手的时候有数据在传输,所以 ACK 和 FIN 报文不能同时发送,需要分两步,所以会比握手多一步。
半关闭
TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。这就是所谓的半关闭。正如我们早些时候提到的只有很少的应用程序使用它。
为了使用这个特性,编程接口必须为应用程序提供一种方式来说明“我已经完成了数据传送,因此发送一个文件结束(FIN)给另一端,但我还想接收另一端发来的数据,直到它给我发来文件结束(FIN)”。
TCP连接复位
除了使用FIN标志正常关闭TCP连接外,还可以使用RST标志非正常关闭连接。TCP首部中的RST标志是用于复位的,复位主要用于快速结束连接。一般来说,一个报文段发往指定的连接无论何时出现错误,TCP都会发出一个复位报文段。TCP连接复位主要有以下几种情形。
1.拒绝连接请求 假定一方TCP向另一方并不存在的端口(或者目的端口没有打开,没有进程正在监听)发出连接请求,另一方TCP就发送RST置位的报文段来取消这个请求。而对于UDP来说,当一个数据报到达没有打开的目的端口时,它将产生一个ICMP端口不可达的信息。
2.异常关闭连接 由于出现了异常情况,某一方的TCP可能希望异常关闭连接。关闭连接的正常方式是发送FIN段,正常情况下没有任何数据丢失,因为在所有排队数据都已发送之后才发送FIN段。异常关闭是发送一个RST段来中途释放一个连接,这有两个优点,一是丢弃任何待发数据并立即复位,二是收到RST段的一方能够区分另一方执行的是异常关闭还是正常关闭。当然应用程序必须提供产生异常关闭的手段。
3.终止空闲的连接 一方TCP可能发现在另一方TCP已经空闲了很长时间,就可以发送RST段来撤销这个连接。这个过程与异常关闭一条连接一样。
TCP连接复位都是用RST标志来实现的,可以抓取相应的数据包来验证。这里以浏览器浏览网页后关闭为例,浏览器端将向服务器发送RST段,如图7-20所示。
TCP状态码
TCP连接建立、数据传输和连接关闭状态转换:
TCP可靠性
TCP除了提供进程通信能力外,还具有高可靠性。TCP采用的可靠性技术主要包括差错控制、流量控制和拥塞控制。
TCP差错控制
TCP的差错控制包括检测损坏的报文段、丢失的报文段、失序的报文段和重复的报文段,并进行纠正。应用程序将数据流交付给TCP后,就依靠TCP将整个数据流按序且没有损坏、没有部分丢失、没有重复地交付给另一端的应用程序。TCP中的差错检测和差错纠正的方法有校验和、确认和重传。
1.校验和
数据损坏可以通过TCP的校验和检测出来。每一个报文段都包括校验和字段,用来检查受损的报文段。若报文段遭到破坏,就由接收方TCP将其丢弃,并且被认为丢失了。
2.确认
TCP采用确认来证实收到了报文段。控制报文段不携带数据,但消耗一个序列号。控制报文段也需要被确认。只有ACK报文段永远不需要被确认。目前ACK的确认机制最常用的规则有以下几种。
(1)ACK报文段不需要确认,也不消耗序列号。
(2)发送数据时尽量包含(捎带)确认,给出对方所期望接收的下一个序列号,以减少通信量。
(3)如果接收方没有数据要发送,并且收到了按序到达的报文段,同时前一个报文段也已经确认了,那么接收端就推迟发送确认报文段,直到另一个报文段到达,或再经过了一段时间(通常是500 ms)。
(4)当具有所期望的序列号的报文段到达时,同时前一个按序到达的报文段还没有被确认,那么接收端就要立即发送ACK报文段。任何时候不能有两个以上的按序的未被确认的报文段,以避免不必要的重传而导致网络的拥塞。
(5)当收到一个序列号比期望序列号还大的报文段时,立即发送ACK报文段,让对方快速重传任何丢失的报文段。
(6)当收到丢失的报文段,立即发送ACK报文段,告知对方已经收到了丢失的报文端。
(7)当收到重复的报文段,立即发送ACK报文段进行确认。这就解决了ACK报文段丢失所带来的问题。
3.重传
差错控制机制的核心就是报文段的重传。当一个报文段损坏、丢失或者被延迟了,就要重传。目前的TCP实现中,有以下两种报文段重传机制。
(1)超时重传。
TCP为每一个发送的报文段都设置一个超时重传(Retransmission Time Out, RTO)计时器。计时器时间一到,就认为相应的报文段损坏或者丢失,需要重传。注意仅携带ACK的报文段不设置超时计时器,因而也就不重传这种报文段。
在TCP中RTO的值根据报文段的往返时间(Round Trip Time, RTT)动态更新。RTT是一个报文段到达终点和收到对该报文段的确认所需的时间。由于在Internet中传输延迟变化范围很大,因此从发出数据到收到确认所需的往返时间是动态变化的,很难确定。TCP的重传定时值也要不断调整,并通过测试连接的往返时间对重传定时值进行修正。
多数TCP实现至少使用4种计时器:
- 重传计时器:用于重传丢失的报文段。
- MSL计时器:用于连接终止。
- 保活计时器(KeepAlive Timer):用于防止连接出现长时间的空闲。
- 持久计时器(Persistence Timer):用于对付窗口大小为0而引起的死锁
如果RTO值不是太大,上述超时重传比较可行。但是,有时一个报文段丢失了,而接收方会收到很多的失序报文段以致无法保存它们(缓冲区空间有限)。要解决这个问题,现在一般采用三个重复的ACK报文段之后重传的规则。也就是说,发送方收到了3个重复的ACK报文段之后,立即重传这个丢失的报文段。 需要注意的是,不消耗序列号的报文段不进行重传,特别是对ACK段不进行重传。
4.失序报文段的处置
当一个报文段推迟到达、丢失或被丢弃,在这个报文段后面的几个报文段就是失序到达。最初的TCP设计是丢弃所有的失序报文段,这就导致重传丢失的报文段和后续的一些报文段。现在大多数的实现是不丢弃这些失序的报文段,而是把这些报文段暂时存储下来,并把它们标志为失序报文段,直到丢失的报文段到达。注意失序的报文段并不交付到进程。TCP保证数据必须按序交付到进程。
5.重复报文段的处置
重复的报文段一般是由超时重传造成的,接收方可以根据序列号判断是否是重复报文段,对于重复报文段只需要简单丢弃即可。TCP的确认和重传技术对每一个报文段都有唯一的序列号,这样当对方收到了重复的报文段后很容易区分,报文段丢失后也容易定位重传的报文段的序列号。
6.选择确认(SACK)
TCP最初只使用累积确认(ACK),接收方忽略所有收到的失序报文段(包括被丢弃的、丢失的或重复的报文段),只报告收到了最后一个连续的字节,并通告它期望接收的下一个字节的序列号。在TCP首部中的32位ACK字段用于累积确认,而它的值仅在ACK标志为1时才有效。
现在一些新的TCP实现中支持选择确认(SACK),报告失序和重复的报文段,将其作为TCP首部选项字段的一部分。SACK并不是代替ACK而是向发送方报告附加的信息。
SACK在TCP首部的后面作为选项来实现,具体来说有以下两个选项。
-
允许SACK选项:只用在连接建立阶段,发送SYN段的主机增加这个选项以说明它能够支持SACK选项。如果另一方在它的SYN/ACK段中也包含了这个选项,那么双方在数据传输阶段就都可以使用SACK选项。这个选项是不能在数据传输阶段使用的。该选项格式为:种类为4,长度为2字节。
-
SACK 选项:只用在数据传输阶段,前提是双方已在连接建立阶段交换了允许SACK 选项,都已同意使用SACK选项。它的种类为5,具有可变长度。该选项包括失序到达的块(block)的列表。每一个块用两个32位数定义块的开始和结束。由于TCP所允许的选项大小仅有40字节,一个SACK选项能够定义的不能超过4个块(占4×2×4+2=34字节)。如果SACK选项和其他选项一起使用,那么块的数目还要减少。
TCP流量控制
TCP在传输层上实现端到端的流量控制,为接收方对发送方发送数据进行控制,以避免大量的数据导致接收方瘫痪,这是通过滑动窗口机制来实现的。
1.滑动窗口机制
面向连接的传输过程中,收发双方在发送和接收报文时要协调一致。如果发送方不考虑对方是否确认,一味地发送数据,则有可能造成网络拥塞或因接收方来不及处理而丢失数据。如果发送方每发出一个报文(极端的情况下甚至只发送一个字节的数据)都要等待对方的确认,则又造成效率低下,网络资源得不到充分利用。为此,TCP采用一种折中的方法,即在缓冲区(暂时存放从应用程序传出并准备发送的数据)上使用滑动窗口,TCP发送数据的多少由这个滑动窗口定义。这样既能够保证可靠性,又可以充分利用网络的传输能力。
滑动窗口机制通过发送方窗口和接收方窗口的配合来完成传输控制。发送方窗口如图所示。
发送缓冲区中是一组按顺序编号的字节数据,这些数据的一部分在发送窗口之中,另一部分在发送窗口之外。发送缓冲区左端和右端空白处表示可以加入数据的空闲空间,整个缓冲区就是一个左端和右端相连的环。发送窗口左侧是已发送并被接收方确认的数据,相应的缓冲区部分被释放。发送窗口中靠左的部分是已发送但尚未得到确认的数据,靠右的部分是可以立即发送的数据,也就是当前可用的窗口。发送窗口右侧是暂时不能发送的数据,一旦发送窗口内的部分数据得到确认,窗口便向右滑动,将已确认的数据移到窗口左侧空闲空间。发送窗口右边界的移动使新的数据又进入到窗口中,成为可以立即发送的数据。
接收方窗口反映当前能够接收的数据的数量,大小取决于接收方处理数据的速度和发送方发送数据的速度,当从缓冲区中取出数据的速度低于数据进入缓冲区的速度时,接收窗口逐渐缩小,反之则逐渐扩大。接收方将当前窗口大小通告给发送方(利用TCP报文段首部的窗口大小字段),发送方根据接收窗口调整其发送窗口,使发送窗口始终小于或等于接收窗口的大小。只有在接收窗口滑动时(与此同时也发送了确认),发送窗口才有可能滑动。收发双方的窗口按照以上规律不断地向前滑动,因此这种协议又称为滑动窗口协议。
当发送窗口和接收窗口的大小都等于1时,每发送一个字节的数据都要等待对方的确认,这就是停止等待协议。当发送窗口大于1,接收窗口等于1时,就是回退N步协议。当发送窗口和接收窗口的大小均大于1时,就是选择重发协议。协议中规定窗口内未经确认的分组需要重发。这种分组的数量最多可以等于发送窗口的大小,即滑动窗口的大小n减去1(因为发送窗口不可能大于(n-1),起码接收窗口要大于等于1)。 TCP的窗口以字节为单位进行调整,以适应接收方的处理能力。
处理过程如下:
(1)TCP连接阶段,双方协商窗口大小,同时接收方预留数据缓存区;
(2)发送方根据协商的结果,发送符合窗口大小的数据字节流,并等待对方的确认;
(3)发送方根据确认信息,改变窗口大小,增加或者减少发送未得到确认的字节流中的字节数。如果出现发送拥塞,发送窗口缩小为原来的一半,同时将超时重传的时间间隔扩大一倍。
在滑动窗口的操作中可能出现一个严重的问题,这就是发送应用程序产生数据很慢,或者接收应用程序消耗数据很慢,或者两者都有。不管是上述情况中的哪一种,都使得发送数据的报文很小,这就引起操作效率的降低,这个问题叫做糊涂窗口综合症(Silly Window Syndrome)。
2.发送方产生的糊涂窗口综合症 如果发送方TCP为生产数据很慢的应用程序服务,就可能产生糊涂窗口综合症。解决的方法是防止发送方TCP逐个字节地发送数据,强迫发送方TCP等待,凑成大块数据再发送。为了使TCP等待的时间更为合理,采用了Nagle算法,具体解决方法如下。
(1)发送方将其从发送应用程序收到的第一块数据(即使只有1字节)发送出去。
(2)发送第1个报文段以后,发送方TCP就在输出缓冲区中积累数据并等待,直到或者接收方TCP发送出确认,或者已积累到足够的数据可以装成最大长度的报文段。这时,发送方TCP就可以发送这个报文段。
(3)对剩下的传输,重复步骤2。如果收到了对报文段2的确认,或者已积累到足够的数据可以装成最大长度的报文段,报文段3就必须发送出去。 采用Nagle算法,如果应用程序比网络更快,则报文段就较大(最大长度报文段)。若应用程序比网络慢,则报文段就较小(小于最大长度报文段)。
3.接收方产生的糊涂窗口综合症
如果接收方TCP为消耗数据很慢的应用程序服务,就可能产生糊涂窗口综合症。当接收缓冲区已满,通知窗口大小为0,发送方必须停止发送数据。接收应用程序从缓冲区中取走1字节的数据,通知窗口值为1字节,发送方可能会传送拥有1字节的段。这样的过程一直继续下去,1字节的数据被消耗掉,然后发送1字节数据的报文段。这又是一个效率问题和糊涂窗口综合症。针对这种情形,有两种解决方法。
(1)延迟通告(Delayed Advertisement) 延迟通告又称Clark法,只要有数据到达就发送确认,但在缓冲区已有足够大的空间放入最大长度报文段(Maximum Sized Segment, MSS)之前,或者缓冲区空闲空间达到一半之前,一直都宣布窗口值为0。
(2)推迟确认(Delayed Acknowledgement) 当报文段到达时并不立即发送确认。接收方在对收到的报文段进行确认之前一直等待,直到输入缓冲区有足够的空间为止。推迟发送确认防止了发送方TCP滑动它的窗口。当发送端TCP发送完数据后就停下来了。推迟确认还有另一个优点,就是减少了通信量,不需要对每一个报文段进行确认。不过推迟确认有可能迫使发送方重传它未被确认的报文段,为此推迟确认设置不能超过500ms。
提示:
滑动窗口机制为端到端设备间的数据传输提供了可靠的流量控制机制。然而,它只能在源端设备和目的端设备起作用,当网络中间设备(如路由器等)发生拥塞时,滑动窗口机制将不起作用。
TCP拥塞控制
流量控制是由于接收方不能及时处理数据而引发的控制机制,拥塞(Congestion,又译为阻塞)是由于网络中的路由器超载而引起的严重延迟现象。拥塞的发生会造成数据的丢失,数据的丢失会引起超时重传,而超时重传的数据又会进一步加剧拥塞,如果不加以控制,最终将会导致系统崩溃。对于拥塞造成的数据丢失,仅仅靠超时重传是无法解决的。为此TCP提供了拥塞控制机制。发送方所能发送的数据量不仅要受接收方的控制(流量控制),而且还要由网络的拥塞程度来决定。
为了避免和消除拥塞,RFC 2581为TCP定义了4种拥塞控制机制,分别是慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快重传(Fast Retransmit)和快恢复(Fast Recovery)。
1.拥塞窗口 网络中的一个重要问题就是拥塞。如果网络上的负载(即发送到网络的数据包数)大于网络的容量(即网络能够处理的数据包数),在网络中就可能发生拥塞。在TCP的拥塞控制中,仍然是利用发送窗口来控制数据流速度,减缓注入网络的数据流后,拥塞就会自然解除。发送窗口的大小取决于两个因素,一个因素是接收方的处理能力,由确认报文段所通告的窗口大小(即可用的接收缓冲区的大小)——接收窗口(rwnd)来表示;另一个是网络的处理能力,由发送方所设置的变量——拥塞窗口(cwnd)来表示。发送窗口的大小最终取接收窗口和拥塞窗口中较小的一个。与接收窗口一样,拥塞窗口也处于不断的调整中,一旦发现拥塞,TCP将减小拥塞窗口,进而控制发送窗口。
2.拥塞策略 TCP处理拥塞基于3个阶段:慢启动、拥塞避免和拥塞检测。在慢启动阶段,发送方从非常慢的发送速率开始,但很快就把速率增大到一个阀值;当达到阀值时,数据发送速率的增大就放慢以避免拥塞;最后,如果检测到拥塞,发送方就又回到慢启动或拥塞避免阶段。为了避免和消除拥塞,TCP循环往复地采用3种策略来控制拥塞窗口的大小。
(1)慢启动(Slow Start) 首先使用慢启动策略在建立连接时将拥塞窗口设置为一个最大段(MSS)大小。对于每一个报文段的确认都会使拥塞窗口增加一个MSS,此算法开始很慢,但按指数规律增长。例如,开始时只能发送一个报文段,当收到该段的确认后拥塞窗口加大到两个MSS,发送方接着发送两个报文段,收到这两个报文段的确认后,拥塞窗口加大到4个MSS,接下来发送4个报文段,依此类推。 慢启动不能无限制地继续下去,当以字节计的窗口大小到达慢启动阀值(一般为65535字节)时就停止了,转而进入下一个阶段。
(2)拥塞避免(Congestion Avoidance) TCP拥塞避免使拥塞窗口按照加法规律增长,而不是按照指数规律。当拥塞窗口的大小达到慢启动阀值时,就进入此阶段就了。在这种策略中,每当窗口中的所有报文段都被确认,拥塞窗口的大小增加1个MSS,即使确认是针对多个报文段的,拥塞窗口也只加大1个MSS,这在一定程度上减缓了拥塞窗口的增长。但在此阶段,拥塞窗口仍在增长,直到拥塞被检测到。
(3)拥塞检测(Congestion Detection) 如果拥塞发生了,拥塞窗口的大小就必须减小。发送方判断拥塞已经发生的唯一方法是它必须重传一个报文段。但是,重传发生在两种情形之一——RTO计时器超时或者收到了3个重复的ACK,阀值就下降到原来的一半(乘法减小)。大多数TCP实现有下面这两种反应。
-
如果发生超时,那么出现拥塞的可能性就很大,某个报文段可能在网络中的某处丢失了,而后续的一些报文段也没有消息。遇到这种情况,TCP的反应比较强烈,将阀值设置为当前窗口值的一半,将拥塞窗口设置为一个报文段,再开始一个新的慢启动阶段。
-
如果收到了3个ACK段,那么出现拥塞的可能性就较小,一个报文段可能已经丢失了,但以后的几个报文段又安全地到达了,因为收到了3个ACK。这就是所谓的快重传和快恢复。遇到这种情况,TCP的反应不够强烈,将阀值设置为当前窗口值的一半,将拥塞窗口设置为阀值(某些实现在阀值上增加3个报文段),再开始一个新的拥塞避免阶段。
总结
TCP协议是传输层里非常重要的协议,它通过三次握手建立连接通道,通信的双方可以依靠这条通道来传输数据,在传输过程中会有校验和、确认和重传机制来纠正差错,又有滑动窗口来进行流量控制保证双方协调一致,通过四次挥手断开连接,保证在关闭的时候不会终端数据传输,是非常可靠的协议。
参考文献
《TCP-IP协议及其应用》 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(上)