2.1.1 TCP的头部格式
在1.4节中,我们按照TCP/IP 五层模型对数据从应用程序发送的消息转化为比特流的过程进行了解释。从本章开始,读者就要开始深入了解“封装”的操作方法。为此,我们需要先“引见”几个基本的术语。
既然称为“封装”,那么这个数据在经历各层的处理时,会由相应的协议在这个数据“周围”添加一些“补充说明”,以便接收方在解封装时看到这些补充说明,从而执行一些相应的功能。当然,设备给数据添加“补充信息”时并不能任性,它必须参考相应的协议标准,而这些协议自然也都定义了添加补充信息的格式。虽说是在“周围”添加数据,但数据毕竟只有长度这一个维度,因此所谓的“周围”也只包括前、后两个位置。如果补充信息添加在了源数据的前面,这些信息就称为“头部”,如果添加在源数据的后面,则称为“尾部”。而各个协议所规定的补充信息添加格式,也就称为这些协议的“头部格式”和“尾部格式”。
注释:
如果我们的讨论只局限在路由交换技术领域,而不涉及安全加密领域的话,那么给数据包添加头部信息要比给数据包添加尾部信息的做法常见得多,后者多用于对数据包进行校验。由于校验的功能往往由数据链路层提供,因此封装尾部的做法多发生于数据链路层。“帧”(frame)这个词在英文中的原意是“框子”,之所以用它代指经过数据链路层封装后的信息,就是因为这一层在对数据进行封装时会在头尾两侧都添加信息,因此数据链路层输出给下层的信息就变成了一个“夹心儿数据框”。在此我们顺便建议读者,如有可能,请尽量多阅读英文文档,尤其是RFC文档。在大多数情况下,准确地把握住了一个词,就能串出一套完整的概念体系,如frame→框子→头尾都添加数据→校验→数据链路层的功能→分层体系。
说了这么多,唯一的目的就是引出TCP的头部格式,如图2-2所示。
源端口号和目的端口号
“端口号”这个概念在本书中第一次出现。这个概念极为重要,值得进行一番介绍。
一次通信的过程,既不以一台设备将比特流发送出去作为开端,也不以一台设备将比特流接收进来作为终结。对于每个参与通信的人来说,计算机接收到的这些比特流毫无意义,人们要的是自己看到、听到之后,转化到人脑里成为脑电波。所以,如果你想知道:网络中有那么多台计算机,传输设备是怎么从万千计算机中找到对方的计算机的呢?那么,你就不妨也思考一个极为类似的问题:应用层有那么多的进程,传输层协议是怎么从万千进程中把这个数据进程找到的呢?
图2-2 TCP的头部格式[1]
[1] 在定义了TCP的RFC 793中定义了6个控制位(URG、ACK、PSH、RST、SYN和FIN),保留位变为3个,这3个控制位包括CEC和CWR(RFC 3168)以及NS(RFC 3540)。这部分内容超出了CCNA的教学大纲,对此感兴趣的读者可以参考相关的RFC文档。
这个问题很快可以转换为,在整个通信的过程中,谁知道应该把这个数据交付给哪个进程?答案是:谁生成了这个数据,谁就知道应该把它交付到哪里。因此,当传输层协议封装数据时,它就有义务通过某种方法告诉对端设备的传输层协议,这个数据应该最终交给上层的哪个服务去处理。当然,传输层协议没法直接把“HTTP”四个大写英文字母封装在数据的头部,而只能封装代表相应服务的编码,这个编码就是端口号。端口号的使用也是有规矩的,其中0~1023的端口号被分配给了一些固定的协议,如FTP(TCP 20、21)、SMTP(TCP 25)等,这些端口号称为知名端口号,是由IANA分配的;1024~49150的端口号可由应用程序动态选用,IANA对这些端口的使用情况进行了登记;49151~65535是临时端口。
除了OSI模型在传输层和应用层之间定义了两个功能冗余的会话层和表示层,在现实世界(和TCP/IP五层模型)中,传输层之上只有一层,那就是应用层。因此,端口号就是传输层赋予应用层进程的编号。这句话可以延伸为,所有有端口号的协议全都工作在应用层。
注释:
如果你拥有一点路由技术方面的基础,读到这里时有可能会产生一个关于动态路由协议的疑问:难道拥有端口号的路由协议也是应用层协议吗?事先预告一下,我把自己对于这个问题的理解写在了第10章中(是的,不是第9章)。如果你压抑不住冲动,可以去找找看。对于零基础的读者,不建议提前思考这个问题。
把上面的内容总结起来就是:当传输层协议处理信息时,它会通过源端口号字段说明这个信息是由哪个进程生成的,同时通过目的端口号字段说明这个信息需要由接收方的哪个进程进行处理。
序列号和确认号
在数据从发送方主机的传输层到达接收方主机传输层的这个过程中,有可能会出现信息乱序的情况,甚至也很有可能丢失信息。而我们在前面刚刚说过,TCP是面向连接的协议,因此它得对这些情况负责。序列号和确认号的作用正在于此。
TCP为了保证自己发送的每一个字节都可以被对方收到,并且都是按顺序收到的,就必须对每一个字节都进行编码。当然,为了保证传输的效率,TCP倒还不至于对每个字节都进行标识,否则网络上传输的编码就比实际数据还多,岂不是本末倒置?事实是,TCP只会通过序列号来表示这个数据段第一个字节的编码,后面每一个字节的编码也就不言自明了。比如,一个数据段的序列号字段是1117,那么,如果这个数据段一共携带了810字节的数据,它的最后一个字节的编码就是1926,下一个数据段的序号应该从1927开始。
而确认号是接收方设备告诉发送方设备,接下来,它希望收到以哪个序列号开头的数据。如果一个数据段的序列号字段是1117,而这个数据段一共携带了810字节的数据。那么,如果接收方正确地接收到了这个数据段,它就会要求发送方给自己提供1927开头的数据。换句话说,在正常情况下,此时接收方发送给发送方的TCP数据包,确认号就应该是1927。
头部长度
如图2-2所示,TCP头部当中可以根据需要添加一些可选项字段。所以,TCP头部的长度是不固定的。既然TCP头部的长度不确定,在TCP头部中,就需要有一个头部长度字段用来说明TCP头部的长度是多长,从哪里开始是TCP封装的数据部分。
URG与紧急指针
URG叫作“紧急位”,当这个位的数值为1时,后面16位的紧急指针就会生效。如果一个数据段的URG位为1,TCP在处理时,就会将这个数据段中的紧急部分插入数据段的最前面,而紧急指针字段则会指明这个数据段中紧急数据部分的长度。由于紧急部分位于数据段最前面,因此知道了紧急部分的长度,就知道了正常数据的起始位置。当TCP优先处理完紧急数据时,就会以正常操作的形式再去处理后续的数据。
ACK
ACK叫作“确认位”,当这个位的数值为 1 时,32 位的确认号才会生效。由于TCP提供的是可靠传输,而确认位在保障可靠传输的体系中厥功至伟,因此几乎所有TCP封装的头部都会将ACK位置于1,例外情况详见后面的TCP连接的建立过程,这里暂时卖个关子。
PSH
当PSH位的数值为1时,接收方不会将这个数据段放在TCP缓存中等待其他数据,而会立刻将数据包提交给用户进程。
RST
当RST位的数值为1时,相当于TCP要求对端不经“四次握手”的过程,而是立刻断开TCP连接。关于TCP连接的断开过程,我们也会在后面单独介绍。
SYN
当SYN位的数值为1时,表示这台设备希望与对端建立TCP连接。
FIN
当FIN位的数值为1时,表示这台设备希望与对端断开TCP连接。
校验和
校验和的作用是检验数据信息在传输的过程中是否出现过变化。
窗口大小
窗口大小这个概念是所有同类图书作者的噩梦,不动用两页纸左右的篇幅、一整套抽象图形和大量纯理论术语,想把这个概念说清楚几乎就是天方夜谭。YESLAB敢为天下先,尝试用一种不算那么理论的解释方法,来简单地介绍一下窗口这个概念的大致含义。
作者小时候,经常和父亲一起彻夜玩儿红白机双人《魂斗罗》游戏。玩游戏的两个人是合作关系而不是竞争关系,但是遇到天上不时飞过的霰弹枪,两个玩游戏的人也经常会因为分配不均而起争执。可总的来说,玩家还是会尽量与对方合作,以避免万一一方真的恼羞成怒,后果也很麻烦,因为任何参与游戏的人都有一种很恶劣的耍赖方式,可以让另一位玩家也没法继续进行游戏,那就是——赖着不走,见图2-3。
图2-3 魂斗罗场景
注意,在图2-3中,如果左边的小人儿就这么站在那儿不走,右边的小人儿就算再英勇也没法单骑闯关,因为《魂斗罗》的窗口会被左边的小人儿拖住。换句话说,在《魂斗罗》游戏中,游戏的进展取决于落在最后的那个人的进展,这可以称之为“魂斗罗版的短板效应”。
回到窗口大小的话题。前面我们通过序列号和确认号说明了一个概念,那就是TCP传输是讲究顺序的,数据是不能丢的,丢了是要重传的。因此所有数据都有自己的编号,所有编了号的数据都要到位。这样一来,当两台设备相互之间建立了TCP连接,一方发送数据,另一方接收数据的时候,收发的双方就可以了解对方发/收数据的进展了。为了实现可靠传输,TCP定义了一个滑动窗口的概念(见图2-4),以便发送方只发送接收方能够接收的信息。
图2-4 滑动窗口示意
A是发送方的设备,我们可以看到它发送了序列号为300~309的数据,其中300~303已经收到了确认,304~309没有得到确认。310~316已经可以发送但是还没有发送,而317之后的数据还不能发送。如果此时A从B那里接收到了对304数据的确认,滑动窗口就可以相应地向前滑动到317,这就是说,A也可以对317数据进行发送了。对于A来说,已发送但还没有收到确认的那些数据就像是拖在最后的那个小人儿,它拖住了窗口向前移动,而TCP窗口的最前端也就相当于魂斗罗窗口的最前端,那是在最左边的小人儿移动之前,TCP这个游戏最多可以继续发展的情节。
B是接收方的设备,它对自己接收到的300~303的数据向A进行了确认,同时它也接收到了306~308的数据,但它没有对这些数据进行确认,因为304和305没有收到。滑动窗口允许B接收序列号316及以前的数据,但304和305也像《魂斗罗》窗口中左边的小人儿一样拖住了这个窗口。好在316之后的数据,A倒是暂时也还不能发送。
除了端口号,我们对于其他字段的解释基本只能称为简介。在CCNA阶段,读者不需要对这些头部字段进行过于深入的了解。之所以介绍滑动窗口机制以及其他头部字段,都是为了帮助读者了解TCP是怎么为数据提供一种可靠的传输机制的。同时希望读者能够理解,在通过TCP传输数据的过程中,通信双方之间会交互一些负责保障数据有序、完整、未经修改的信息。
在前文中,我们尽量本着有话则长、无话则短的原则,对TCP定义的数据头部各个字段进行了简要的说明。常有些爱看剧的人,连续三集没意思就会弃剧。这种做法也常见于读书,只不过单位从“集”换成了“段”。无奈的是,连我自己也知道,TCP数据包的字段分析有可能成为(继OSI模型各层介绍之后)本书被弃的第二个高峰,如果你觉得无聊但并没有直接弃书而跳读到了这一段,我向你表示由衷的感谢,作为对你的回报,我在这里提供给你以下三点信息。
● 关于数据包头部字段的介绍确实乏味,甚至并不是CCNA级别所要求的范畴,但这些数据包头部是协议赖以实现其功能的“定义级”信息,而本章即将介绍的这四个协议(尤其是头三个协议)又是当今互联网的“定义级”协议,重要性不言自明。
● 如果你实在不喜欢阅读理论性过强的内容,或者认为这些信息过于抽象,可以把书中这些枯燥的信息当成工具类信息使用,不通篇逐字阅读,仅简略浏览,在后面阅读的过程中遇到问题时再来有目的地重读。
● 马上要介绍的内容是TCP连接建立与断开的过程,我遗憾地通知你,设计这个过程的人也没有在他们的设计理念中融入太多的幽默元素。