通常所说的TCP/IP协议指的其实是“TCP/IP协议族”,是指包括TCP、IP等网络协议在内的众多网络协议的集合。TCP/IP协议也可以认为是对OSI模型的一种实现(但它只有四层)。从底层到上层的结构如下图所示。
图1-1 TCP/IP协议栈
链路层主要是负责物理设备的驱动。
网络层主要处理分组(可以简单的认为就是传输报文段)在网络中的活动。如分组的选路。
传输层主要为传输两端的应用程序提供“端对端”的通信。
应用层负责处理这些数据。
首先,世界上任何一台主机的物理地址(MAC地址)是不一样的。在链路层,通过这个物理地址就可以实现数据的传输。事实上,在局域网就是直接靠MAC地址进行数据传输的。
而成千上网的局域网组合在一起,就形成了互联网。局域网与局域网之间是通过一个或多个路由器连通在一起的。其拓扑结构可以简单的理解成下图(实际上肯定比这个复杂)。链路每个路由器都维护着一个理由表。这个路由表可以简单的认为拥有下属所有直接主机IP地址和MAC地址,外加一个默认的表目(可以多个)指向下一个路由设备。
图1-2 互联网拓扑结构图
1. 从bsdi发送数据到sun
在每台主机上其实同样存在着一个路由表。在windows上我们可以通过route print命令查看。bsdi的IP层收到上层的数据后,首先搜索本机的路由表。发现目的IP地址104.252.13.33和自己在同一网络(104.252.13.0)上。于是bsdi发出ARP广播,sun收到该广播后发现自己的IP与ARP数据包的查询IP地址完全匹配,则回发一个包含自身MAC地址的数据包。有了目的MAC地址后,bsdi就可以将数据包发送到sun了。实际上在bsdi存在ARP缓存(也就是bsdi已经知道对端的MAC地址)的情况下。IP地址对于数据的传输是多余的。局域网内就是靠MAC地址通信的。
2. 从bsdi发送数据到192.48.96.9
可以发现这个IP地址192.48.96.9其实并不在图1-2的任何一个局域网内。而是存在于外网Internet中。
同样,bsdi搜索本机路由表,发现自己的路由表中并不存在该IP地址的表目。于是将数据包发送到默认表目。默认表目指向路由器sun(sun是主机但也充当路由器角色)。因此在bsdi的链路层中会构造一个目的IP地址为192.48.96.9但MAC地址却是sun的MAC的物理层数据包。由于MAC地址才是传输的根本,因此IP报文被发送到了sun。
sun收到报文后,同样遍历自己的路由表,发现并不存在对应表目后,将IP数据包发送到其默认表目对应的路由器netb。这里所不同的是,sun与netb之间是通过SLIP连接的,从sun到netb的数据包并不要MAC地址的参与。因为他们之间的传输是靠SLIP协议进行的。从这里可以看出,一个数据包从源主机到目的主机,中间经历的传输协议是多样的。但目的IP地址始终保持不变。
netb收到IP数据包之后。同样遍历自己的路由表,仍旧发现不存在匹配表目,因此IP数据包被发送到默认表目指定的路由器gateway。以此类推。
假设某个节点假设路由表中存在着192.48.XX.XX的表目。该类路由器将发现IP数据包的目的IP地址192.48.96.9的网络号与该表目的网络号都是192.48。因此IP数据报被发往该表目指向的路由器。假设该路由器的IP地址为192.48.1.5(不过似乎一般子网的末尾应该是1),也就是下图的路由器3。
查阅资料可以发现IP地址可分为A、B、C、D、E五类。如下图所示。
图1-3 IP地址分分类
192.48.1.5明显是B类地址。B类地址网络号在上图中已经明确指明了。而IP路由时又说需要匹配网络号。这个网络号难道就是这个网络号么?这里经本人反复思考研究,《TCP/IP详解卷一》第三章关于这部分内容描述时,所说的网络号其实应该准确理解为“网络号+主机号中的子网号”。
路由器3搜索自己的路由表,发现并没有完全匹配的表目(网络号+主机号),却有一个网络号匹配的地址192.48.64.6路由器4(网络号匹配需要子网掩码的参与),于是将IP数据包发送给路由器4。这里的网号匹配过程如下(假设路由器3的子网掩码为ffff40)
路由器4同样通过匹配网络号将IP数据包发送给路由器5。
路由器5搜索自己的路由表发现有一个完全匹配的表目。于是将IP数据包发送到了IP地址为192.48.96.9的B主机。至此数据被正确传输。
其匹配路径如下图所示。
图1-4 IP数据包路由
这里需要说明几点。
1,请不要将这里的路由器与家庭路由器联系在一起。家庭路由器管理的是私有IP,而这里所说的IP都是公共IP,本人觉得这两类IP差别很大。
2,IP数据报不会一直存在于网络中,IP头部有一个八位长度的TTL(生存时间),其一般实现是每经过一个路由器减1,当它为零时路由器丢弃该IP数据包。
IP协议是整个TCP/IP的核心。
IP数据报的格式如下图所示
图2-1 IP数据报格式
其中TOS现在已被忽略,但它后4位必须置0。
16位总长度代表整个IP数据报的长度。
标识字段标识主机发送的每一份数据报。通常每发送一份数据就会加1。这实质上是为每一个数据IP报文打上了标记。在IP报文被分片后,路由器或主机可根据这个标志确定IP分片属于哪个IP报文。
由于IP报文传输的构成中有可能存在分片,因此3位标志位和片偏移作用于分片和组装。
TTL表明该数据报在网络上能存在的时间。
子网掩码和IP地址作“与”运算就得出了该IP所在的子网。如140.252.4.5,子网掩码为255.255.255.0。那么做“与”预算之后得出所在子网为140.152.4.0。在前面所说的路由器匹配路由表的网络号时就是通过这种方法进行的。
待续
TCP协议的头部如下图所示。
图4-1 IP数据报格式
32位序号,即ISN号,指明所发送数据的编号(指向该报文的第一个数据)。建立连接时所记录的ISN号实际上标识了该连接(该起始ISN+源目IP和源目端口,实际上唯一标识了任意一个连接)。以后发送的数据的编号都是从该起始ISN号开始。理论上这个起始ISN应该是随机的,且任意两次都不会重复,这和普通的random是不同的,因此TCP有自己的一套随机算法,一般可能是每隔0.5s加64000或每建立一个连接也加64000,大概9.5小时轮完一次。
这里其实存在两个问题。
1, 某一连接发送数据的ISN号与下一个连接的起始ISN号一样,这完全是有可能的。但加上IP头的源目IP和TCP头的源目端口,两个IP报文完全可以区分。(这个简单的问题困扰了本人好久)。TCP之所以需要随机起始ISN号,目的并不是要区分一个主机与不同主机的连接(事实上他们之间天然就被源目IP区分)。而是为了区分两个主机,且源目端口相同的先后两个连接。这样可以避免某个IP数据报被延迟发送,而造成对端对其做出错误的解释(《TCP\IP详解卷一》第176页)。
事实上对TCP的终止时的状态TIME_WAIT有一定了解的人仍然会感到奇怪:《UNIX网络编程卷1》第37页中明确指出TIME_WAIT有两个作用,其中一个作用就是“它允许老的重复的分节在网络中消逝”。和上面所说的起始ISN号(以下称为SYN号)的随机选择应该是相同作用。
这里进行分析:假设前一连接A的ISN号因为数据的的传输由0到了11,然后连接中断。紧接着立即重新连接(假设立马连接上了),连接为B(AB连接显然不会同时存在,同IP同端口不可能建立同时存在两个连接)。假设B的SYN号为10。A的某个11号报文被延迟发送,但A连接已经关闭(因为收到了另一个重发的11号报文而关闭),则它可能会被连接B接受收,这显然是误收了。这种情况的出现概率有多大呢?
假设AB建立的间隔时间为t,AB之间无任何连接建立,这里根据ISN0.5s加64000,建立连接加64000的算法,,则AB的SYN的存在以下关系(假设在同一9.5h周期内)
syn_a+64000*(t/0.5+1)=syn_b (t>0)
最极端的情况是A以某个速度(假设为v)不停发送数据,而B连接一直空闲,则B一直期待一个syn_b+1的IP报文。则要满足前面出现的情况需要
由于ISN号单位是字节,时间单位为妙,128000Byte/s算上每个字节的起止符,为128M/Bs。需要1280兆的网速!事实上由于IP和TCP首部的存在,128MB/s这个速率是不够的。因此这种情况几乎没有可能。
事情变得越来越复杂了,我们知道ISN号一定时间轮回一次,那么在下一次轮回有可能在网络中出现两个相同ISN的数据包么?假设A连接建立在SYN号为isn0上,9.5h完全不发送数据,在最后快到9.5h时发送数据并终止连接,假设其中某个包序号为isn0+64000+1,且被延迟。紧接着B连接建立SYN号恰好为isn0+64000,它立马又发送了一个数据,那么其首个数据字节ISN号为isn0+64000+1。这就有可能让对端造成误读。这种情况要求TCP连接的持续时间大于9.5小时,而且是小概率事件。但小概率事件也有可能发生。
那么万无一失的办法应该是想办法避免两个同ISN号的数据包同时存在于网络中。
我们知道在IP首部有一个字段TTL(生存时间)。也就是说一个报文在发出TTL时间后肯定已经被链路丢弃。然而,理论上应该为时间的TTL参数,难以用“时间”实现,其一般的实现是TTL在每经过一个路由器时减1,到0时被丢弃。因此只能对报文的生存时间进行估计,MSL(报文生存的最长时间)应运而生,该参数存在于主机的系统中并且只是真实MSL的一个估计值。由于路由器转发IP报文所需的时间和报文在链路上的时间是无法准确估计的,因此MSL无法准确估计。如果将TCP主动终止连接端在连接终止时,设计为持续2*MSL时间的TIME_WAIT状态,那么可以作如下推算:ISN号每隔9.5个小时才可能重复,只要实际MSL小于9.5h,B连接在A连接终止2*MSL(2MSL<9.5)后才能建立,此时,A的所有IP数据包肯定已经消逝了。假设发送端a接收端b,则2 *MSL的时间足以确保最终a端接收b端fin信号后再发出ack信号。
仔细思考,有了TIME_WAIT后,随机SYN号似乎是多余的。因为从上面已经发现ISN号重复难以完全避免。然而,MSL一般设计为2min或30s等,前面的分析中已经明确指出MSL是估计出来的肯定是不准确的,极端情况下某一路由器将某个数据包延迟很长时间(超过估计值MSL)完全是可能的,这将导致误读,当然这同样也是小概率事件。
那么ISN随机+TIME_WAIT都失败的情况就是小概率事件中的小概率事件。事实上这个概率是非常小非常小的。
本人以前一直困扰于这个小概率事件,以为两种方案均万无一失,以致始终无法理解。当然以上均为本人根据现有资料推测出来的,如有偏差,万请告知,感激不尽。
2, 2当32位序号全位1后,下一个序号需要重新开始,因此需要对这样的回绕做处理。
32位确认序号,指明期望收到对端的数据序号,这也潜含了在这个序号之前的数据已经接受完毕的信息,因此才叫ACK信号。
TCP的连接和数据传输以及挥手的一般情况如下图所示。
图4-2 TCP连接、数据传输和挥手的过程
从图中右边的分析中可以发现,建立TCP连接需要3次握手(两边同时发起TCP连接的极端情况会有四次,当然有些系统实现并不能正确处理同时发起连接)。对TCP包的确认机制和流量控制是和TCP滑动窗口相关联的。将在下面分析。
下图为TCP滑动窗口示意图。
图4-3 TCP滑动窗口
假设a与b建立连接,并且只有a向b发送数据,b端接收数据并丢弃。假设a的syn号为0,则数据的第一个字节的ISN号为1,ACK标志位在建立连接后会一直保持1,a→b的IP报文ack序号为a端期望收到b端数据的编号,由于b端一直不发送数据,则它一直会是b端的syn号+1。
由于是数据传输,PSH标志置去1,这个标志实际上触发了b端的read返回(但目前的实现其实忽略了这个标志位,b端会智能决定read的返回时机)。
现在,a开始向b发送数据包。假设第一个报文包含3个字节数据,则b端收到后会发送一个ack序号为3+1=4的报文(这时如果b有数据发送给a,这个ack报文是可以携带数据的),以表明期望收到下一个数据报的起始序号为4,a端收到这个ack后就知道对端已经收到了所有序号小于4所有数据,滑动窗口左边缘向右移动到序号4。发送ISN号为4的数据报。
另一种可能是a接连发送多个数据报给b,如一个ISN:1(3)—ISN号为1长度为3—的数据报data1,和另一个ISN:4(3)数据报data2。假设b先接到的是data1,而data2还没到达。B会延时一段时间后再发送ack报文:如果这段时间内data2没有到达,则回发ack:4;如果data2到达,直接回发ack:7,表明7前面报文全部收到,滑动窗口左边缘移动到7。这里一个问题是当b回发ack:4时,a会不会触发ISN:4(3)的重新发送?答案是并不会,a在设定的时间没收到对应报文的ack才会重发。
上面两种情况都会形成如图4-3所示的TCP滑动窗口图(第二种情况的第一种小情况)。此时ISN号为456的数据发出去了但未被确认。并且由于滑动窗口向右移动,ISN为789的数据被囊括在了滑动窗口内,a可以在不收到456确认的情况下发送789。这样,发送的效率显然被提高了。
一般来说在网速无限快的情况下滑动窗口越大,传输效率越高(这样可以连续发送多组报文,1个ack也可以确认多个报文)。但是窗口过大,也就意味着同一时刻,在网络上传输的数据量增多,网络上的路由器因此变得拥挤,当路由器负载饱和后,后面的数据包将会被丢弃。(需要注意的是以上所说的滑动窗口的重传机制只是众多实现中的一种,千万不要与其他重传机制混淆)。
这个其实是由RTO(TCP超时重传机制)控制的。重传的时间间隔是一个自适应的值,以谋取最优,因此并不固定!某些具体的TCP实现会直接采用一些典型的时间间隔值(这样方便啊),但显然这不会是最优值。
SYN的重传
就是发起SYN连接的报文丢失引起的重传。事实上这里的重传的说法是不严谨的,因为在每次重传的SYN号是变化的,这个和数据的重传不一样。
《TCP/IP详解》中所用主机的第一次SYN重传发生在6s后,事实上由于实作计算机时钟滴答500ms一次,而第一次起始计算时间可能发生在某个滴答的任意时刻(如下图),导致实际重传的时间应该在5.5~6s之间。第二次重传的时间发生在第一次重传的24s后,接着是48。一般75s后就不再尝试连接了。
图4-4 时钟滴答
一般这里的重传时间需要实时测量RTT(也就是给定连接的往返时间),然后根据RTT算RTO,是一个自适应的值。一般重传时间策略叫做“指数退避”策略。但SYN的重传由于是第一个报文,无法得知RTT。因此SYN的重传一般是由操作系统设定的,典型的如Windows在3、6、12、24、48…后重传,而上面分析的主机是在6、12、24、48…后重传。
而对于数据包的重传则是根据上次测量到的RTT时间计算出来的。典型的时间是1.5、3、6、12、24、48、64后面一直是64s。
重传会持续一段时间,具体时间与实现有关,比如9min、2min。失败后会发送复位报文,这是终止TCP的非常规方法。
事实上TCP最少要维护9个定时器!前面以及介绍了重传定时器和TIME_WAIT的2*MSL定时器,下面将分析另外两种常见的定时器。
1, 坚持定时器
在前面TCP数据的传输过程中可以知道,在连接的过程中,可用滑动窗口MSS的大小是相互通告的。坚持定时器将处理对端MSS为0的情况。
假设a瞬间发送多个报文ISN:1(2)、ISN:3(2)、ISN:5(2)到b,b的滑动窗口大小为6,则b在收到所有报文后将回发ACK:7 MSS:0的报文。a端接收到该ACK后,发现对端b窗口无法再容纳数据则不会发送报文。一种情况如图序号为9的报文,b端在应用层读取了滑动窗口的一些数据后再次发送一个ACK:7 MSS(XXX)的报文通知a,a因此被激活。但如果这个报文丢失,根据TCP的重传机制,TCP不会对纯ACK进行确认,该ACK报文是无法被重传的。这时候a错误的仍然睡眠并且将再也不可能被唤醒,造成死锁。
一个办法就是在发送端a发送探测MSS的报文。多久发出一个探测呢?和重传一样的“指数退避”!a发送一个数据序号为8的探测报文,但b端由于MSS=0,只能回发一个ACK:7 MSS:0,如果此时b端MSS>0,则回发ACK:8 MSS(XXX)。死锁因此被避免了。事实上如果b的MSS>0但仍还很小,为了避免发送一个包含太少数据的IP包,b会回发一个ACK:8 MSS(0)!
可以想到的是,这个坚持定时器在没有收到对端的非0窗口通告的情况下,会一直持续运作。
图4-5 MSS为0的TCP交互
2, 保活定时器
说白了这个就是为了在一个TCP连接长时间控制后,某端探测连接是否中断的定时器。比如,某个服务器的某个客户端因死机而终止,由于死机时客户端不会发送fin信号,服务器无法得知客户端已经死掉而错误地继续等待这个客户端的服务请求。而造成资源浪费。太多这样的死连接会导致服务器性能低下甚至崩溃。因此有了保活定制器,这个一般2小时发送一次探测报文。《TCP/IP详解》明确指出尽量避免使用这个保活机制。如果需要保活连接,可以自己在应用层实现,效率高而且可定制。