协议对于通信就像算法对于计算一样。算法允许人们在不必知道特定的CPU指令集的情况下指定或理解具体的计算形式。同样地,通信协议允许人们不依赖特定厂家的网络硬件来指定或理解数据通信。
网络协议通常分不同层次进行开发,每一层分别分则负责不同的通信功能。
一个协议族
,比如TCP/IP
,是一组不同层次上的多个协议的组合。
TCP/IP
通常被认为是一个四层协议族。每一层负责不同的功能。TCP协议族又称为"Internet协议族(Internet Protocol Suite)"
链路层
有时又称为"数据链路测过"或"网络接口层",通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆(或其他任何传输媒介)的物理接口细节。网络层
有时又称为"网络互联层",处理分组在网络中的活动,例如分组的选路。
在TCP/IP协议族中,网络层协议包括:IP协议(网际协议)、ICMP协议(Internet互联网控制报文协议)、IGMP协议(Internet组管理协议)。
ICMP是IP协议的附属协议。IP层用它来与其他主机或路由器交换错误报文和其他重要信息。
IGMP用于将一个UDP数据报广播到多个主机。
传输层
传输层主要为两台主机上的应用程序提供端到端的通信。在TCP协议族中,有两个互不相同的传输协议:TCP协议(传输控制协议)和UDP协议(用户数据报协议)。
TCP协议为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于TCP提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。
UDP协议为应用层提供一种非常简单的服务。它只是把称作"数据报"的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。任何必须的可靠性必须由应用层来提供。
一个数据报是指从发送方传输到接收方的一个信息单元(例如,发送方指定的一定字节数的信息)。
应用层
应用层负责处理特定的应用程序细节。
几乎各种不同的TCP/IP实现都会提供下面这些通用的应用程序:Telnet远程登录
FTP文件传输协议
SMTP简单邮件传输协议
SNMP简单网络管理协议
应用程序通常是一个用户进程,而下三层则一般在(操作系统)内核中执行。尽管这不是必需的,但通常都是这样处理的,例如UNIX操作系统。
封装
当应用程序用TCP传送数据时,数据被传送到协议栈中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息)。
TCP传送给IP的数据单元称作"TCP报文段"或简称为"TCP段(TCP Segment)"。
UDP传送给IP的数据单元称作""UDP数据报(UDP Datagram)。
IP传送给网络接口层的数据单元称作"IP数据报(IP datagram)"。
通过以太网传输的比特流称作"帧(Frame)"。
以太网数据帧的物理特性是其长度必须在46~1500字节之间。
由于TCP、UDP、ICMP和IGMP都要向IP传送数据,因此IP必须在生成的IP首部中加入某种标识,以表明数据属于哪一层。为此,IP在首部中存入一个长度为8bit的数值,称作"协议域"。1表示ICMP协议,2表示IGMP协议,6表示TCP协议,17表示UDP协议。
许多应用程序都可以使用TCP或UDP来传送数据。传输层协议在生成报文首部时要存入一个应用程序的标识符。TCP和UDP都适用一个16bit的端口号来表示不同的应用程序。TCP和UDP把原端口号和目的端口号分别存入报文首部中。
网络接口分别要发送和接收IP、ARP和RARP数据,因此也必须在以太网的帧首部中加入某种形式的标识,以指明生成数据的网络层协议。为此,在以太网的帧首部也有一个16bit的帧类型域。
以太网最小帧长
以太网要求的最小帧长为60字节。
以太网最大帧长
以太网和802.3对数据帧的长度都有一个限制,其最大值分别是1500和1492字节。
链路层
MTU(Maximum Transmission Unit,最大传输单元)与分割
"最大传输单元(MTU)"是指某一链路上面(即物理网络)所能通过的最大数据包大小(以字节为单位)。
因为协议数据单元的包头和包尾的长度是固定的,MTU越大,则一个协议数据单元的承载的有效数据就越长,通信效率也越高。MTU越大,传送相同的用户数据所需的数据包个数也越低.
MTU也不是越大越好,因为MTU越大, 传送一个数据包的(传输)延迟也越大;并且MTU越大,数据包中 bit位发生错误的概率也越大。
MTU越大,通信效率越高而传输延迟增大,所以要权衡通信效率和传输延迟选择合适的MTU
网络 | MTU |
---|---|
超通道 | 65535 |
16Mb/s 令牌环(IBM) | 17914 |
4Mb/s 令牌环(IEEE 802.5) | 4464 |
FDDI | 4352 |
以太网 | 1500 |
IEEE 802.3/802.2 | 1492 |
X.25 | 576 |
点对点(低时延) | 296 |
(超通道网络的MTU为65535字节的原因是由于IPv4数据报的最大长度为65535,而IP数据报又是互联网承载数据的基础)
路径MTU
当在同一个网络上的两台主机互相进行通信时,该网络的MTU是非常重要的。但是如果两台主机之间的通信要通过多个网络,那么每个网络的链路层就可能有不同的MTU。重要的不是两台主机所在网络的MTU的值,重要的是两台通信主机路径中的最小MTU,它被称作"路径MTU".
两台主机之间的路径MTU不一定是个常数,它取决于当时所选择的路由。而选路不一定是对称的(从A到B的路由可能与从B到A的路由不同),因此路径MTU在两个方向上不一定是一致的。
在因特网协议中,一条因特网传输路径的“路径最大传输单元”被定义为从源地址到目的地址所经过“路径”上的所有IP跳的最大传输单元(MTU)的最小值。或者从另外一个角度来看,就是无需进一步分片就能穿过这条“路径”的传输单元的最大值.
网络层
在网络层,互联网提供所有应用程序都要使用的两种类型的服务,尽管目前理解这些服务的细节并不重要,但在所有TCP/IP概述中,都不能忽略他们:
无连接分组交付服务(Connectionless Packet Delivery Service)
无连接交付抽象地表示大多数分组交换网络都能提供的一种服务。简单地讲,指的是TCP/IP灰暗网按照报文上携带的地址信息把短报文从一台机器传递到另一台机制。因为无连接服务单独传递每个分组,所以不能保证可靠、有序地传递。而且,由于无连接服务通常直接映射到底层的硬件上,所以非常有效。更重要的是,有了无连接分组交换作为互联网服务的基础,使得TCP/IP协议协议可以适应多种网络硬件。可靠的数据流传输服务(Reliable Stream Transport Service)
大多数应用程序要求得到比分组交换更多的服务,因为它们要求通信软件能够从传输错误、分组丢失或发送者与接受着之间路径上的交换机的故障中自动恢复过来。可靠的传输服务处理了这些问题。它允许一台计算机上的应用程序与另一台计算机上的应用程序之间建立一个"连接",然后通过该链接发送大量数据,就像这是一个永久的直接硬件连接。当然,在底层,通信协议把数据流分成一个个小报文来发送它们,一次一个,同时等待接受这发送确认接收的信息。
无连接交付系统
最基本的互联网服务由一个分组交付系统组成。从技术角度来讲,该服务被定义为不可靠的、尽最大努力交付的、无连接分组交付系统,类似于由运行在尽最大努力交付模式的网络硬件所提供的服务。这种服务是不可靠的(unreliable),因为不能保证交付。分组可能丢失、重复、延迟或不按序交付等,但服务不检测这种情况,也不提醒发送方和接收方。服务是无连接的(connectionless),因为每一个分组都被独立对待。从一台计算机发送到另一台上的分组序列,可能经过不同的传输路径,或者有的丢失有的到达。最后,服务会尽最大努力交付(best-effort delivery),因为互联网软件尽力发送每个分组。也就说,互联网并不随意地放弃分组,只有当资源用完或底层网络出现故障时才可能出现不可靠性。
到此,我们给面向连接作一个定义:
将每一分组按照序号进行排列、传输和处理。
我们给可靠传输的定义:
能够从传输错误、分组丢失或发送者与接受着之间路径上的交换机的故障中自动恢复过来。
。
IP分片
物理网络层一般要限制每次发送数据帧的最大长度,即MTU。
分片是分组交换的思想体现,也是IP协议解决的三个主要问题(寻址、、分片和重组、路由选择)之一。在IP协议中的分片算法主要解决异构网络MTU的不同。
任何时候IP层接收到一份要发送的IP数据报时,它要判断向本地哪个接口发送数据(即选路),并查询该接口获得其MTU。
IP把MTU与数据报长度进行比较,如果数据报长度大于MTU,则需要进行分片。由于每一段链路的MTU都可能不同,因此分片可以发生在原始发送端主机上,也可以发生在中间路由器上。此外,已经分片过的数据报可能会再次进行分片以满足该段链路的MTU。
IP首部中包含的数据为分片和重新组装提供了足够的信息。
把一份IP数据报分片以后,只有到达目的地才会进行重新组装(这里的重新组装与其它网络协议不同,其它网络协议要求在下一跳就进行重新组装,而不是在最终目的地)。因为无法确保所有的分片都经过同一个路由器啊。
重新组装由目的端的IP协议来完成,其目的是使IP分片和重新组装过程对传输层(TCP和UDP)是透明的。
IP首部中的如下字段用于分片过程:
标识字段(Identification):标识主机发送的每一份IP数据报
对于发送端发送的每份IP数据报来说,其标识字段都包含一个唯一值,用于标识发送端发送的每一份IP数据报。
该值在对IP数据报进行分片时会被复制到每个IP分片中。
通常主机每发送一份IP数据报,它的值就会加1.标志字段(IP Flags)
标志字段用其中的第三个比特位来表示"更多的分片",除了最后一个分片之外,其它每个组成IP数据报的分片都要把该字段置1。
注意:标志字段中的第二个比特位称作"不分片"位。如果将这一比特置1,IP将不对数据报进行分片。如果当数据报的长度大于MTU时,则会将数据报丢弃,并发送一个ICMP差错报文。
Tip:标志字段中的第一个比特位保留,暂时没有任何意义。
偏移字段(Fragment Offset)
偏移字段指的是该IP分片在其所属的IP数据报中的偏移位置(距离开始处)。
此外,当数据报分分片之后,每个片的总长度要改为该IP分片的长度值。
当IP数据报被分片后,每一个分片都成为一个分组,具有自己的IP首部,并在选择路由时与其它分组独立。这样,当数据报的这些分片到达目的端时可能会失序。但是在IP首部中的上述字段(标志字段和偏移字段)已经能够让接收端正确组装这些IP数据报分片。
分片的重组
将IP分片放在接收方进行重组有两个缺点:
由于在通过一个小MTU网络后没有对分片立即重组,所以数据报从分片的位置起一只传输到最终目的的站,在目的站重组会导致效率较低,即时遇到某些具有大MTU的网络,也只能传输小的分片
如果任何一个数据报分片丢失了,就无法重组数据分片。接收机器从它收到初始分片锯开始启动一个重组计时器(reassembly timer).如果在所有分片到达之前计时器超时,则接收机丢弃已收到的分片,不处理数据吧片。因为丢失一个数据报分片意味着丢失整个数据报,所以,当发生分片时,就增加了数据报丢失分片的概率。
尽管有这些小的缺陷,但是在最终目的站重组数据报分片的办法是比较好的。它允许每个分片独立地选择路由,而且不需要中间路由器存储或重组分片。
分片与不可靠
尽管IP分片过程看起来是透明的,但有一点让人不想使用它:即使只丢失一个数据报分片也要重新发送整个数据报。这使得一个IP数据报分片的丢失也会造成整个IP数据报传输的不可靠。
为什么会发生这种情况呢?因为IP层本身没有超时重传机制——由更高层的协议来负责超时和重传(TCP有超时重传机制,但UDP没有。一些UDP应用程序本身也要执行超时和重传)。当来自TCP报文段的某一分片丢失后,TCP在超时后会重新发送整个TCP报文段,该报文段对应于一份IP数据报。但没有办法只重传数据报中的一个数据报分片。因此,TCP会试图避免IP分片。
事实上,如果对数据报分片的是中间路由器,而不是起始端系统,那么起始端系统就无法知道数据报是如何被分片的。就这个原因,经常要避免分片。
术语解释:IP数据报是指IP层端到端的传输单元(在分片之前和重新组装之后);分片是指在IP层和链路层之间传输的数据单元。一个分组可以是一个完整的IP数据报,也可以是IP数据报的一个分片。
分片风暴***
分片***:在IP的分片包中,所有的分片包用一个分片偏移字段标志分片包的顺序,但是,只有第一个分片包含有TCP端口号的信息。当IP分片包通过分组过滤防火墙时,防火墙只根据第一个分片包的Tcp信息判断是否允许通过,而其他后续的分片不作防火墙检测,直接让它们通过。这样,***者就可以通过先发送第一个合法的IP分片,骗过防火墙的检测,接着封装了恶意数据的后续分片包就可以直接穿透防火墙,直接到达内部网络主机,从而威胁网络和主机的安全
分片风暴***:故意发送部分IP数据报分片而不是全部,导致目标主机总是等待分片,消耗并占用系统资源。
分片带来的问题——避免分片
IP报文里是有五元组的(源地址、目的地址、源端口、目的端口、协议号),但报文要进行分片时,只有第一个报文携带IP的五元组信息,后续的分片不会保留TCP/UDP报文所标识的信息,例如端口号信息等,这种情况下,如果设备又进行了NAT操作,这造成报文不能正确重组。
最大UDP数据报长度
理论上,IP数据报的最大长度是65535字节,这是由IP首部的16位比特总长度字段所限制的。除去20个字节的IP首部和8个字节的UDP首部,UDP数据报中用户数据的最大长度为65507字节。
但是,大多数实现所提供的长度比这个最大值小。
我们将遇到两个限制因素。第一,应用程序可能会受到其程序接口的限制。Socket API提供了一个可供应用程序调用的函数,以设置接收和发送缓冲的长度。对于UDP Socket,这个长度与应用程序可以读写的最大UDP数据报的长度直接相关。现在的大部分系统都默认提供可读写大于8192字节的UDP数据报(使用这个默认值是因为8192是NFS读写用户数据报的默认值)。
第二个限制来自TCP/IP的内核实现。可能存在一些实现特性(或差错),使IP数据报长度小于65535字节。
TCP的MSS
前面我闷说过,TCP会试图避免IP数据报分片。TCP通过MSS机制来试图避免IP数据报分片。
MSS就是TCP数据包每次能够传输的最大数据分段。
为了达到最佳的传输效能,TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的往往用MTU值代替(需要减去IP首部(20个字节)和TCP数据段首部(20个字节)),所以对于以太网(MTU=1500)来说这个值往往MSS是1460。通信双方会根据双方提供的MSS值的最小值确定这次连接的最大MSS值,以试图避免IP数据报分片,降低TCP分段重传的概率。
IP协议
IP协议是TCP/IP协议族中最为核心的协议。所有的TCP、UDP、ICMP及IGMP数据都以IP数据报格式传输。
许多刚开始接触TCP/IP的人对IP提供不可靠、无连接的数据报传输服务感到很奇怪,特别是那些具有X.25或SNA背景知识的人。
不可靠(Unreliable)的意思是它不能保证IP数据报能成功地到达目的地。IP仅提供最好的传输服务。如果发生某种错误,例如某个路由器暂时用完了了缓冲区,IP有一个简单的错误处理算法——丢弃该数据报,然后发送ICMP消息给发送端。任何要求的可靠性必须由上层来提供,例如TCP
无连接(Connectionless)这个术语的意思是IP协议并不维护任何关于后续数据报的状态信息,每个数据报的处理相互独立的。这也说明,IP数据报可以不按发送顺序接收。如果一发送端向同一个接收端发送两个连续的数据报(先是A,后是B),每个数据报都是独立地进行路由选择,可能选择不同的路线,因此B可能在A到达之前到达。
IP协议的作用:
规定互联网上传输的数据的确切格式
完成选路的功能,选择一个数据发送的路径
出了数据格式和选录的精确而正式的定义外,IP还包括了一组体现了不可靠分组交付思路的规则。这些规则指明了主机和路由器应该如何处理分组、何时及如何发出错误信息以及在什么情况下可以放弃分组
物理网络和TCP/IP互联网之间有很轻的相似性。在一个物理网络上,传输的单元是一个包含首部和数据的帧,互联网则把它的基本传输单元称为Internet数据报(Datagram),有时称为IP数据阿波,或仅称为数据报。
数据报服务类型和区分服务
8比特的服务类型(Service Type)字段,在非正式场合下可称为TOS(Type Of Service,TOS),它规定了数据报的处理方式。
3bit的优先级(PRECEDENCE)子字段指明数据报的优先级,允许发送方表示每个数据报的重要程度,优先级的值从0(普通优先级)到7(网络控制)。尽管一些路由器会忽略服务类型,但它的概念十分重要,因为它提供了一种机制,允许控制信息比一般数据的优先级更高。例如,许多路由器使用优先级值6或7来进行路由通信,因此可能在网络拥塞的情况下交换选路信息。
D、T和R比特表示数据报所希望的传输类型。值设为1时,D代表低时延(Delay)需求,T代表高吞吐量(Throughput)需求,R代表要求高可靠性(Reliability)需求。当然,互联网不一定能保证提供所需求的传输(例如,到目的站的路径都不具备所需求的属性).因此,我们把传输需求作为路由算法的一个提示,而不是要求。例如,假设某个路由器可以在一条低容量租用线和一条高带宽(大时延)卫星线路这两条路径之间进行选择时,则把键入的信息丛用户传到远端计算机的数据报可以设置为D比特位为1,使它尽快发送出去;传输一个大块文件的数据报可以设置T比特位为1,要求在高容量卫星线路上传输。
ICMP不可达
情况一:主机不可达
情况二:端口不可达
UDP的规则之一是,如果收到一份UDP数据报而目的端口与某个正在使用的进程不相符,那么UDP返回一个ICMP不可达报文。情况三:IP数据报需要分片,但IP数据报中又声明了不进行分片
当路由器收到一份需要分片的数据报,而在IP首部又设置了不分片(DF)的标志比特。
ICMP源站抑制(Source Quench)错误
当一个系统,路由器或主机,接收IP数据报的速度比其处理速度快时,可能产生这个差错。注意我们使用的限定词"可能",即使一个系统已经没有缓存并丢弃IP数据报,也不要求它一定要发送源站抑制错误(因为源站抑制错误要消耗网络带宽,且对于拥塞来说是一种无效而不公平的调整)。
在BSD的UDP实现中通常忽略其收到的源站抑制报文,而BSD的TCP实现接收并处理源站抑制报文:一个接收到的源站抑制报文引起拥塞窗口被设置为1个报文段大小,并执行慢启动,但是慢启动门限ssthresh没有变化,所以窗口将打开直至它或者开放了所有的通路(受窗口大小和往返时间的限制)或者发生了拥塞。
UDP
UDP检验和
UDP检验和覆盖UDP首部和UDP数据,不同于TCP,UDP的检验和是可选的。
为了计算一份UDP数据报的检验和,首先把检验和字段设置为0.然后,对整个UDP数据报以每16bit为一组计算二进制反码,并求和,最后将所得的值放入检验和字段中。当收到一份UDP数据报后,同样对UDP数据报以每16bit位为一组计算二进制反码,并求和。由于接收方在计算过程中包含了发送方在首部中的检验和,因此,如果UDP数据报在传输过程中没有发生任何差错,那么接收方计算的结果应该全为1。如果结果不是全1(即检验和错误),那么UDP将丢弃收到的数据报。
但是,UDP数据报的长度可以是奇数字节,因此不一定全是16bit的整数倍,故在计算检验和的时候,可能需要在UDP数据报的后面填充字节0以成为16bit的整数倍,以便计算。
此外,UDP数据报包含了一个12字节长的伪首部,它是为了计算检验和而设置的。
伪首部包含IP首部的一些字段。其目的是让UDP两次检查数据是否已经正确到达目的地。
UDP检验和是一个端到端的检验和。它由发送端计算,然后由接受端验证。其目的是为了发现UDP首部和数据在发送端和接收端之间发生的任何变动。
UDP输入队列
大多数UDP服务器是交互服务器。这意味着,单个服务器进程对单个UDP端口上的所有客户请求进行处理。
通常程序所使用的每个UDP端口都与一个有限大小的输入队列相联系。这意味着,来自不同客户的差不多同时到达的请求将由UDP自动排队。接收到的UDP数据报以其接受顺序交给应用程序(在应用程序要求送交下一个数据报时)
然而,排队溢出造成内核中的UDP模块丢弃数据报的可能性是存在的。
TCP概述
TCP提供一种面向连接的、可靠的字节流传输服务。
面向连接:
TCP通过下列方式来提供可靠性:
应用数据被分割成TCP认为最合发送的数据块,以避免IP分片造成的因一个IP分片丢失造成整个报文重传,加重网络拥塞。
当TCP发出一个报文段之后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。==》可靠性
当TCP收到来自TCP连接另一端的报文段时,它将发送一个确认。通常这个确认不是立即发送,而是推迟几分之一秒,以减少报文段的发送数量,缓解网络拥塞。
TCP将保持它首部和数据的校验和。这时一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到报文段的校验和有差错,TCP将丢弃这个报文段,但并确认收到次报文段,并等待对端超时重发
既然TCP报文段使用IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果有必要,TCP将对收到的数据进行重新排序,将收到的字节流以正确的顺序交给应用程。
既然IP数据报可能会发生重复,TCP的接收端必须丢弃重复的数据。
TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间(输入、输出缓冲区),TCP的接收到只允许另一端发送接收端所能容纳的数据。否则,将导致缓冲区溢出。
简而言之,TCP通过发送方的超时重传机制实现可靠传输。通过对字节流分段,缓解网络压力。
两个应用程序通过TCP连接交换8bit字节构成的字节流。TCP不再字节流中插入记录标识符。我们将这称为"字节流服务(Byte Stream Service)"。
TCP分段
TCP提供字节流传输服务。由于MTU的限制,因此当字节流大小大于MTU时,TCP需要对字节流进行分段传输和重组。在建立TCP连接的过程中,TCP会通过MSS机制协商出最合适的最大字节流分段长度。
和IP分片一样,为了正确地进行重组,TCP需要对TCP分段进行编号以便为正确地重组提供依据。
除此之外,为了实现数据的高效传输,TCP需要对丢失TCP分段进行重传,而不是重传整个字节流(事实上也不可能,因为是字节流传输服务,因此不可能知道字节流的大小,即无法获知本次TCP连接的整个字节流)。为此TCP的实现需要使用输入缓冲区和输出缓冲区来实现字节流的可靠传输。
术语解释:由TCP传递给IP的信息单元成为"报文段(Datagram Segment)"或"段(Segment)"
MSL(Maximum Segment Lifetime,报文段最大生存时间)
每个具体的TCP实现必须选择一个MSL。MSL是任何报文段被丢弃前在网络内的最长时间。
我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
TCP选项
TCP首部可以包含选项部分。在TCP的选项中定义的选项有选项表结束、无操作、最大报文段、窗口扩大因子、时间戳。
每个选项的开始是1个字节的kind字段,说明选项的类型。
kind为0表示选项表结束,kind为1表示无操作,这两个选项仅占用1个字节。其它的选项在kind字段后还有len字段,用于说明选项的总长度(包括kind字段和len字段)。
(设置无操作选项的原因在于允许发送方填充选项字段为4字节的倍数)。
TCP连接
TCP是一个面向连接的协议。无论哪一方向向另一方发送数据之前,都必须先在双方之间建立一条连接。
TCP的通信模型采用C/S架构。
TCP连接的连接
请求端(通常称为客户)发送一个
SYN
报文段指明其打算连接的服务器的端口,以及初始序号(Initial Serial Number,ISN)。服务器发回包含服务器的初始序号SYN报文段作为应答,同时将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。
客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认。
当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN岁时间而变化,因此每个连接都将具有不同的ISN。RFC指出ISN可看作是一个32比特的计数器,每4ms加1.这样选择序号的目的在于防止网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它作错误的解释。
平静时间
对于来自某个连接的较早替身的迟到TCP报文段,2MSL等待可以防止将它解释成使用相同Socket对的新连接的一部分。但这只有处在2MSL等待连接中的主机处于正常工作状态时才有效。
如果使用处于2MSL等待端口的主机出现故障,它会在MSL秒内重新启动,并立即使用故障前仍处于2MSL的Socket对来建立一个新的连接吗?如果是这样,在故障前从这个连接发出的迟到报文段会错误地当作属于重新启动后新连接的报文段。无论如何选择重新启动后新连接的初始序号,都会发生这种情况。
因此,为了防止这种情况,RFC 793指出TCP在重启后的MSL秒内不能建立任何连接。这就称为"平静时间(quiet time)".
只有极少数的TCP实现遵循这一原则,因为大多数主机重启的时间都比MSL秒要长。
同时打开
两个应用程序同时彼此执行主动打开的情况是可能的,尽管发生的可能性极小。
每一段必须发送一个SYN,且这些SYN必须传递给对方。这需要每一方使用一个对方熟知的端口作为本地端口,这又称为"同时打开(Simultaneous Open)"。
例如,主机A中的一个应用程序使用本地端口7777,并与主机B的端口8888执行主动打开,主机B的应用程序则使用本地端口8888,并与主机A的端口7777执行主动打开。
TCP的设计特意考虑了同时打开的情况,对于同时打开,TCP仅建立一条连接而不是两条连接(其它的协议族,最突出的是OSI传输层,在这种情况下将建立两条连接而不是一条连接)。
当出现同时打开的情况时,两端几乎在同时发送SYN,并进入SYN_SENT状态。当每一端收到SYN时,状态变为SYN_RCVD,同时它们都再发SYN+ACK(对收到的SYN进行确认),当双方都收到SYN及相应的ACK时,状态都变迁为ESTABLISHED。
一个同时打开的连接需要交换4个报文段,比正常的三次握手多一个。此外,要注意的是我们没有将任何一段称为客户或服务器,因为每一段既是客户又是服务器。
TCP连接的终止
建立一个连接需要三次握手,而终止一个连接需要经过4次握手。这由TCP的半关闭(Half-Close)造成的。
既然一个TCP连接是全双工的,即数据在两个方向上能同时传输,因此每个方向必须单独地进行关闭。
这原则就是当一方完成它的数据发送任务之后就能发送一个FIN来终止这个方向连接,当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。
当TCP收到一个FIN,会向应用层返回一个EOF
FIN_WAIT_2状态
在FIN_WAIT_2状态,我们已经发出了FIN,并且另一端也以对此FIN进行确认,即本端到对端的连接已经关闭,此时等待对端发送FIN来关闭对端到本端的连接。
只有当另一端的进程完成这个完毕,即发送FIN,本端才会从FIN_WAIT_2状态进入TIME_WAIT状态,否则,本端可能永远保持FIN_WAIT_2状态。但,如果此时对端的主机奔溃了怎么办呢,对端重启后根本就不会再发送FIN给我们,本端无法进入TIME_WAIT状态。
在BSD的TCP实现中,为了防止FIN_WAIT_2状态的无限等待。如果执行主动关闭的应用层将进行全关闭(close),而不是半关闭(shutdown)来说明它还想接收数据,就设置一个定时器。如果这个连接空闲10分钟75秒,TCP将进入CLOSED状态。
TIME_WAIT
TIME_WAIT状态也称为2MSL等待状态。
每个TCP实现必须选择一个报文段最大生存时间(Maximum Segment Lifetime,MSL)。它是任何TCP报文段被丢弃前在网络内的最长时间(IP数据报有一个TTL,是基于跳数)。我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
RFC 793指出MSL为2分钟。然而,实现中的常用值是30秒,1分钟,或2分钟。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL,这样可以让TCP再次发送最后的ACK以防止这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的Socket对(客户端的IP地址和端口号。服务器的IP地址和端口)不能再使用。这个连接只能在2MSL结束后才能再被使用。
遗憾的是,大多数TCP实现,譬如BSD的实现,强加了更为严格的限制——在2MSL等待期间,(主动关闭的一方)Socket中使用的本地端口在默认情况下不能再被使用。但是它们通常提供SO_REUSEADDR
选项来重用仍然处于2MSL等待的端口,但TCP不能允许一个新的连接建立在相同的Socket对上。。
在连接处于2MSL等待期间,即TIME_WAIT状态,任何迟到的报文段都将被丢弃。因为处于2MSL等待的、由该Socket对定义的连接在这段时间内不能再被使用,因此当要建立一个有效的连接时,来自该连接的一个较早替身(incarnation)的迟到报文段作为新连接的一部分不可能被曲解(一个连接由一个Socket对来定义,一个连接的新的实例(instance)称为该连接的替身)。
同时关闭
和同时打开一样,同时关闭(Simultaneous Close)也是可能的,即双方都执行主动关闭。
应用层发送关闭命令时,两端均从ESTABLISHED变为FIN_WAIT_1,这将导致双方各发送一个FIN,两个FIN经过网络传送到另一端,收到FIN,状态由FIN_WAIT_1变迁为CLOSING,并发送最后的ACK,当收到最后的ACK时,状态变化为TIME_WAITR。
连接的异常终止
前面介绍的终止TCP连接的方式都属于正常方式,又称为"有序释放(orderly release)",因为其会在输出缓存中的数据都发送出去之后才发送FIN,即正常情况下没有任何数据丢失。
但是有时候有可能发送一个复位RST报文段,而不是FIN,来中途释放一个连接,有时这称为"异常释放(aboritive release)"。
异常终止一个连接对应用程序来说有两个优点:
丢弃输出缓存中的所有数据并立即发送RST报文段
RST报文段的接方会区分另一端执行的是以异常关闭还是正常关闭。
注意:RST报文段不悔导致另一端产生任何响应,另一端根本不进行确认。收到RST的一方将终止连接,并通知应用层连接复位。fr
Socket API通过"linger on close"选项(SO_LINGER
)提供了这种异常关闭TCP连接的能力。
检测半打开连接和Keepalive
如果一方已经关闭或异常终止连接而另一方却还不知道,我们称这样的TCP连接称为“半打开(Half-Open)”的。任何一端的主机异常都可能导致发生这种情况。只要不打算在半打开连接上传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常。
半打开连接的另一个常见原因是客户主机突然掉线而不是正常的结束客户应用程序后再关机。我们可以使用TCP的KeepAlive选项能够使TCP的一端发现另一端已经消失。
半关闭
相信各位已对“关闭套接字的一半连接”有了充分的认识,但还有一些疑惑。
“究竟为什么需要半关闭?是否只要留出足够长的连接时间,保证完成数据交换即可?只要不急于断开连接,好像也没必要使用半关闭。”
这句话也不完全是错的。如果保持足够的时间间隔,完成数据交换后再断开连接,这时就没必要使用半关闭。但要考虑如下情况:
“一旦客户端连接到服务器端,服务器端将约定的文件传给客户端,客户端收到后发送字符串‘Thank you’给服务器端。”
此处字符串“Thank you”的传递实际是多余的,这只是用来模拟客户端断开连接前还有数据需要传递的情况。此时程序实现的难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端则无法知道需要接收数据到何时。客户端也没办法无休止地调用输入函数,因为这有可能导致程序阻塞(调用的函数未返回)。
“是否可以让服务器端和客户端约定一个代表文件尾的字符?”
这种方式也有问题,因为这意味着文件中不能有与约定字符相同的内容。为解决该问题,服务器端应最后向客户端传递EOF表示文件传输结束。客户端通过函数返回值接收EOF,这样可以避免与文件内容冲突。剩下最后一个问题:服务器如何传递EOF?
“断开输出流时向对方主机传输EOF。”
当然,调用close函数的同时关闭I/O流,这样也会向对方发送EOF。但此时无法再接收对方传输的数据。换言之,若调用close函数关闭流,就无法接收客户端最后发送的字符串“Thank you”。这时需要调用shutdown函数,只关闭服务器的输出流(半关闭)。这样既可以发送EOF,同时又保留了输入流,可以接收对方数据.
TCP服务器
呼入连接请求队列
一个并发服务器调用一个新的进程来处理每个客户请求,因此处于被动连接请求的服务器应该始终准备处理下一个呼入的连接请求。那正是使用并发服务器的根本原因。
但仍有可能出现当服务器在创建一个新的进程时,或操作系统正忙于处理优先级高的进程时,到达多个连接请求。当服务器正处于忙时,一个TCP的实现是如何处理这些呼入的连接请求的呢?
在BSD的TCP实现中采用以下规则:
正等待连接请求的一端有一个固定长度的连接队列,该队列中的连接已被TCP(实现)接受,即TCP的三次握手已经完成,但还没有被应用层所接受。
注意:TCP(实现)接收一个连接是将其放入这个队列,而应用程接收连接是调用accept()
将其从该队列中移除。应用层需要指明该连接队列的最大长度,这个值通常被称为"积压值(backlog)"。它的取值范围是[0,5],大多数的应用程序都将这个值设置为5.
注意,积压值说明的是TCP监听的端点已被TCP实现所接受而等待应用层接受的最大连接数.这个积压值对系统所允许的最大连接数,或者并发服务器所能并发处理的客户数,并无影响。
当一个连接请求(即SYN)到达时,TCP使用一个算法,根据当前连接队列中的连接数来确定是否接收这个连接。
我们期望应用程说明的积压值为这一端点所能允许接受连接的最大数目,但情况并没有这么简单。
积压值 | 传统BSD的最大排队的连接数 | Solaris2.2最大排队连接数 |
---|---|---|
0 | 1 | 0 |
1 | 2 | 1 |
2 | 4 | 2 |
3 | 5 | 3 |
4 | 7 | 4 |
5 | 8 | 5 |
从上面我们可以发现,Solaris系统规定的值正如我们所期望的。而传统的BSD系统,将这个值(由于某些原因)设置为积压值乘3除以2,再加1
如果对于新的连接请求,该TCP监听的端点的连接队列中还有空间,TCP实现奖对SYN进行确认并完成连接的建立。但应用层只有在第三次握手中的第三个报文段收到后才会知道这个新连接。
另外,当客户进程的主动打开成功但服务器的应用层还不知道这个新的连接时,它可能会认为服务器已经准备好接收数据了(如果发生这种情况,服务器的TCP仅将接收的数据放入其输入缓冲队列)。
如果对于新的连接请求,连接队列中已没有空间,TCP将不会处理收到的SYN,即不会回复任何报文段,包括RST报文段。
如果应用层不能及时接受已被TCP实现接受的连接买这些连接可能占满整个连接队列,客户的主动打开最终将超时。
当队列已满时,TCP将不理会传入的SYN,也不发回RST作为应答,因为这是一个软错误,而不是一个硬错误。通常队列已满是由于应用程序或操作系统忙造成的,这样可以防止用用程序对传入的连接进行服务。这个条件在一个很短的时间内可以改变。但如果服务器的TCP以系统复位作为响应,客户进程的主动打开将被废弃(如果服务器程序没有启动我们就会遇到)。由于不应答SYN,服务器程序迫使客户TCP随后重传SYN,以等待连接队列有空间接受新的连接。
TCP数据传输
一些有关TCP通信量的研究发现,如果按照分组数量计算,约有一半的TCP报文段包含成块数据(如FTP、电子邮件和新闻),另一半则包含交互数据(如Telnet和RLogin)。
如果按字节计算,则成块数据与交互数据的比例分别越为90%和10%。
这是因为成块数据的报文段基本是是满长度(full-size)的;而交互数据则小得多(研究表明Telnet和Rlogin分组中通常越90%左右的用户数据小于10个字节)。
很明显,TCP需要同时处理这两类数据,但使用的处理算法则有所不同。
对于交互数据,TCP需要减少因大量小型的交互数据产生的大量分组来缓解网络拥塞。TCP针对交互数据流提出了Nagle算法用于缓解网络拥塞。
对于成块数据,TCP需要控制两端的收发速率以确保数据能被接受,避免因数据不能被接受而造成的数据段重发,进而加剧网络拥塞。针对成块数据,TCP采用滑动窗口机制。
滑动窗口机制
数据的接收方可能因为种种原因来不及对输入缓冲区中的数据进行处理,如果不控制发送方的发送速率,当接收方的输入缓冲区溢出之后,接收方将丢弃后面收到的TCP报文段,这可能会造成发送方的不断重发,加剧网络拥塞。
此外,当接收方的缓冲区无法容纳发送方的TCP报文段时,则会造成接收方的缓冲区溢出,或者部分数据被丢弃,这种现象违背了TCP所声明的可靠传输。
为此TCP必须根据接收方的输入缓冲区大小来控制发送方的发送,又称为"流量控制"。TCP采用滑动窗口机制来解决该问题。
滑动窗口机制将接收方输入缓冲区中的空闲空间称为"接收窗口",将发送方输出缓冲区中的能发送的空间称为"发送窗口"。并在TCP首部的中设置一个窗口大小字段用于通告对端我方的接收窗口大小,因此接收窗口又称为"通知窗口(Offered Window)"。发送方根据通知窗口大小,即接收方的接收窗口大小,来调整器发送窗口大小。
和文件I/O一样,输入缓冲区越大,数据的传输效率就越高。
在4.2BSD的TCP实现中,发送缓冲区和接收缓冲区的默认大小为2048个字节,在4.3的BSD实现中,发送缓冲区和接收缓冲区的默认大小为4096个字节。目前较新的系统中使用较大的值,可以是[8192,61440]字节间的任何值。
我们可以通过Socket API中的setsockopt()
函数和SO_RCVBUF
选项来设置输入缓冲区的大小,SO_SNDBUF
选项来设置输出缓冲区的大小。
为了支持TCP快恢复算法机制,TCP套接字输入缓冲区和输出缓冲区的大小至少是相应连接的MSS值的四倍。因此,典型的缓冲区大小默认值是8192字节或更大。
[Mogul 1993]
显示了在改变发送和接收缓存的大小(在单向数据流的应用中,如文件传输只需要改变发送方的发送缓存和接收方的接收缓存大小)的情况下,位于以太网上的两个工作站之间进行文件传输时的一些结果。它表明对以太网而言,默认的4096字节并不是最理想的大小,将两个缓冲大小增加到16384个字节可以增加约40%的吞吐量。
注意:使用TCP的滑动窗口协议时,接收方不必确认每一个收到的分组。在TCP中,ACK是累积的——它们表示接收方已经正确收到了一直到确认序号减1的所有字节。
那么窗口应当设置为多大呢?或者说,窗口应当至少为多大才能满足TCP连接最大吞吐量的要求?答案如下:
capacity(bit) = bandwith(b/s) * round-trip time(s)
上述公式得出的是TCP连接通道的容量,又称为"带宽时延乘积"。
坚持定时器(Persist Timer)
当通知窗口的大小为0时,意味着对端的输入缓冲区的可用空间为0,本端应当停止发送字节流,直到收到一个对端的通知窗口更新(其本质是一个ACK确认报文段)。
但是如果通知窗口的更新报文段丢失了怎么办?,这是一个比较难过的问题。本端继续等待对端通知窗口更新的报文段而不发送字节流;同时对端以为带有通知窗口更新的ACK确认报文段已经被收到,则不会再发送通知窗口更新,这样本端到对端的数据流就出现了死锁的情况——我端等待通知窗口更新才会发送数据,对端等待数据才会发送通知窗口更新。
究其原因在于,通知窗口更新存在于接收方的ACK报文段中,但发送方并不对ACK报文段进行确认,即ACK报文段的传输并不可靠。
TCP的解决方案是:当接受方的通知窗口为0时,采用一个坚持定时器来周期性地向接收方查询通知窗口,我们称此报文段为"窗口探查报文段(Window Probe)",以便发现窗口是否扩大,从而可以避免因唯一的一个带窗口更新ACK报文段丢失而造成的死锁。
坚持定时器与超时重传之间的一个不同点在于:TCP从不放弃对对端的通知窗口探测,这些探查每60秒发送一次,这个过程将持续到对端窗口被打开,或者应用程序所使用的连接被终止。
糊涂窗口综合症
基于窗口的流量控制方案,会导致一种被称为"糊涂窗口综合症(Silly Window Syndrome,SWS)"的状况。如果发生这种情况,则少量的数据将通过连接进行交换,而不是满长度的报文段。
这种现象可以发生在两端中的任何一段:接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告),而发送方也可以发送少量的数据(而不是等待其它的数据以便发送一个大的报文段)。可以在任何一段采取措施避免出现糊涂窗口综合症的现象:
接受方:不通告小窗口更新。
通常的算法是接受方只有当该窗口可以增加一个报文段大小(MSS),或者可以增加其输入缓冲的一半,不论有多少,才通告其接收窗口更新,否则不通告此窗口更新。发送方
发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才会发送数据:可以发送一个满长度(MSS)的报文段
可以发送至少是接收方通告窗口大小一半的报文段
Nagle算法
交互数据流之Nagle
试想,我们在使用即时通讯软件进行通讯时,通常我们发送的一条消息的长度约为20个字节左右,这就产生了一些60个字节的分组:20个字节的IP首部,20个字节的TCP首部和20字节的用户数据。
我们将这些字节数小于MSS的报文段称为"微小分组(Tinygram)"。这些微笑分组在局域网上并引起网络拥塞,而在广域网上则会加剧网络拥塞。TCP作为在广域网上进行通讯的主要手段,因此必须要缓解因微笑分组所带来的对网络拥塞的加剧。
一种简单而友好的方法就是采用RFC 896种所建议的Nagle算法。
Nagle算法要求:一个TCP连接上最多只能有一个未被确认的微小分组,在该分组的确认到达前,不能发送其它的微小分组。此外,TCP利用这个该微小分组被确认的时间差来收集后续的少量字节流,并在该确认到达时再将收集的字节流发出去。
关闭Nagle算法
有时我们的微小分组必须无时延地发送,这时我们就必须关闭Nagle算法。
在BSD的实现中,Nagle算法默认是开启的。我们可以通过setsockopt()
和TCP_NODAYLAY选项来关闭Nagle算法。
慢启动(Slow Start)和拥塞避免
当发送方和接收方处于同一个局域网时,发送方一开始便向网络发送多个报文段,这是正常的。但,在广域网上,发送方和接收方之间存在众多路由器并且存在一些速率较慢的链路,此时就会出现一些问题。一些中间路由器必须缓存分组以待发送,并有可能耗尽路由器的存储器空间。这会严重降低TCP连接的吞吐量。
因此,TCP需要支持一种被称为"慢启动"的算法来解决上述问题。
慢启动为发送方的TCP增加了一个窗口——"拥塞窗口(Congestion Window)",记为cwnd。
当一个TCP连接建立时,拥塞窗口被初始化为1个报文段(MSS)大小。
此后每收到一个ACK,拥塞窗口就增加1个报文段(MSS)大小(cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加)
发送方取拥塞窗口和通知窗口的最小值作为发送上限,即取拥塞窗口和通知窗口的最小值作为发送窗口的大小。
当发送方发出的报文段出现超时,则将拥塞窗口设置为1个报文段
拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
发送方开始时发送一个报文段,然后等待ACK。当收到该ACK时,拥塞窗口从1增加为2,则可以发送两个报文段。当收到这两个报文段的ACK时,拥塞窗口就增加为4.在一个往返时间RTT内,这是一种指数递增的关系。(注意,这只是针对存在连续不断的数据流而言,如果是小数据流,譬如即时通讯的消息,可能每次都只发送一个报文段,那么拥塞窗口则是线性增加)。
对于连续不断的成块数据流,在慢启动的情况下,每一次所能连续发送的报文段数目也终将会达到中间路由器的极限,此时分组将会被丢弃。拥塞避免算法是一种处理丢失分组的方法。
拥塞避免算法假定犹豫分组受到损坏引起的丢失是非常少的(远小于1%),因此分组丢失就意味着源主机和目的主机之间的网络某处发生了拥塞。有两种分组丢失的指示:发生超时和收到重复的确认(即由快重传机制引起的重复确认)。
拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来做到这一点。在实际中这两个算法通常在一起实现。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口(cwnd)和一个慢启动门限(ssthresh)。这样得到的算法的工作过程如下:
当一个TCP连接建立时,拥塞窗口cwnd被初始化为1个报文段(MSS)大小,慢启动门限ssthresh被初始化为65535个字节。
此后每收到一个ACK,拥塞窗口就增加1个报文段(MSS)大小(cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加)
发送方取拥塞窗口和通知窗口的最小值作为发送上限,即取拥塞窗口和通知窗口的最小值作为发送窗口的大小。
当拥塞窗口cwnd大于慢启动门限ssthresh时则进行拥塞避免,而不进行慢启动
当拥塞发生时(超时或者收到重复确认),慢启动门限ssthresh被设置为当前发送窗口的一半。并且如果是由于超时引起了拥塞,则将拥塞窗口设置为1个报文段(这就是慢启动).
拥塞避免的思想是使用sssthresh来记录上一次发生网络拥塞时,发送窗口的情况。然后在本次慢启动过程中设置一个阈,即慢启动门限ssthresh,当超过该阈值时,就开始尝试避免网络拥塞。
拥塞避免算法要求每次收到一个确实时,就将拥塞窗口cwnd增加1/cwnd。与慢启动的指数增加比起来,在一个往返时间RTT内,这是一种加性增加(additive increase)。我们希望在一个往返时间RTT内最多为cwnd增加一个报文段,而不管在在这个RTT中收到了多个ACK
。
拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。拥塞避免是发送方感受到的网络拥塞的估计,而通告窗口与接收方在此连接上的可用缓存大小有关系。
快重传和快恢复
我们知道当收到一个失序的报文段时,接收方必须立即产生一个ACK(相比失序报文段的前一个ACK来说,是一个重复的ACK)。这个重复的ACK不应该被迟延。该重复的ACK的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。
由于我们不知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的ACK到来。加入这只是一些报文段的重新排序,则在重新排序的报文被处理并产生一个新的ACK之前,只可能产生1~2个重复的ACK。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快重传算法。此后,接下来执行的不是慢启动算法,而是拥塞避免算法。
这种情况下没有执行慢启动的原因是由于收到重复的ACK不仅仅告诉我们一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的ACK,而该报文段已经离开了网络并进入了接收方的缓存,也就是说,在收发量段之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。
这个算法通常按如下过程进行实现:
当收到第3个重复的ACK是,将ssthresh设置为当前拥塞窗口的一半。并重传丢失的报文段。设置当前拥塞窗口cwnd为ssthresh加上3倍的报文段大小。
每次收到另一个重复的ACK时,拥塞窗口cwnd增加1个报文段大小,并发送1个分组(如果新的cwnd允许发送)
当下一个确认数据的ACK到达时,设置拥塞窗口cwnd为ssthresh(在1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第一个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
PUSH
在C/S架构中,Client通过TCP实现向Server发送数据时,Client端的TCP实现可能并不会立即将数据传输到Server端,而是继续等待额外的数据(Nagle算法就是这么做的)。
此外,Server端的TCP实现收到Client端发送过来的数据之后并不会立即将这些数据递交给Server,而是继续等待额外的数据。
但是有时候,我们希望发送方发送的数据,能够立即被传输到接收方。为此,TCP协议规定,发送方在发送数据时,能够在TCP报文段中使用PUSH
标志通知接收方的TCP实现将其所收到的数据全部交给应用层。这里的数据包括与该PUSH标志所在报文段中的数据,以及TCP实现中输入缓存中的数据。
然而,目前大多数的API,包括BSD Socket实现,都没有向应用层提供通知其TCP实现设置PUSH标志的能力.
由于BSD 的TCP实现一般不会将接收到的数据推迟交付给应用层,因此它们会忽略所接收到的PUSH标志。
此外,BSD的TCP实现默认只会推迟发送微小分组,我们可以通过关闭Nagle算法来要求其立即发送,但Nagle算法并不会对我们的数据发送产生太多的延迟。
因此,BSDd的TCP实现并不提供设置PUSH标志的能力。
紧急数据
在交互应用程序中,有时候需要提供一些紧急数据处理功能。譬如,我们在使用ssh命令行工具进行远程登录时,用户通过客户端向服务器发送一个系列命令启动了一些列任务,如果用户因某种原因想立即停时这些任务(包括还未开始执行的任务),这时客户端程序应提供向服务器端发送紧急数据的能力,即通知服务器端立即处理此数据而不是让该数据继续排队等待处理。
为此,TCP实现提供了"紧急方式(Urgent Mode)",它使一端可以告诉另外一端有些具有某种方式的"紧急数据"已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。
大多数RFC文档,都称为"紧急指针(urgent pointer)",更准确的名称是"紧急数据偏移量(urgent offset)"。
可以通过设置TCP首部中的两个字段来发出这种从一端到另一端的禁忌数据已经被放置在数据流中的通知。URG比特位被置1,并且一个16bit的禁忌指针被设置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。
仍有许多关于紧急指针是指向紧急数据的最后一个字节还是指向就近数据最后一个字节的下一个字节的争论。最初的TCP规范给出了两种解释,但Host Requirements RFC确定之乡最后一个字节是正确的。
TCP必须通知接收进程,何时已经接收到一个紧急数据指针以及何时某个紧急数据指针还不在此连接上,或者禁忌指针是否在数据流中向前移动。接着接收进程可以读取数据流,并必须能够被告知何时碰到了紧急数据指针。只要从接收方当前读取位置到紧急数据指针之间有数据存在,就认为应用程序处于"紧急方式"。在紧急通过之后,应用程序便转回到正常方式。
TCP本身对紧急数据知之甚少。没有办法指明紧急数据从数据流的何处开始。TCP通过连接传送的唯一信息就是紧急方式已经开始(TCP首部中的URG比特)和指向紧急数据最后一个字节的指针。其它的事情留给应用程序去处理。
不幸的是,许多TCP实现不正确地称TCP的禁忌方式为带外数据(out-of-band data),包括BSD Socket。如果一个应用程序确实需要一个独立的带外信道,我们可以使用第二个TCP连接来达到这个目的(许多传输层协议确实提供许多人认为的那种真正的带外数据:使用同一个连接的独立的逻辑数据通道作为正常的数据通道。但TCP并不提供)。
TCO的紧急方式与带外数据之间的混淆,也是因为主要的编程接口Socket API将TCP的紧急方式映射称为带外数据的接口。
TCP的可靠性
TCP的可靠性体现在通信双方接收到的数据是一致的,即接收方收到到的数据是正确的。因为TCP强调数据的一致性,因此数据报之间的顺序变得很重要,因此数据报被组织成"流(Stream)",因此TCP提供的是字节流传输服务。即数据传输的过程是可靠的,不会发生失序的情况。
TCP的可靠性指的并不是报文一定可以被收到,这是任何通信技术都做不到的。
TCP提供可靠的传输层,它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器超时时还没有收到确认,就重传该数据。
对于任何TCP实现而言,关键之处在于超时和重传的策略,即怎样决定超时间隔和如何确定重传的频率。
对于每个连接,TCP管理4个不同的定时器:
重传定时器
重传定时器用于希望收到另一端的确认。坚持(Persist)定时器
坚持定时器用于使通知窗口大小保持不断流动,即使另一端关闭了其接收窗口。保活(KeepAlive)定时器
保护定时器用于检测到一个空闲连接的另一端何时奔溃或重启。2MSL定时器
2MSL定时器用于测量一个连接处于TIME_WAIT状态的时间。
往返时间(Round-Trip Time,RTT)测量
TCP超时与重传中最重要的部分就是对与一个给定连接的往返时间的测量。
由于路由器和网络流量均会变化,因此我们认为这个时间可能经常会发生变化,TCP实现应该跟踪这些变化并相应地改变其超时时间。
首先TCP必须测量在发送一个带有特别序号的字节和接收到包含该字节的确认之间的往返时间,RII。
为了搜集自适应算法所需的数据,TCP对每个报文段都记录下发送出的时间和其确认信息到达的时间。由此TCP计算出所经历的时间,即样本往返时间(Sample Mund Trip Time)或往返时间样本。每当得到新的往返样本之后,TCP就修改这个连接的平均往返时间。通常TCP软件把估算的往返时间RTT(Round Trip Time)存储起来作为一个加权平均值。再使用心得往返岩本逐步地修改这个平均值。
例如,在计算新的加权平均值时,有一种早起用过的平均技术是使用一个常数因子α(0 <= α < 1),对旧的平均值和最新的样本往返时间进行加权:
RTT = (α * OLD_RTT ) + (1 - α) * New_Sample_RTT
选用的α
值接近1(推荐值为0.9),则加权平均值对短暂的时延变化不敏感(例如,仅有一个报文段遇到了时延很长的情况),而α值接近0则加权平均值很快地随时延变化
超时(Timeout)
发送TCP报文段时,TCP需要计算出定时时限(Timeout),以作为判断TCP报文段是否超时的依据。
定时时限是一个关于当前的往返时间估计值的函数。
早期运行的TCP协议都使用常数加权因子β(β > 1),使定时时限大于当前平均往返时间:
Timeout = β * RTT
选择合适的β值很困难。一方面,为了迅速检测到分组的丢失,定时时限要尽可能接近当前的平均往返时间(即β要接近1)。迅速检测分组的丢失能提高网络吞吐率,因为TCP在重传之前无需进行不必要的长时间等待;另一方面,如果β=1,那么TCP就显得太急切了,因为任何微小的时延就会导致不必要的重传,这会浪费网络带宽。
我们可以总结出下列概念:
为了适应互联网环境变化的时延,TCP使用自适应重传算法来检测各个连接的时延,并调整相应的超时参数。
TCP的保活定时器
TCP是在无连接的IP网络之上实现面向连接的信息传输,其是通过在通信双方保持连接的相关信息来实现"连接",链路本身并不保存连接的信息。因而即使中间路由器奔溃或重启,只要通信双方依然保持着连接的相关信息,本次连接就不会断开。
这一点,和HTTP协议中的Cookie机制是一样的。HTTP协议本身是无状态的,通过使用Cookie在客户端保存连接的状态,在服务器端保持连接的状态,来共同实现HTTP连接。这样,只要浏览器中的Cookie依旧存在,Web服务器和客户端之间的连接就依旧存在。
TCP的这一特点使得只有进行了四次握手,本次连接才会真正地断开。
但是有时候,连接的一端可能会出现如下的情况:
未进行四次握手,直接断开
主机可能因为断电等异常原因,来不及进行四次握手来断开连接。而对方却一直以为该主机依然"在线",从而造成业务逻辑出错。物理连接断开
主机可能会出现物理断开连接,譬如,网线接触不良。此时,通信双方都一直以为双方都"在线",从而造成业务逻辑出错。
上述两种情况所造成的连接断开,只有当一方发送数据给另一方时才会发现。显然,这很多情况下这是不能容忍的,因此,我们必须实现对真实连接状态的监测,我们称这一功能为"保活探测"。
保活探测主要是为服务器应用程序提供的。服务器应用程序希望知道客户主机是否奔溃,从而可以代表客户使用资源。客户端有时候也需要使用保活探测,譬如即时通信客户端需要借助保活探测来判断自身的在线状态。
TCP保活探测并不是TCP协议的一部分,幸运的是在很多TCP实现中都提供了保活探测功能。我们可以通过Socket API提供的setsockopt()
和SO_KEEPALIVE
选项来设置。
TCP保活探测机制:如果一个给定的连接在两个小时之内没有任何动作,则开启保活探测机制的一方就向对方发送一个探查报文段(Keepalive probe)。根据TCP协议,对方必须通过发送ACK确认来响应此TCP报文段,它会导致以下三种情况之一:
对端以期望的ACK响应。
说明对端依然在线.应用层得到不到通知,因为一切正常。然后在两小时以后再次打开保活定时器.如果在两小时之内次连接有应用层的数据通过,则定时器在交换数据后的两小时再次打开保活定时器。
嗯,保活定时器是用来衡量一个TCP保活探查报文段的超时时间的。
对端以RST响应
说明对端主机奔溃并已经重启。对端主机对收到的探测报文段以RST响应,使得我方终止这个连接。
为什么是奔溃呢?因为主机正常退出时,操作系统会在TCP连接上发送一个FIN。
此时,TCP实现将会给应用层返回TCP差错报告,在Socket API中,会将该差错作为读操作的返回值返回给应用层,即将Socket的待处理错误设置为ECONNREST
,并自动关闭套接字。对端不响应
说明对端主机已经奔溃,并且关闭或者正在重新启动。
在这种情况下,我方将接收不到对探查报文段的响应,并在75秒之后保活定时器超时,服务器会接着发送探查报文段,总共发送10个,每个间隔75秒。如果我方一直没有收到一个响应,则认为对端主机已经关闭并终止连接。
此时,TCP实现将会给应用层返回TCP差错报告,在Socket API中,会将该差错作为读操作的返回值返回给应用层,即将Socket的待处理错误设置为ETIMEOUT
,并自动关闭套接字。
然而,如果该Socket接收到一个ICMP错误作为某个探查报文段的响应,那就返回响应的错误,并自动关闭套接字。这种情况下,一个常见的ICMP错误是"host unreachable"(主机不可大),说明对端可能并没有奔溃,只是不可达,这种情况下Socket的待处理错误设置为EHOSTUNREACH
。
TCP的保活机制的最长非活动时间——2小时,并且不能针对每一个连接修改此时间,因此并不一定能被我们的业务逻辑所接受,此时我们需要自己在应用层实现保活机制。
长肥管道
我们把一个连接的容量表示为:
capacity(b) = bandwith(b/s) x round-trip time(s)
并称之为"带宽时延乘积",也称为"两端的管道大小"。
当这个乘积变得越来越大时,TCP的某些局限性就会暴露出来
网络 | 带宽(b/s) | RTT(ms) | 带宽时延乘积(字节) |
---|---|---|---|
以太网局域网 | 10 000 000 | 3 | 3750 |
横跨大陆的T1电话线 | 1 544 000 | 60 | 11 580 |
卫星T1电话线 | 1 544 000 | 500 | 96 500 |
横跨大陆的T3电话线 | 45 000 000 | 60 | 337 500 |
横跨大陆的gigabit线路 | 1 000 000 000 | 60 | 7 500 000 |
可以看到,带宽时延乘积的单位是字节,这是因为我们用这个字节来测量每一端的缓存大小和窗口大小。
具有大的带宽时延乘积的网络被称为"长肥网络(Long Fat Network,LFN)",而一个运行在长肥网络上的TCP连接被称为"长肥管道"。
管道可以被水平拉长(一个长的RTT),或被垂直拉高(较高的带宽),或者两方向均拉伸。
使用长肥管道会遇到多种问题:
TCP首部中窗口大小为16bit,从而将窗口限制在65536个字节内。但是从上表来看,现有的网络需要一个更大的窗口来提供最大的吞吐量。(我们可以通过窗口扩大因子来实现)
在一个长肥网络内的分组丢失会使得吞吐量急剧减少。如果只有一个报文段丢失,我们需要利用快速重传和快速恢复来使得管道避免耗尽。但是即时使用这些算法,在一个窗口内发生的多个分组丢失也会典型地使管道耗尽(如果管道耗尽了,慢启动会使它渐渐填满,但这个过程需要经过多个RTT).
在RFC 1072中建议使用有选择的确认(SACK)来处理在一个窗口发生的多个分组丢失。但是这个功能在RFC 1323种被忽略了,因为作者觉得再把它们纳入TCP之间需要先解决一些技术上的问题。许多TCP实现对每个窗口的RTT仅进行一次测量。它们并不对每个报文段进行RTT测量。在一个长肥网络上需要更好的RTT测量机制。
在长肥网络上,TCP的序号会被盗一个不同的问题。由于序号空间是有限的,在已经传输了4 294 967 296(即4GB)之后序号会被重用。如果一个包含序号N字节数据的报文段在网络上被延迟并在连接仍然有效时出现,会发生什么情况?这仅仅是一个相同序号N在MSL期间是否被重用的问题,也就是说,网络是否足够快以至于在不到一个MSL的时候序号就发生了回绕。
在一个以太网上要发送如此多的数据通常需要60分钟左右,因此不会发生这种情况。但是在带宽增加时,这个事件就会减少:一个T3的电话线(45Mb/s)在12分钟内就会发生回绕,FDDI(100Mb/s)在5分钟内就会发生回绕,而一个千比特兆的网络(1000 Mb/s)在34秒内就会发生回绕。
为此,我们使用TCP的时间戳选贤的PAWS(Protection Against Wrapped Sequence numbers)算法(抱回回绕的序号)。
窗口扩大选项
窗口扩大选项使得TCP的窗口定义从16bit增加为32bit。
这并不是通过修改TCP首部来实现的,TCP首部仍待使用16bit,而是通过定义一个选项来实现对16bit的扩大操作(scaling operation)来完成的。于是TCP在内部将实际的窗口大小维持为32bit的值。
窗口扩大选项只能出现在SYN报文段中,因此当连接建立起来后,在每个方向的扩大因子是固定的。
为了使窗口扩大,两端必须在它们的SYN报文段中发送这个选项。主动建立连接的一方在其SYN中发送这个选项,但是被动建立连接的一方只能够在收到带有这个选项的SYN之后才可以发送这个选项,但是注意,每个方向上的扩大因子可以是不同的。
如果主动连接的一方发送一个非零的扩大因子,但是没有从另一端收到一个窗口扩大选项,说明另一端不支持窗口扩大选项,它就将发送和接收的移位计数器置为0.这就允许较新的系统能够与较旧的,不理解新选项的系统进行互操作。
假定我们正在使用窗口扩大选项,发送移位计数S,而接收移位计数则为R.于是我们从另一端收到的每一个16bit的通告窗口将被左移R位以获得实际的通告窗口大小。每次当我们向对方发送一个窗口通告的时候,我们将实际的32bit窗口大小右移S比特,然后用它来替换TCP首部中的16bit的值。
TCP实现根据输入缓存的大小自动选择移位计数。
时间戳选项
时间戳选项使发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的ACK计算RTT(我们必须说“每一个收到的ACK"而不是"每一个报文段",是因为TCP通常用一个ACK来确认多个报文段)。
我们提到过目前许多TCP实现为每一个窗口只计算一个RTT,对于包含8个报文段的窗口而言这是正确的。然而,较大的窗口则需要进行更好的RTT计算。
时间戳是一个单调递增的值。由于接收方只需要回显收到的内容,因此不需要关注时间戳单元是什么。时间戳选项不需要在两个主机之间进行任何形式的时钟同步。RFC 1323推荐在1毫秒内和1秒之间将时间戳的值加1
4.4BSD在启动时将时间戳始终设置为0,然后每隔500ms将时间戳时钟加1。
在连接建立阶段,对这个选项的规定与前面提到的窗口扩大选项类似。主动发起连接的一方在它的SYN中指定选项。只有在它从另一方的SYN中收到了这个选项之后,该选项才会在以后的报文段中进行设置。
PAWS防止回绕的序号
接收方将时间戳视为序号的一个32bit的扩展。
客户机-服务器模型
大部分网络应用程序在编写时都假设一段是客户,另一端是服务器,其目的是为了让服务器为客户提供一些特定的服务。
可以将这种服务分为两种类型:重复型或并发型。
重复型服务器通过以下步骤进行交互:
I1、 等待一个客户请求的到达
I2、 处理客户请求
I3、 发送响应给发送请求的客户
I4、 返回第1步
重复型服务器主要的问题发生在I2状态。这个时候,它不能为其他客户机提供服务。
并发型服务器采用以下步骤:
I1、 等待一个客户请求的到达
I2、 启动一个新的服务器来处理这个客户的请求。在这期间可能生成一个新的进程、任务或线程,并依赖底层操作系统的支持。这个步骤如何进行取决于操作系统。生成的新服务器对客户的全部请求进行处理。处理结束后,终止这个服务器。
I3、 返回第1步
并发服务器的优点在于它是利用生成其它服务器的方法来处理客户的请求。也就是说,每个客户都有它自己对应的服务器。如果操作系统允许多任务,那么就可以同时为多个客户服务。
应用编程接口
使用TCP/IP协议的应用程序通常采用两种应用编程接口(API):Socket和TLI(Transport Layer Interface,传输层接口)。这两个接口都可以从《UNIX网络编程,卷1》中获知。