前言
随着之前所讲到的可靠数据传输,则我们会由可靠数据传输想到TCP协议,本节就来讲述TCP协议的具体内容。
提示:以下是本篇文章正文内容,下面案例可供参考
一、TCP连接
1.面向连接
我们很清楚的知道TCP是面向连接的,那为什么称TCP是面向连接的呢?原因就是TCP在客户端与服务端进行通信的时候会进行三次握手,握手之后就建立一条通信的连接,可用于双方数据的传输,在握手的双方都必须互相发送握手的预备报文段,TCP的连接不是在电路交换中的连接,它不是物理上的连接,而是逻辑上的连接,在我们看来他们之间像是连接上了一条通信的线路,但是在路由器中看来,这只是一些数据包而已。
而TCP连接是怎么建立的呢,发起连接的进程称为客户进程,接收连接的进程称为服务器进程,该客户的客户进程首先通知运输层,想要与服务器上的一个进程建立一条连接,实现这个的代码在之前的博客中已经有实验代码编辑过,是通过套接字接口完成。
clientSocket.connect((serverName,serverPort))
客户与服务器端互相发送三个报文,前两个报文没有有效载荷,简单来说就是没有应用层数据,而第三个报文段,承载有效载荷,这种称之为三次握手。
在建立起第三次连接之后,两方就可以互相发送报文数据了,客户通过套接字接口发送数据流,数据通过这个门后一切都是由TCP建立的连接进行传输,TCP将这些数据引导至发送端数据缓存,这个发送缓存是由三次握手建立起来的,接下来,TCP就会从发送缓存中时不时的随机取出一块数据传输至网络层,当TCP接收端收到数据后,就会将数据放入该TCP连接的接收端数据缓存中。
2.TCP报文段结构
TCP报文段由首部字段和一个数据字段组成。数据字段包含一块应用数据。下图所示:
与UDP一致,TCP报文段包含首部,首部包含源端口号和目的端口号,它被用于多路复用或多路分解。同时TCP也拥有检验和字段。
其中还包括:
- 32位比特的序号字段、32位比特的确认号字段(这里后面会重点讲述),这些字段用来实现可靠数据传输服务。
- 16比特位的接收窗口字段,该字段用于流量控制,用于指示接收方愿意接受的字节数量。
- 4比特的首部长度字段,该字段只是了已32比特的字为单位的TCP首部长度。注意这里TCP首部长度是可变的。
- 可选与变长的选项字段,该字段用于发送防御接收方的协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。
- 6比特的标志字段。ACK比特用于只是确认字段中时有效的,也就是成功接收报文的回应。RST、SYN、FIN是用来连接、建立TCP与拆除TCP连接的。CWR与ECE比特是明确拥塞通告中使用的。PSH在被置位时,就指示接收方应立即交付给上层。URG比特用来指示报文段里存在着被发送端的上层个体置为“紧急”的数据。紧急数据的最后一个字节由16比特的紧急数据指针字段指出。
如果看到这里可能大家在第5点有不懂的标志,我看到这里的时候也是有很多疑问,但是没关系,我们一个个回往下讲述,大家只用知道这里有这个标志位就可以。
3.序号和确认序号
在上面一节讲到的TCP报文段结构中,提到过序号和确认序号,这两个首部字段是TCP中两个最重要的两个字段。也是TCP提供可靠数据传输的关键。
首先我们要知道,TCP把数据看成一个无结构的、有序的字节流,序号是建立在传输的字节流之上的。一个报文段的序号是该报文段的首字节的字节流编号。
假设要传输一个文件是500 000字节的,那么在mss(最大报文段数据)为1000的条件下,数据流首字节的编号是0,文件被划分成如下:
则第一个报文段的编号是0,而第二个报文段是1000,以此类推。
知道了序号的概念后,现在来看确认序号,确认序号比序号难理解一点,为了使得TCP更加人性化一点,考虑到多种因素,比如当你在发送给B的时候,可能也在接收B给你发送的数据,而这个确认序号是你期望从B那收到的下一个字节的序号。
举个简单的例子便于理解:假如你收到了0-567的字节数据,你期望下一个数据是567之后的数据,则你返回给B的TCP首部字节中的确认序号写的是568。
在这个例子中你可能会展开想,如果B给我的是0-567,888-1024的字节的数据,那么中间的568-887的数据怎么办,你确认序号该写什么,在这里我们要知道一个概念,TCP中的累计确认,如果你收到了0-567,888-1024的数据,那么你返回的数据报文中一定是568这个确认序号。你可以理解为,你必须要求这个字节流是连贯的序号。
更加方便理解的一个例子是这样的:
这个例子是简单的Telnet的TCP协议流程,在这里我们可以看到,用户键入‘C’,这里的序号是Seq=C的Ascii码,这个不是最重要的,我们理解最重要的是,A的确认序号ACK是79,B收到了A发送的42后,发现了A想要的下一个序号是79,则返回的序号中是A想要的79,而B确认了A收到的报文则B想要确认的序号是42后一位43,接着这样往下走,这样互相确认下去。
4.往返时间与估计超时
既然说3.4节为了TCP这一节打下了基础,那么是怎么打下了基础呢,在这一点上我们讨论往返时间与估计超时,在看到这里时候,请你想一个问题,如果使用超时重传机制,那么客户端怎么估计这个等待回复报文时间是否超时了呢,显然超时的时间间隔其实必须大于连接的往返时间,但是在不同场景和实际情况下,每一个用的的传输往返时间都是不相同的,所以这里实际情况下实现超时的时间间隔大于连接的往返时间是一个复杂的问题。
我们现在来看看,其实报文段的样本RTT表示为SampleRTT激素hi从某报文段被发出(即交给IP)到对该报文段的确认被首刀之间的时间量,大部分的TCP的实现仅在某个时刻做一次SampleRTT的测量,而不是每一个发送出去的报文段都测量一个SampleRTT,此外着重需要注意的一点是,TCP绝对不为已被重传的报文段计算SAmpleRTT,在实际情况下,路由器的负载变化,或者拥塞程度的变化都会随之波动。因此为了估计一个典型的RTT值则对SampleRTT取平均值,这个称之为EstimatedRTT,公式如下:
新的EstimatedRTT的值是由一定分量的以前的EstimatedRTT值与现阶段的SampleRTT的值产生的,在[RFC 6298]中给出的这个α推荐值是0.125.从公式上我们也可以看出,这个对RTT的平均值的加权加到最后是对最近的SampleRTT的影响占比比较大,这个原因也是可知的,因为网络的负载拥塞当然是依据最新的传输速率改变的,越近的样本越能体现出网络当前的拥塞情况。
除了估算这个RTT以外,测量RTT的变化也是有价值的,以下公式定义了RTT变化:
注意到DevRTT是一个SampleRTT与EstimatedRTT之间插值的EWMA,如果SampleRTT波动较小,则DevRTT的值就小,繁殖很大,这个β的值推荐为0.25.
到这里我们已经介绍完毕了SampleRTT、EstimatedRTT、DevRTT的值,我们知道了这三个值之后,那么这个超时重传的超时时间间隔到底是什么呢?,显然超时的时间间隔一定要大于EstimatedRTT,否则会造成不必要的重传,而超时的时间间隔也不应该大于太多,否则在报文段丢失后,重传的报文发送太晚,这会造成不必要的时延,因此这里的时间间隔应该设置为EstimatedRTT的值加上一定的余量,当DevRTT波动较大时,这个加的余量就应该大一些,如果波动较小,则加的余量应该少一些。
推荐的初始值TimeoutInterval为1s,同时出现超时后,这个值就翻倍,然而只要收到报文段并更新EstimatedRTT,就用上述公式重新计算TimeoutInterval的值。
二、可靠数据传输
Ip的服务是对携带报文是不提供可靠数据传输的,不保证数据的交付,也不保证数据的按序交付,也不保证数据的完整性。
TCP在此基础上建立了一个可靠数据传输服务,我们通过以下几个图来展示,并体验一下TCP所提供的可靠传输服务:
第一种情况如上图所示,A发送序号为92字节数为8的数据报文段,确认序号理所应当是100,B接收到了A所发送的92号8字节数据报文段,并回复一个100序号的ACK确认报文,但是这个报文在传输过程中丢失,丢失后,A等待接收B发送的ACK超时,故重新发送这个92号8字节的数据,这个时候B判断这个92号数据其实已经被接收,所以丢弃这个传送过来的92号字节数据,并再次回复一个100的ACK报文。
第二种情况如上图所示,A所发出的两个包被B正确的接收,B返回接收报文ACK,但是再返回过程中两个ACK包还没传到,对于92号的时间间隔超出了,重新开始计时的时候,只要B发送的两个数据包能在下一时间间隔到达则不用重传第二个包,如果看到这里会有疑问,为什么不传ACK100而传ACK120呢,我们往下看。
第三种情况如上图,假设主机A与在第二种情况中完全一样,发送两个报文段。第一个报文段在网络中丢失,但在超时之前收到了第二个报文段,所以主机A知道了第二个报文段120字节之前的所有字节已经被正确接收,所以主机不会重传这两个报文。
三、流量控制
先讲流量控制是为什么?之前我们提到过,一条TCP连接的每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、按序的字节后,它就将数据放入接收缓存。相关数据会被相关的应用程序进程从中读取,但不必是数据杠一到达就立即读取,有可能相关进程正在忙于其他事物。如果某应用程序读取数据时相对缓慢,而发送方发送太多、太快,发送的数据就会很容易的使连接的接收缓存溢出。
所以TCP提供了流量控制服务,消除发送方缓存溢出的可能性。TCP让发送方维护一个称为接收窗口的变量来提供流量控制。通俗的说,接收窗口用于给发送方一个指示——该接收方还有多少可用的缓存空间。我们定义这个RcvBuffer来表示其大小。主机B上的应用程序不断从该缓存中读取数据。我们定义如下变量:
LastByteRead:主机B上的应用进程从缓存读出的最后一个字节编号。
LastByteRcvd:从网络中到达的并且已放入主机B接收缓存中的数据流的最后一个字节编号。
由于TCP不允许已分配的缓存溢出,下式必须成立:
接收窗口用rwnd来表示,根据缓存可用空间的数量来设置:
由于空间是随着时间变化的,所以rwnd是动态的。下图对变量rwnd进行图示:
首先主机B通过把当前的rwnd值放入它发给主机A的报文段中,通知了主机A他在该连接的缓存中还有多少可用的空间。开始时,主机B设定rwnd=RcvBuffer。
主机A轮流跟踪两个变量,LastByteSent和LastByteAcked,注意到这两个之间的差值就是主机A发送到连接中但未被确认的数据量。通过将未确认的数据量控制在接收窗口rwnd这个值中,就可以保证主机A不会使主机B的接收缓存溢出。
那么现在还有一个问题,如果B接收的rwnd满了rwnd=0,则A停止发送,但进程是从缓存中读取数据出来的,但A怎么知道现在终于有空间了呢,这里定义如果B的rwnd为0,那么A会继续发送只有1字节的数据报文段,这个报文段会被接受方确认。这时候A就会计算出现在B还有多少rwnd值。
四、TCP连接管理
这里我们会更仔细的讲述TCP是如何建立-拆除一条TCP连接的。
第一步:客户端的TCP向服务器端的TCP发送给一个特殊的TCP报文段,里面不包含任何的应用数据,在报文段首部SYN标志位被置为1,这个报文段被称为SYN报文段。
第二步:一旦包含TCPSYN的报文段IP数据到达服务器主机,服务器从报文段中提取出TCP SYN报文段,为该连接分配缓存和变量,并返回一个允许链接的报文段,这个报文段也不包含应用层数据,这个报文段被称为SYNACK报文段。
第三步:在收到SYNACK报文段后,客户也要给该链接分配缓存和变量。客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认,两方将SYN比特置为0.
当然参与连接的两方都可以结束了这次连接,当连接结束后主机中的“资源”(缓存中的变量)将被释放。客户进程发出一个关闭连接的指令
则客户TCP发送一个报文给服务器,这里的报文段首部的FIN被置为1.当服务器接收到该报文段后,就向发送方会送一个确认报文段。然后服务器发送他自己的终止报文段,其FIN比特被指为1.最后该客户对这个服务器的终止报文段进行确认。此时两台主机上的链接都被释放。
PS:这里其实我比较纳闷,为啥服务器返回确认之后再将自己的FIN位置为1,为啥不能置为1顺带确认返回这个ACK报文呢,这不是少了一个通信吗。
但书中下列的演示其实我觉得也蛮正常的,好像确实是这么做的(按流程来,但我还是不服。。。)
以上是客户端TCP经历的状态,下图是服务器端的状态,将两个对比起来看,我们就可以看懂啦。
总结
码字又码累了,这章其实能学到的东西好多啊,TCP包含的知识点实在是多,但是其实多看看图,结合知识点,多理解理解其实很好懂,书中最后还给出了SYN洪泛攻击,这个其实就是完成了前两次握手不完成第三次握手,但这个不是重点,重点是了解TCP,但这节结束了其实TCP的学习也没有完全结束,还有之后要学习的拥塞协议什么的,那个咱们之后再说哦,加油。