从字面上来看,很多人会认为 TCP/IP 是 TCP、IP 这两种协议,实际上TCP/IP 协议族指的是在 IP 协议通信过程中用到的协议的统称
可以看到协议的分层从上往下依次是
应用层的本质是规定了应用程序之间如何相互传递报文, 以 HTTP 协议为例,它规定了:
HTTP 客户端和 HTTP 服务端的首要工作就是根据 HTTP 协议的标准组装和解析 HTTP 数据包,每个 HTTP 报文格式由三部分组成:
以本例的请求报文格式为例:
除了我们熟知的 HTTP 协议,还有下面这些非常常用的应用层协议
传输层的作用是为两台主机之间的「应用进程」提供端到端的逻辑通信,相隔几千公里的两台主机的进程就好像在直接通信一样。
虽然是叫传输层,但是并不是将数据包从一台主机传送到另一台,而是对传输行为进行控制,这本小册介绍的主要内容 TCP 协议就被称为传输控制协议(Transmission Control Protocol),为下面两层协议提供数据包的重传、流量控制、拥塞控制等。
假设你正在电脑上用微信跟女朋友聊天,用 QQ 跟技术大佬们讨论技术细节,当电脑收到一个数据包时,它怎么知道这是一条微信的聊天内容,还是一条 QQ 的消息呢?
这就是端口号的作用。传输层用端口号来标识不同的应用程序,主机收到数据包以后根据目标端口号将数据包传递给对应的应用程序进行处理。比如这个例子中,目标端口号为 80,百度的服务器就根据这个目标端口号将请求交给监听 80 端口的应用程序(可能是 Nginx 等负载均衡器)处理。
网络互连层提供了主机到主机的通信,将传输层产生的的数据包封装成分组数据包发送到目标主机,并提供路由选择的能力。
IP 协议是网络层的主要协议,TCP 和 UDP 都是用 IP 协议作为网络层协议。这一层的主要作用是给包加上源地址和目标地址,将数据包传送到目标地址。
IP 协议是一个无连接的协议,也不具备重发机制,这也是 TCP 协议复杂的原因之一就是基于了这样一个「不靠谱」的协议。
网络访问层也有说法叫做网络接口层,以太网、Wifi、蓝牙工作在这一层,网络访问层提供了主机连接到物理网络需要的硬件和相关的协议。这一层我们不做重点讨论。
分层的好处是什么呢?
分层的本质是通过分离关注点而让复杂问题简单化,通过分层可以做到:
TCP 是面向连接的协议
面向连接(connection-oriented):面向连接的协议要求正式发送数据之前需要通过「握手」建立一个逻辑连接,结束通信时也是通过有序的四次挥手来断开连接。
无连接(connectionless):无连接的协议则不需要
通过三次握手协商好双方后续通信的起始序列号、窗口缩放大小等信息。
IP 是一种无连接、不可靠的协议:它尽最大可能将数据报从发送者传输给接收者,但并不保证包到达的顺序会与它们被传输的顺序一致,也不保证包是否重复,甚至都不保证包是否会达到接收者。不保证有序、去重、完整。
TCP 要想在 IP 基础上构建可靠的传输层协议,必须有一个复杂的机制来保障可靠性。主要有下面几个方面:
校验和(checksum) 每个 TCP 包首部中都有两字节用来表示校验和,防止在传输过程中有损坏。如果收到一个校验和有差错的报文,TCP 不会发送任何确认直接丢弃它,等待发送端重传。
包的序列号保证了接收数据的乱序和重复问题假设我们往 TCP 套接字里写 3000 字节的数据导致 TCP发送了 3 个数据包,每个数据包大小为 1000 字节:第一个包序列号为[1~1001),第二个包序列号为 [10012001),第三个包序号为[20013001)
假如因为网络的原因导致第二个、第三个包先到接收端,第一个包最后才到,接收端也不会因为他们到达的顺序不一致把包弄错,TCP 会根据他们的序号进行重新的排列然后把结果传递给上层应用程序。
如果 TCP 接收到重复的数据,可能的原因是超时重传了两次但这个包并没有丢失,接收端会收到两次同样的数据,它能够根据包序号丢弃重复的数据。
超时重传 TCP 发送数据后会启动一个定时器,等待对端确认收到这个数据包。如果在指定的时间内没有收到 ACK 确认,就会重传数据包,然后等待更长时间,如果还没有收到就再重传,在多次重传仍然失败以后,TCP 会放弃这个包。后面我们讲到超时重传模块的时候会详细介绍这部分内容。
TCP 是一种字节流(byte-stream)协议,流的含义是没有固定的报文边界。
假设你调用 2 次 write 函数往 socket 里依次写 500 字节、800 字节。write 函数只是把字节拷贝到内核缓冲区,最终会以多少条报文发送出去是不确定的,如下图所示
上面出现的情况取决于诸多因素:路径最大传输单元 MTU、发送窗口大小、拥塞窗口大小等。
当接收方从 TCP 套接字读数据时,它是没法得知对方每次写入的字节是多少的。接收端可能分2 次每次 650 字节读取,也有可能先分三次,一次 100 字节,一次 200 字节,一次 1000 字节进行读取。
在 TCP 中发送端和接收端可以是客户端/服务端,也可以是服务器/客户端,通信的双方在任意时刻既可以是接收数据也可以是发送数据,每个方向的数据流都独立管理序列号、滑动窗口大小、MSS 等信息。
在 TCP 中发送端和接收端可以是客户端/服务端,也可以是服务器/客户端,通信的双方在任意时刻既可以是接收数据也可以是发送数据,每个方向的数据流都独立管理序列号、滑动窗口大小、MSS 等信息。
TCP 是一个可靠的(reliable)、面向连接的(connection-oriented)、基于字节流(byte-stream)、全双工(full-duplex)的协议。发送端在发送数据以后启动一个定时器,如果超时没有收到对端确认会进行重传,接收端利用序列号对收到的包进行排序、丢弃重复数据,TCP 还提供了流量控制、拥塞控制等机制保证了稳定性。
TCP提供了一种字节流服务,而收发双方都不保持记录的边界,应用程序应该如何提供他们自己的记录标识呢?
以 centos7 为例
这篇文章来讲讲 TCP 报文首部相关的概念,这些头部是支撑 TCP 复杂功能的基石。完整的 TCP 头部如下图所示:
我们用一次访问百度网页抓包的例子来开始。
在第一个包的详情中,首先看到的高亮部分的源端口号(Src Port)和目标端口号(Dst Port),这个例子中本地源端口号为 61024,百度目标端口号是 80。
TCP 报文头部里没有源 ip 和目标 ip 地址,只有源端口号和目标端口号。
这也是初学 wireshark 抓包时很多人会有的一个疑问:过滤 ip 地址为 172.19.214.24 包的条件为什么不是 “tcp.addr == 172.19.214.24”,而是 “ip.addr == 172.19.214.24”
TCP 的报文里是没有源 ip 和目标 ip 的,因为那是 IP 层协议的事情,TCP 层只有源端口和目标端口。
源 IP、源端口、目标 IP、目标端口构成了 TCP 连接的「四元组」。一个四元组可以唯一标识一个连接。
TCP 是面向字节流的协议,通过 TCP 传输的字节流的每个字节都分配了序列号,序列号(Sequence number)指的是本报文段第一个字节的序列号。
序列号加上报文的长度,就可以确定传输的是哪一段数据。序列号是一个 32 位的无符号整数,达到 2^32-1 后循环到 0。
在 SYN 报文中,序列号用于交换彼此的初始序列号,在其它报文中,序列号用于保证包的顺序。
因为网络层(IP 层)不保证包的顺序,TCP 协议利用序列号来解决网络包乱序、重复的问题,以保证数据包以正确的顺序组装传递给上层应用。
如果发送方发送的是四个报文序列号分别是1、2、3、4,但到达接收方的顺序是 2、4、3、1,接收方就可以通过序列号的大小顺序组装出原始的数据。
在建立连接之初,通信双方都会各自选择一个序列号,称之为初始序列号。在建立连接时,通信双方通过 SYN 报文交换彼此的 ISN,如下图所示:
初始建立连接的过程中 SYN 报文交换过程如下图所示:
其中第 2 步和第 3 步可以合并一起,这就是三次握手的过程:
可以看到初始序列号的计算函数 secure_tcp_sequence_number() 的逻辑是通过源地址、目标地址、源端口、目标端口和随机因子通过 MD5 进行进行计算。如果仅有这几个因子,对于四元组相同的请求,计算出的初始序列号总是相同,这必然有很大的安全风险,所以函数的最后将计算出的序列号通过 seq_scale 函数再次计算。
seq_scale 函数加入了时间因子,对于四元组相同的连接,序列号也不会重复了。
序列号是一个 32 位的无符号整数,从前面介绍的初始序列号计算算法可以知道,ISN 并不是从 0 开始,所以同一个连接的序列号是有可能溢出回绕(sequence wraparound)的。TCP 的很多校验比如丢包、乱序判断都是通过比较包的序号来实现的,我们来看看 linux 内核是如何处理的,代码如下所示。
其中 __u32 表示无符号的 32 位整数,__s32 表示有符号的 32 位整数。为什么 seq1 - seq2 转为有符号的 32 位整数就可以判断 seq1 和 seq2 的大小了呢?
以 seq1 为 0xFFFFFFFF、seq2 为 0x02(回绕)为例,它们相减的结果如下。
seq1 - seq2 = 0xFFFFFFFF - 0x02 = 0xFFFFFFFD
0xFFFFFFFD 最高位为 1,表示为负数,实际值为 -(0x00000002 + 1) = -3,这样即使 seq2 回绕了,也可以知道 seq1
TCP 使用确认号(Acknowledgment number, ACK)来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。
关于确认号有几个注意点:
TCP 有很多种标记,有些用来发起连接同步初始序列号,有些用来确认数据包,还有些用来结束连接。TCP 定义了一个 8 位的字段用来表示 flags,大部分都只用到了后 6 个,如下图所示
下面这个是 wireshark 第一个 SYN 包的 flags 截图
我们通常所说的 SYN、ACK、FIN、RST 其实只是把 flags 对应的 bit 位置为 1 而已,这些标记可以组合使用,比如 SYN+ACK,FIN+ACK 等
可以看到用于表示窗口大小的"Window Size" 只有 16 位,可能 TCP 协议设计者们认为 16 位的窗口大小已经够用了,也就是最大窗口大小是 65535 字节(64KB)。就像网传盖茨曾经说过:“640K内存对于任何人来说都足够了”一样。
自己挖的坑当然要自己填,因此TCP 协议引入了「TCP 窗口缩放」选项 作为窗口缩放的比例因子,比例因子值的范围是 0 ~ 14,其中最小值 0 表示不缩放,最大值 14。比例因子可以将窗口扩大到原来的 2 的 n 次方,比如窗口大小缩放前为 1050,缩放因子为 7,则真正的窗口大小为 1050 * 128 = 134400,如下图所示
可选项的格式入下所示
以 MSS 为例,kind=2,length=4,value=1460
常用的选项有以下几个:
前面的文章中介绍过一个应用层的数据包会经过传输层、网络层的层层包装,交给网络接口层传输。假设上层的应用调用 write 等函数往 socket 写入了 10KB 的数据,TCP 会如何处理呢?是直接加上 TCP 头直接交给网络层吗?这篇文章我们来讲讲这相关的知识
数据链路层传输的帧大小是有限制的,不能把一个太大的包直接塞给链路层,这个限制被称为「最大传输单元(Maximum Transmission Unit, MTU)」
下图是以太网的帧格式,以太网的帧最小的帧是 64 字节,除去 14 字节头部和 4 字节 CRC 字段,有效荷载最小为 46 字节。最大的帧是 1518 字节,除去 14 字节头部和 4 字节 CRC,有效荷载最大为 1500,这个值就是以太网的 MTU。因此如果传输 100KB 的数据,至少需要 (100 * 1024 / 1500) = 69 个以太网帧。
不同的数据链路层的 MTU 是不同的。通过netstat -i 可以查看网卡的 mtu,比如在 我的 centos 机器上可以看到
IPv4 数据报的最大大小为 65535 字节,这已经远远超过了以太网的 MTU,而且有些网络还会开启巨帧(Jumbo Frame)能达到 9000 字节。当一个 IP 数据包大于 MTU 时,IP 会把数据报文进行切割为多个小的片段(小于 MTU),使得这些小的报文可以通过链路层进行传输。
IP 头部中有一个表示分片偏移量的字段,用来表示该分段在原始数据报文中的位置,如下图所示
前面我们提到 IP 协议不会对丢包进行重传,那么 IP 分段中有分片丢失、损坏的话,会发生什么呢?这种情况下,目标主机将没有办法将分段的数据包重组为一个完整的数据包,依赖于传输层是否进行重传。
利用 IP 包分片的策略,有一种对应的网络攻击方式IP fragment attack,就是一直传More fragments = 1的包,导致接收方一直缓存分片,从而可能导致接收方内存耗尽。
因为有 MTU 的存在,TCP 每次发包的大小也限制了,这就是下面要介绍的 MSS。
TCP 为了避免被发送方分片,会主动把数据分割成小段再交给网络层,最大的分段大小称之为 MSS(Max Segment Size)。
MSS = MTU - IP header头大小 - TCP 头大小
这样一个 MSS 的数据恰好能装进一个 MTU 而不用分片。在以太网中 TCP 的 MSS = 1500(MTU) - 20(IP 头大小) - 20(TCP 头大小)= 1460。
这就要说到 TSO(TCP Segment Offload)特性了,TSO 特性是指由网卡代替 CPU 实现 packet 的分段和合并,节省系统资源,因此 TCP 可以抓到超过 MTU 的包,但是不是真正传输的单个包会超过链路的 MTU。
全部0条评论
快来发表一下你的评论吧 !