传输层位于 OSI 七层模型的第4层,也位于 TCP/IP 五层模型的第4层,如图5-1所示:
图5-1 传输层
传输层包括两大基本协议:TCP(Transmission Control Protocol 传输控制协议)、UDP(User Datagram Protocol,用户数据报协议)。
无论是 TCP 还是 UDP,它所“传输”的都是应用层数据。这句话既是“废话”,也让人疑窦丛生,如图5-2所示:
图5-2 数据传输组网图
图5-2中,A 发送了一份数据到 B,中间经过路由器。这些路由器运行着网络层协议,将数据从 A 运输到 B。如果路由器能够说话,它们应该会说:我都累成孙子了,你(TCP/UDP)说你是传输层协议,那我到底是干什么的?
都是误会
都是误会!
那么传输层协议到底是干什么的呢?我们先看看传输层与网络层之间的关系,如图5-3所示:
图5-3 传输层与网络层之间的关系
从图5-3可以看到,如果协议的角度来分析,两者之间的关系是:
(1)在发送端,传输层将数据传递给网路层
(2)在网络上,网络层将数据从发送端传输到目的端
(3)在目的端,网络层将数据传递给传输层
这么说,似乎有点抽象,不过图5-3也已经做了比喻,如果把应用层(数据)比作货物的话,那么发送端的传输层,其实只是相当于将货物装车,目的端的传输层,相当于将货物卸车,而真正起到运输作用的是网络层。我们把这个比喻再形象一点,如图5-4所示:
图5-4 传输层与网络层之间的比喻
这样一来,误会应该能消除了:传输层并不负责传输,负责传输的其实是网络层。
都是名字惹的祸!
但是,TCP 的名字其实取的完全没毛病,人家叫“传输控制协议”,可没有叫“传输协议”。另外,UDP,“用户数据报协议”,虽然看起来不知道是什么意思,但是也没有说自己是“传输”的意思。
只是当初 OSI 模型为什么要将其取名为传输层(Transport Layer)呢?
好吧,我们就不再纠结名字的事情,毕竟那已经约定俗成,也无法改变。我们只需要明白名字背后所蕴含的事实:
(1)传输层是一个“传输控制”的协议(我们把 UDP 的“不控制”也当作一种广义的“控制”),而网络层才是一个“传输”的协议
(2)传输层协议,是运行在通信的两端,也就是运行在主机上的协议,而网络层是一个运行在路由器上的协议(当然,两端的主机上也会运行网络层协议)。
5.1 TCP 报文结构
1974年,罗伯特·卡恩(Robert Elliot Kahn)和文顿·格雷·瑟夫(Vinton Gray Cerf)提出TCP/IP协议,定义了在计算机网络之间传送报文的方法("A Protocol for Packet Network Intercommunication", IEEE Transactions on Communications, Vol. COM-22, No. 5, pp 637-648, May 1974)。他们也因此于2004年获得图灵奖,并被尊称为 Internet 之父。
TCP 是一种面向连接的、可靠的、基于字节流的端到端的传输层通信协议(RFC 793)。这里面的每一个关键词的背后,都蕴含着 TCP 的思考和实现机制。为了一探究竟,我们还是从 TCP 的报文结构开始。
TCP 报文承载于 IP 报文,如图5-5所示:
图5-5 TCP 报文位置
从图5-5可以看到,TCP 报文位于 IP 报文头之后,从 IP 的视角来看,TCP 报文属于 IP 的数据(对应着 IP Header 中的“协议”字段等于6)。
进一步打开,TCP 的报文结构,如图5-6所示:
图5-6 TCP 报文结构
图5-6中某些字段,需要放到一定的上下文中才能解释清楚,这个我们放到下面的小节再介绍。本节暂时只介绍一些相对“独立”的字段。
5.1.1 源端口号/目的端口号(Source Port/Destination Port)
源端口号、目的端口号的数据类型都是无符号整数,各占据 16 bits,所以其取值范围都是 0~65535(64K)。(由于两者的意义完全相同,除了一个指代“源”、一个指代“目的”这一区别,下文为了行文方便,有时候会以“端口号”进行统称)
端口号是一种数字资源,这一点与门牌号码、身份证号码、IP 地址是一样的。它们是数字世界里的1个数字,但是总与现实世界里的物理对象相挂钩,比如身份证号,就与1个人相挂钩。
1个端口号就与 Host 里的1个进程相挂钩。笔者犹豫再三才写下这句话。笔者的纠结是:在 TCP/UDP 的介绍中,是否要涉及编程的概念?笔者感觉,适当引入编程的概念,非常有助于理解 TCP/UDP。同时我也尽可能弱化编程,期望不妨碍您的理解,即使您没有编程经验。
需要强调的是,1个端口号与1个进程挂钩,但是它并等于该进程的进程 ID。
TCP 为什么要引入端口号这个概念呢?这个与 TCP 的连接有关,也与传输层、应用层之间的关系相关,如图5-7所示:
图5-7 传输层与应用层之间的关系
图5-7中,P1、P2 代表不同的进程(Process),I1 代表应用层与传输层之间的接口(Interface)。
在前面的章节中,我们丝毫没有涉及网络层与数据链路层、数据链路层与物理层之间的接口,包括本章,我们也不会涉及传输层与网络层之间的接口,因为本文的目的是介绍相关的协议,而不是介绍具体的编程和实现,我们只是抽象地谈问题。
但是到了传输层就避无可避,需要明确指出是应用层哪个进程数据交给传输层(发送出去),传输层接收到的数据,又得交给应用层哪个进程。这就引出了端口号的概念,如图5-所示:
图5-8 进程与传输层之间的关系
图5-8中,主机A 的进程 Px 与 主机B 的进程 Py 进行 TCP/UDP 通信。对于 Px/Py 来说,它们是调用传输层的接口(I1),对于传输层来说,它们是与 Px/Py 进行了绑定。这样才不会乱套,才能使 Px 与 Py 正确地通信。
就像蔡琴所唱的“是谁在敲打我窗”——传输层得知道是谁在敲打它的窗,才能将数据送给对方。
是谁在敲打我窗
那一段被遗忘的时光
以上的解释可能还是有点复杂,再换一个角度理解:网络层的使命是将数据从一个 IP 送到另一个 IP(只考虑单播),所以 IP 报文头中有“源 IP/目的 IP”两个字段;而传输层的使命是将数据从一个主机的进程送到另一个主机的进程,所以 TCP/UDP 报文头还需要另外两个字段“源端口号/目的端口号”,而这两个端口号的背后绑定的就是两个进程。
TCP/UDP 是如何知道端口号与其背后的进程之间的绑定关系的呢?无论是从编程的角度,还是从协议的角度,都涉及到了 socket 这个概念,如图5-9所示:
图5-9 socket
socket(套接字)作为一种编程接口,最早是由1983年发布的 BSD Unix 4.3版本所提出,而 socket 这个名词,更早是由1970年2月发表的 RFC 33 所提出。美国计算机历史博物馆还特意强调,RFC 33 比 BSD Socket 差不多早了12年。
图5-9所表达的是一种编程层面的概念:socket 是对 TCP/UDP 的一种抽象和封装,它对外提供了编程接口(I1)。
我们稍微看一点点编程的内容:
int main()
{
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
......
}
我们只须看两行加粗的代码。
第1行加粗的代码,创建了1个 socket:
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
第2行加粗的代码,将 IP 地址、端口号绑定到这个 socket:
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
另外还有很关键的一点,这是一个 main 函数,也就是说这个函数只要一运行,就会启动一个进程。所以说,bind 函数,更本质的含义是:将进程与端口号相绑定!
啰里啰嗦这么多,我们做个小结:
(1)TCP 的目的是将数据从一个主机的某个进程传递到另一个主机的某个进程。
(2)标识主机的是 IP 地址,而 IP 地址体现在 IP 报文头的字段中(源 IP/目的 IP)。
(3)标识进程的就是 TCP 报文头中的字段“源端口号/目的端口号”,端口号与进程之间的绑定关系,通过 socket 编程接口设置。
在 RFC 793中,有时候称 TCP 是一个 host-to-host(主机到主机)的协议,有时候称之为 process-to-process(进程到进程)的协议,有时候又陈之为 end-to-end(端到端)的协议。严格来说,host-to-host 并不准确,准确的是 process-to-process,或者是 end-to-end。这里的“end”指的是 host 上的 process。
最后,我们对 socket 做一个补充——这个会在下面的章节会用到——标识1个 socket,是两个元素:IP 地址、端口号(背后是1个进程)。这个可以从 bind 函数可以看出来。
5.1.2 数据偏移量(Data Offset)
Data Offset,占位4个 bits,指的是 TCP 数据从哪开始,单位是4个字节(32 bits)。这个字段的含义,其实也就是表达的是 TCP 头部的长度(单位是4字节):数据的偏移量,潜台词就是 TCP 头部的长度。这里面有3层含义:
(1)因为 Data Offset 只有4个 bits,所以它的最大值是15,也就是说 TCP 头部最多是60个字节(15 * 4 = 60)。
(2)TCP 头部包括固定部分和可选部分,固定部分占有20个字节,所以可选部分最多是40个字节。
(3)因为 Data Offset 的单位是4字节,这也意味着可选部分的长度也必须是4字节的整数倍——如果不是,需要用填充(Padding)字段补齐(也就是用“0”补齐)。
5.1.3 保留(Reserved)
Reserved 字段,顾名思义就是保留给将来使用,当前其值必须为0。有一点值得一提的是,在 RFC 793中,Reserved 字段是6 bits,后来 RFC 3168 又增加了2个标志位,所以现在 Reserved 字段只有4个bits。
5.1.4 标志位(Flags)
RFC 793 定义了6个标志位,RFC 3168 又增加了2个,一共8个,每个标志位占用1个 bit。这些标志位,我们会在后面的章节中继续介绍,这里只是简要描述一下,如表5-1所示:
表5-1 TCP 标志位
代号 | 简称 | 简述 | RFC |
C | CWR | Congestion Window Reduced,拥塞窗口减少。与拥塞控制有关 | 3168 |
E | ECE | ECN-Echo,ECN:Explicit Congestion Notification(显示拥塞通知)。与拥塞控制有关 | 3168 |
U | URG | Urgent Pointer field significant,紧急指针字段标记 | 793 |
A | ACK | Acknowledgment field significant,确认字段标记 | 793 |
P | PSH | Push Function,数据要尽快“Push”到应用程序 | 793 |
R | RST | Reset the connection,重新设置连接 | 793 |
S | SYN | Synchronize sequence numbers,同步序列号 | 793 |
F | FIN | Finish 的缩写,No more data from sender,没有数据需要传输 | 793 |
5.1.5 校验和(Checksum)
TCP 的校验和与 IP 的校验和的算法是一样的,只是所计算的内容不同。TCP 校验和计算的内容包括:TCP 伪头部、TCP 头部、TCP 数据,如图5-10所示:
图5-10 TCP 校验和
TCP 伪头部(TCP Pseudo Header)并不是一个真正存在的 Header(不会在网络上传输),只是 TCP 为了计算校验和时所引入的一个数据结构,它所包含的内容,如表5-2所示:
表5-2 TCP 伪头部的内容
字段名 | 长度(bits) | 简述 |
Source Address | 32 | 源 IP 地址 |
Destination Address | 32 | 目的 IP 地址 |
Zero | 8 | 全“0” |
Protocol | 8 | 协议字段。TCP:6,UDP:17 |
TCP Length | 16 | TCP 报文长度,单位是字节,包括 TCP Header 和 TCP Data 的长度。但是 TCP Data 不包含 Padding 字节(下文会描述) |
TCP 伪头部这些字段,都是从 IP Header 中获取的(包括 TCP Length 也是从中获取),从某种意义上讲,这使 TCP 和 IP 之间产生了过度耦合及不合“常理”的依赖,并不是一个好主意。但是就算有一万个不合理,考虑到已经既成事实,现在修改这一算法也不大可能。从另一个角度来讲,TCP 这么做,其本意可能恰恰是为了不“依赖” IP 层的正确性(当然,也只能是部分不依赖,不可能完全不依赖):TCP 试图通过自己的校验和算法,也能保证“源 IP、目的 IP、协议、TCP 长度”这些字段的正确性。
用架构上的依赖来解决结果(校验和)上的不依赖,怎么说呢......算了,对于既成事实,咱就不纠结了......
TCP 校验和所计算的内容,其他两部分:TCP Header、TCP Data,其含义非常明确,这里就不再啰嗦。只是有一点需要强调:由于 TCP 校验和的算法要求总长度(TCP 伪头部长度 + TCP 头部长度 + TCP Data 长度)必须是偶数字节,所以如果实际的总长度为奇数字节,必须在 TCP Data 最后补上1个字节的 Padding(其值为“0”)。但是这1个字节的 Padding:(A)不会在网络上传输;(B)也不会影响 TCP 伪头部中的 TCP Length 字段(即 TCP Length 不包括这1个字节的 Padding)。
TCP 校验和的算法,与 IP 校验和的算法相同。对于 TCP 发送方而言:
(1)首先将校验和字段置为0;
(2)将 TCP 校验和所涉及的所有字段(TCP 伪头部、TCP 头部、TCP 数据),将每16 bits 当作一个数字,每个数字进行二进制求和;
(3)如果和的高16 bits 不为0,则将和的高16 bits 和低16 bits 反复相加,直到和的高16 bits 为0,从而获得一个16 bits 的值;
(4)将该16 bits 的值转为反码,存入校验和字段。
而对于 TCP 的接收方,则只需要执行上述算法的第“2”两步,如果计算结果为0,则认为 TCP 伪头部、TCP 头部、TCP 数据都是完整的和正确的。不过有一点需要强调的是:此时的校验和字段并没有被设置为0,而是保留原值。
5.1.6 选项(Options)
TCP Header 中的选项从数据结构的角度来讲,分为两大类型,如图5-11所示:
图5-11 TCP Options 数据结构
第一类数据结构就是1个单字节(8 bits),只包含 option-kind。第二类数据结构是多字节,包括:option-kind(8 bits)、option-length(8 bits)、option-value(变长)等3部分,其中 option-length 的单位是字节,包括“option-kind、option-length、option-value”三者加在一起的长度。
常用的 TCP Options,如表5-3所示:
表5-3 常用的 TCP Options
类型 | 长度 | 名称 | RRC | 简述 |
0 | 1 | EOL | 793 | 选项结束 |
1 | 1 | NOP | 793 | 无操作 |
2 | 4 | MSS | 793 | Maximum Segment Size,最大Segment长度 |
3 | 3 | WSopt | 1323 | TCP Window Scale Option,窗口扩大系数 |
4 | 2 | SACK-Premitted | 2018 | TCP Sack-Permitted Option,表明支持 表明支持SACK |
5 | 可变 | SACK | 2018 | A Selective Acknowledgment,SACK Block(收到乱序数据) |
8 | 10 | TSopt | 1323 | TCP Timestamps Option,时间戳 |
19 | 18 | TCP-MD5 | 2385 | MD5认证,已废弃,被 TCP-AO 取代 |
28 | 4 | UTO | 5482 | TCP User Timeout Option,超过一定闲置时间后拆除连接 |
29 | 可变 | TCP-AO | 5925 | TCP Authentication Option,TCP 认证选项 |
253 | 可变 | Experimental | 4727 | 保留,用于科研实验 |
254 | 可变 | Experimental | 4727 | 保留,用于科研实验 |
表5-3中的 EOL、NOP 两个字段,与 IP Options 中的对应的两个字段,完全相同,这里就不再重复。其他字段中,有些与 TCP 的连接、数据传输等密切相关,需要放到后面的章节中介绍,本节只介绍剩余的几个。
5.1.6.1 MSS
MSS(Maximum Segment Size,最大Segment长度)的数据结构,如图5-12所示:
图5-12 MSS 数据结构
MSS 指的是在 TCP 的过程中,TCP Data(不包含 TCP Header)的最大长度(长度单位是字节)。这个是创建 TCP 连接时,TCP 双方协商的一个参数。关于 TCP 连接的创建,我们下一节会讲述,这里暂且不用管 TCP 连接这个概念,而只关注 MSS 本身。
从某种意义上讲,MSS 是一个“请求”而不是一个“约束”:TCP 的一方通知另一方,其所能接收的 TCP Segment 的最大长度是 MSS,至于另一方是否遵守那另当别论——当然,也没有谁的代码故意去违反这一“请求”。
如果双方没有协商这一参数,理论上说,TCP 的双方可以发送/接收任意长度的 TCP Segment,不过在具体的实践中这也不可能——不仅不可能,双方还谨小慎微:TCP 默认的 MSS 是536(字节)。
536字节,确实稍微有点保守,毕竟以太网的 MTU = 1500,从而可以推导出 TCP Segment 的长度为1460字节(1500 - 20(IP Header 长度,不包含 IP Options) - 20(TCP Header 长度,不包含 TCP Options))。所以,IPv4 网络中的 MSS 典型取值是 1460(IPv6 是 1440)。
最后补充一点,由于 MSS 占用2个字节(16 bits,参见图5-9),所以 MSS 的最大值是 65535(64K),考虑到 IPv4 的 MSS 的典型值仅仅是1460,这已经是足够的。
5.1.6.2 TCP-MD5 & TCP-AO
TCP-MD5 和 TCP-AO 的主要目的都是用于防止TCP欺骗攻击(TCP Spoofing Attacks)。从 RFC 的角度,TCP-MD5(RFC 2385,1998年8月)已经被 TCP-AO 取代(RFC 5925,2010年6月),然而TCP-AO当前的普及率还很低。
MD5(MD5 Message-Digest Algorithm,消息摘要算法)是一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)设计,于1992年公开,用以取代MD4算法。
罗纳德·李维斯特(2002年图灵奖)
TCP-MD5 的数据结构,如图5-13所示:
图5-13 TCP-MD5 数据结构
由于 MD5 digest 固定为16字节,所以 TCP-MD5 的 option-length 字段的值为18(字节)。TCP-MD5 所计算的内容包括:
(1)TCP 伪头部,与 TCP 校验和算法中的 TCP 伪头部相同
(2)TCP 头部,但是不包括 TCP 选项,而且假设 TCP 校验和为0
(3)TCP 数据
(4)一个 TCP 通信双方都知道的密钥
TCP-AO 的数据结构如图5-14所示:
图5-14 TCP-AO数据结构
相对于TCP-MD5,TCP-AO(TCP Authentication Option,TCP 认证选项)的主要改进之处在于:
(1)支持多种消息认证码(Message authentication code,MAC)算法
(2)支持带内的密钥变更操作
详细介绍 TCP-AO 超出了文章主题,具体信息您可以参考 RFC 5925。
最后需要强调的是,TCP-MD5 和 TCP-AO 都没有密钥分发机制,这一点使得密钥的分发存在一定的安全风险,也限制了两者的使用。