TCP总结
TCP
TCP的全称叫传输控制协议(Transmission Control Protocol),这个协议的目的就是为网络数据提供可靠的运输服务。
TCP5个特点:
1) 面向连接。应用程序在使用TCP协议之前,必须先建立TCP连接(三次握手)。数据传输完毕后,必须释放连接。
2)每一条TCP连接只能有两个端点。
3)提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达。(其实我们很多web服务也是要求可靠的,所以服务在应用层的设计是不是可以参考TCP的协议的设计?)
4)全双工通信。通信双方任何时候都能发送数据。
这里面有个重点就是两端都设有发送缓存和接收缓存(在内核中),用来临时存放双向通信的数据。在发送时,应用进程把数据传输给内核中的发送缓存,然后就去做其他事情(典型的异步),然后内核在合适的时候把数据发送出去。接收时,内核把收到的数据放入接收缓存,应用进程在合适的时候读取缓存中的数据。(这两个缓存非常重要,在之后各种IO模型都会在这两个缓存上搞事情)
5)面向字节流。“流”是指流入到进程或从进程流出的字节序列。虽然应用进程和TCP的交互式一次一个大小不等的数据块,但TCP把应用进程传过来的数据看成一连串的无结构的字节流(看成是连续的)。
tcp面向流的概念图
TCP并不关心应用进程一次把多长的报文发送到TCP的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节--如果应用进程传送到TCP缓存的数据块太长,TCP就划分短一些再传送;如果应用进程一次只发过来一个字节,TCP也可以累积有足够多的字节后再构成报文段发送出去。
内容
TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。从这方面来说,读取TCP数据就像从串行端口读取数据一样–无法预先得知在一次指定的读调用中会返回多少字节(也就是说能知道总共要读多少,但是不知道具体某一次读多少)。
为了说明这一点,我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A上的应用程序向主机B发送一条报文。进一步假设主机A有两条报文要发送,并两次调用send来发送,每条报文调用一次。很自然就会想到从主机A向主机B发送的两条报文是作为两个独立实体,在各自的分组中发送的,如图 2-25所示。
但 不幸的是,实际的数据传输过程很可能不会遵循这个模型。主机A上的应用程序会调用send,我们假设这条写操作的数据被封装在一个分组中传送给B。实际 上,send通常只是将数据复制到主机A的TCP/IP栈中,就返回了。由TCP来决定(如果有的话)需要立即发送多少数据。做这种决定的过程很复杂,取决于很多因素,比如发送窗口(当时主机B能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(沿着主机A和B之间的网络路径一次可 以传输的最大数据量),以及连接的输出队列中有多少数据。
下图只显示了主机A的TCP封装数据时可能使用的诸多方法中的4种。在图2-26中,M11和M12表示M1的第一和第二部分,M21和M22与之类似。如图2-26所示,TCP不一定会将一条报文的全部内 容都放在一个分组(一个包)中传送出去。
现在,我们从主机B应用程序的角度来看这种情形。总的来说,主机B应用程序任意一次调用recv时,都不会对TCP发送给它的数据量做任何假设。比如,当主机B应用程序读取第一条报文时,可能会出现下列4种结果。
实际上,可能的结果不止4种,但我们忽略了出错和EOF之类的结果。我们还假设应用程序读取了所有可读的数据。
没有数据可读,应用程序阻塞,或者recv返回一条指示说明没有数据可读。到底会发生什么情况取决于套接字是否标识为阻塞,以及主机B的操作系统为系统调用recv指定了什么样的语义。
应用程序获取了报文M1中的部分而不是全部数据。比如,发送端TCP像图2-26D那样对数据进行分组就会发生这种情况。
应用程序获取了报文M1中所有的数据,除此之外没有任何其他内容。如果像图2-26A那样对数据分组就会发生这种情况。
应用程序获取了报文M1的所有数据,以及报文M2的部分或全部数据。如果像图2-26B或图2-26C那样对数据进行分组就会发生这种情况。
注意,这里还有一个定时问题。如果主机B的应用程序在主机A发送了第二条报文之后一段时间内都没有读取第一条报文,那么这两条报文都会成为可读的。这就和图2-26B所示情况相同了。这些描述说明,通常,在任意指定时刻,可读的数据量都是不确定的。
需要再次说明的是,TCP是一个流协议(stream protocol),尽管数据是以IP分组的形式传输的,但分组中的数据量与send调用中传送给TCP多少数据并没有直接关系。而且,接收程序也没有什么可靠的方法可以判断数据是如何分组的,因为在两次recv调用之间可能会有多个分组到来。即使接收端应用程序的响应非常及时,也可能会发生这种情况。例如,一个分组丢失了,而且后继分组都安全到达,TCP会将后继分组中的数据保存起来,直到重传第一个分组并正确收到为止。此时,所有数据对应用程序都是可用的。
TCP会记录它发送了多少字节,以及确认的字节,但它不会记录这些字节是如何分组的。实际上,有些实现在重传丢失分组的时候传送的数据可能比原来的多一些或少一些。
对TCP应用程序来说,就没有”分组(包)”这种概念。如果应用程序的设计与TCP对数据的分组方式有所关联,就应该考虑重新设计这个应用程序了。
既然任意一次指定的读操作中返回的数据量都是不可预测的,就必须在应用程序中做好应对这种情况的准备,这些情况下边界都是由应用程序级维护的。(交给上层处理)
最简单的情况就是定长报文。在这种情况下,只需要读取报文中固定数量的字节就可以了。
实现
在socket机制中,应用层的程序以send()函数将数据首先发送到本机系统的发送缓存中,我们称之为SendQ,意指这是一个FIFO(先进先出)的队列。这个缓存是系统决定的,并不是在我们的程序中指定的。然后socket机制负责将SendQ中的数据以字节为单位,按照顺序发送给对方的接收缓存RecvQ中。
RecvQ也是一个属于系统的FIFO缓存队列。从程序员的角度看,send()函数只负责把数据送入SendQ,而SendQ何时将数据发送则是不可控的。所以,send()通常不会阻塞,只有在不能立即将数据发送给SendQ的时候才会阻塞,这往往是因为SendQ缓存已满。
另外,SendQ并不负责统计每次send()所发送来的字节流的长度,事实上这个长度在TCP中没有意义,因为所有数据都以字节为单位按照FIFO的形式排列在队列中,而并不在乎来自于哪一次的send()。
这也就是所谓的TCP无边缘保证,TCP的send()并不在乎每次传送的数据有多少,而只是致力于将数据以字节为单位按照FIFO的形式排列在SendQ队列中
可靠传输的工作原理:
说TCP是可靠的,那什么样的条件才算可靠的呢?
理想的可靠传输条件有两个特点:
1) 传输信道不产生差错。
2) 不管多快的发送速度,接收方总能来得及处理收到的数据。
我们不可能达到理想条件,但我们可以尽可能达到。
要达到第一点的方法是,当出现差错的时候,就让发送方重发出现差错的数据;
达到第二点的方法是,接收方来不及处理数据时,及时告诉发送方适当降低下发送速度。
第一点的方法怎么实现呢?先介绍简单的停止、等待协议。
停止、等待协议(做个了解,为后面做铺垫
“停止、等待”就是发送完一个分组(数据单元的统称,在IP称作数据表,在TCP层称作报文段)就停止发送,等待对方的确认。在收到确认后再发送下一个分组。
这种协议会遇到三种情况。
情况1.无差错情况
A发送分组M1,发完就暂停发送,等待B确认(ack)。B收到了M1就向A发送确认。以此类推。这种情况不会出现什么问题。
情况2.出现差错
如图上M1分组的情况--M1分组发送给B的时候,B检测M1出了差错,就丢弃了M1,然就就什么也不做了(不通知A收到有差错的分组);或者M1在传输过程中丢失了,这时B当然什么都不知道。在这两种情况下B是不会给A发送任何消息包括确认消息。
遇到这种情况,A采用的办法叫超时重传: A只要超过了一段时间仍然没有收到确认的话,就认为刚才发送的分组丢失了,因而重传上次发送过的分组。
所以每发完一个分组就为该分组设置一个超时计时器,如果在时间内收到了确认,就撤销该超时计时器。
这里面有三个要点:
1. A需要暂时保留已发送分组的副本。收到确认后清除副本。
2. 分组和分组的确认进行编号,这样才能知道哪些分组收到了确认。
3. 从上图中就可以重传时间要比平均往返更长一些。(具体长多少,要确定这个值比较麻烦,必须要根据当时的网络情况具体分析)
情况3. 确认丢失和确认迟到
这个字面上就很好理解,就是比如B收到了M1分组,但B发送的确认丢失了或者迟到了。 因此A在超时计时器到期后就要重传M1。这时B又收到之前那个M1了!B应该怎么做?(确认丢失)
1. 丢弃这个重复M1。
2. 然后给A发送M1的确认。这个确认必须发送,不能说之前发送过就不发送了。
这里面有个细节,就是当B再次收到M1时,它是知道这是重复的!
确认迟到--因为网络拥塞等情况,B发给A的M1确认可能迟到。B的确认迟到,就意味着A等待确认后超时!如果超时后,会再次发送M(B收到这个重复M1后,直接丢弃,如上面确认丢失的做法),如果A按时收到了B相应的确认,那那个迟到确认就重复收到了,A收到这个重复确认怎么办?很简单,也是收下后直接丢弃!
像上述停止、等待这类可靠的传输协议,我们通常称为自动重传请求ARQ(Automatic Repeat reQuest)
缺点:]
停止、等待协议有个特点,从无差错下面那个图可以看出来,是线性的!发送完一个数据必须等待确认收到后才发送第二个,可以看成像程序上的同步。 我们可以来算这样做的信道利用率。TD是A发送数据需要的时间,RTT是数据在信道中的往返时间,TA是B发送确认的时间(发送确认时间非常短)。因为只有TD那个时间才是发送的有用数据,所以利用率公式为:
U = TD/(TD+RTT+TA)
从这个公式就可以看出,当信道特别长(一定特别长,都跨国了),发送功率特别大(科技发展,发送速度必然越来越快)的时候,我们的信道利用率就特别低!也就是说这个信道多数时候是闲置的。
那我们让A连续发送过个分组,不必每发完一个分组就停下来等待确认呢?类似于我们web开发说的异步服务。
当然有,下面介绍类似这种异步的协议--连续ARQ协议,和再后面的大BOSS滑动窗口协议。
连续ARQ协议
其实该协议很简单,就是有一个发送窗口,一个窗口内的5(数字可以改)个分组一起发送出去。然后接收方采用累积确认的方式,即接收方不必对收到的分组逐个发送确认,而是收到几个分组后,对按序到达的最后一个分组发送确认。
如图:
(a)图先发送12345编好号的分组,在收到接收方发送的最后一个分组确认(即第5个分组的确认)后,(b)图向前移动5个分组,再一次性发送678910分组数据。
根据这个简单的介绍,连续ARQ协议在这种情况下会出现个大问题:
累积确认有优点也有缺点。优点是:容易实现,即使确认丢失也不必重传。但缺点是
不能向发送方反映出接收方已经正确收到的所有分组的信息。
例如,如果发送方发送了前5个分组,而中间的第3个分组丢失了。这时接收方只能对前两个分组发出确认。发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次。这就叫做Go-back-N(回退N),表示需要再退回来重传已发送过的Ⅳ个分组。可见当通信线路质量不好时,连续ARQ协议会带来负面的影响。
在深入讨论TCP的可靠传输问题之前,先了解TCP的报文段首部的格式。
上一大段巴拉巴拉,就是说累积确认有缺陷。在解决这个缺陷之前,我们先来看看TCP的首部格式!
TCP报文段首部格式
TCP传输的数据单元是报文段。一个报文段由TCP头部和报文段数据部分组成。TCP的全部功能都体现在它首部中各个字段的作用。头部图如下:
源端口号和目的端口号都好理解。
序号(seq number): 占4字节,范围是0~2的32次方-1(0~4294967295)。序号增加到4294967295后,下一个序号又回到0。在TCP传输的字节流中的中每一个字节都按顺序编号。首部中的序号字段值是指本报文段所发送的数据的第一个字节的序号。
例如一段报文的序号字段的值是301,携带的数据共100个字节,那下一个报文段的数据序号就是401。(这个序号非常重要,在后面重组报文的时候用)
序号一共就4294967296个,会用完吗?在一般的情况下,可保证当序号重复使用时,旧的序号数据早已经通过网络到达终点了。
确认号(ack):是期望收到对方下一个报文段的第一个数据字节的序号。
例如:B正确收到了A发送过来的一个报文段,其序号值是501,数据长度是200字节(序号501~700),这表明B期望收到A的下一个数据号是701,于是B在发送给A的确认报文段中把确认号设置为701。
头部长度(数据偏移): 头部长度的单位是32位(4字节),即如果这个字段的值位0110(6),那头部的长度就是24个字节,固定头部是20个字节,那选项部分就只有4个字节。 由于头部字段占4位,即最长的头部长度为4 * 16 = 60字节,即选项最多40字节。
保留:保留为今后使用,但目前应置为0。
接下来的6个控制位是说明报文段性质用的。
紧急URG:用于处理紧急报文段。也就是这个报文段优先级高,最先处理。要发紧急报文段就把这个位置为1。后面会介绍如何处理紧急报文段。
确认ACK:是经过确认过的报文段。比如建立连接后的所有传送的报文段都必须把ACK置为1。
推送PSH:推送操作很少使用,可以作为了解。(这个跟我业务上说的push不一样哈)
复位RST:当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。
同步SYN:在连接建立时用来同步序号。一般跟ACK配合使用。
当SYN=1而ACK=0时,表明这是一个连接请求报文段。若对方同意建立连接,则在响应的报文段中使SYN=1和ACK=1。
因此,SYN置为1就表示这是一个连接请求或连接接受报文。后面的文章会介绍TCP连接的建立和TCP连接的释放。
FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
窗口:是TCP流量控制的一个手段(还记得我们之前说实现可靠性的第二个条件吗?控制发送速度!就是通过窗口来控制发送速度的!)。
这的窗口是指发送本报文段己方的接收窗口(还有发送窗口,但这是指接收窗口),占2字节,所以值是的范围是0~2的16次方-1间的整数。
窗口值告诉对方(重点!是告诉对方):从本报文段的确认号算起,自己目前允许对方发送的数据量(还能容纳多少字节数据),这样对方就可以控制发送速度。有点绕哈,举例说明:
如果我发送的这个报文段的确认号是701,窗口值是1000,就表明从701号算起(序号必须是>=701),我还能接收1000个字节数据(接收缓存空间)。
窗口值就是对方设置发送窗口的依据。
校验和:由发送端填充,接收端对TCP报文段执行CRC算法,以检验TCP报文段在传输过程中是否有损坏。
这的校验不仅包括头部,也包括数据部分。是可靠传输的重要保障!
紧急指针:指出本报文段中的紧急数据的字节数。它指出了紧急数据的末尾在报文段中的位置(这不是很理解,没有关系,在后文会有介绍)!紧急指针仅在URG=1时才有意义。
头部选项:
典型的头部选项结构如下图:
选项的第一个kind说明选项的类型。第二个length指定该选项的总长度,该长度包括kind和length所占的两个字节。第三个info是选项的具体信息。
有的TCP选项只有kind字段。常见的TCP选项有7种:
kind=0:表示结束选项
kind=1:空选项,没有特殊含义
kind=2:最大报文段长度(MSS)选项。TCP模块通常将MSS设置为MTU(IP层的最大传输单元,最大值1500)-40(20TCP头部字节+20IP头部字节)。所以MSS最大值是1500-40=1460字节。MSS的默认值是536字节。
kind=3:窗口扩大选项。防止窗口值不够用。如果头部中窗口值是N,窗口扩大因子是M,则,窗口的最大值是2的(N+M)次方 - 1。
窗口扩大选项可以在双方初始建立TCP连接时进行协商。如果某一端实现了窗口扩大,当他不在需要扩大其窗口时,将该项值设置成0即可。
kind=4:选择性确认(SACK)选项。我们前面在停止、等待协议说到会出现重复发送分组或者重复发送确认的情况。SACK就是为改善这种情况发明的技术,它使TCP通信端只重新发送丢失的TCP报文段。
kind=5:SACK实际工作选项。该选项的参数告诉发送方端已经接收到并缓存的是不连续的数据块(有数据丢失),从而让发送端可以根据此检查并重新发送丢失的数据块。
选择确认我们后面还会介绍。
kind=8:时间戳选项。该选项主要有两个功能:
1. 用来计算往返时间RTT。发送方在发送报文段时把当前时钟的时间值放入时间戳值字段,接收方在确认该报文段时,把时间戳字段值复制到时间戳回送回答字段!
2. 处理TCP序号超过2的32次方的情况!称为防止序号绕回(PAWS)。序号只有32位,当使用高速网络,TCP连接的数据传送中很可能,序号很可能被重复使用!那就是加个时间戳不就区别开了!