最近学了些TCP/IP相关的内容,做了些笔记。
网络分层
网络分层的原因
复杂的程序都需要分层,各层次之间是独立的
某一层并不需要知道它的下一层是如何实现的,而仅仅需要知道该层通过层间的接口所提供的服务,这样能降低问题的复杂度,上层工作不影响下层的工作
易与多人协作,易与实现和维护,促进标准化工作
只要是在网络上跑的包,都是完整的,可以有下层没上层,绝不可能有上层没下层
对于TCP来说,只要想发出去包,就必须要有IP层和MAC层
网络如何分层
- 一般有五层模型和七层模型,按个人理解,七层模型只是把五层模型最上层的应用层给细化了
- 物理层(中继器、集线器、HUB、网线)
- 数据链路层(MAC)
- 网络层(IP)
- 传输层(TCP/UDP)
- 应用层(HTTP(S))
网络分层运行的过程
- 接收过程
当一个包经过网口时,首先看看要不要请进来处理一下(配置了混杂模式的网口,凡是经过的都拿进来),拿进来以后交给一段程序处理,于是调用process_layer2(buffer)
,当然这是假函数,但你知道肯定有类似功能的函数的,这个函数功能是从buffer中摘掉二层(MAC)的头查看。
假如发现这个包的MAC地址与自己相符,说明是发给你的,于是调用process_layer3(buffer)
,摘掉三层(IP)的头查看,如果IP地址不是自己的,就应该转发出去,如果IP地址是自己的,就根据IP头的指示,拿掉三层的头,进程下一层的处理,如果地址是TCP的,调用process_tcp(buffer)
,如果是UDP的调用process_udp(buffer)
。
假设是TCP的,调用process_tcp(buffer)
,查看四层的头,看是一个发起,还是应答,或者是一个正常的数据包,分别交给不同的逻辑处理。如果是发起或者应答,接下来可能要发送一个回复包;如果是一个正常的数据包,就需要交给上层了。交给谁呢?是不是有 process_http(buffer)
函数呢?
没有的,如果你是一个网络包处理程序,你不需要有process_http(buffer),而是应该交给应用去处理。交给哪个应用呢?在四层的头里面有端口号,不同的应用监听不同的端口号。如果发现浏览器应用在监听这个端口,那你发给浏览器就行了。至于浏览器怎么处理,和你没有关系。
浏览器自然是解析 HTML,显示出页面来。电脑的主人看到页面很开心,就点了鼠标。点击鼠标的动作被浏览器捕获。浏览器知道,又要发起另一个 HTTP 请求了,于是使用端口号,将请求发给了你。
- 发送过程
收到了一个封装好的HTTP包后,首先调用send_tcp(buffer)
,buffer
里就是HTTP请求的内容,这个函数里面加一个TCP的头,记录下浏览器给的源端口号,一般是80(HTTPS就443)。
然后调用send_layer3(buffer)
,buffer
里面已经有了HTTP的头和内容,以及TCP的头,这个函数里面加一个IP的头,记录下源IP地址和目标IP地址。
然后调用send_layer2(buffer)
,buffer
里面已经有了HTTP的头和内容、TCP的头,以及IP的头,这个函数里面加一个MAC的头,记录源MAC地址,得到的就是本机器的 MAC 地址和目标的 MAC 地址。不过,这个还要看当前知道不知道,知道就直接加上;不知道的话,就要通过一定的协议处理过程,找到 MAC 地址。反正要填一个,不能空着。
只要buffer
里面的内容完整,就可以从网口发出去了。
数据链路层 MAC
MAC的全称是Medium Access Control,即媒体访问控制。
以太网规定,连入网络的所有设备,都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。
每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。
前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。
一块网卡获取另一块网卡MAC地址的方式:ARP协议,当已知IP时请求MAC地址
ARP的工作方式:广播,向同个子网络内所有计算机发送数据包,让每台计算机自己判断自己是否为接收方
网络层 IP
- ARP寻找MAC的广播方式只能在子网络内使用,每个子网络之间通信需要IP协议
- Linux 上查看IP地址的命令:
ifconfig
和ip addr
、没有的话自行安装net-tools
和iproute2
IP地址
- IPv4的网络地址由32个二进制位组成
- IP 地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码
CIDR
- 32位的IP地址分成了5类,但因为太浪费了,所以使用
CIDR
- CIDR代替ABC类分配IP地址段,注意私有IP段,CIDR伴随广播地址和子网掩码
- 32位的IP地址分成了5类,但因为太浪费了,所以使用
IP的作用
- IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
从IP得到MAC的方式
- 因为IP数据包是放在以太网数据包里发送的,所以我们必须同时知道两个地址,一个是对方的MAC地址,另一个是对方的IP地址。通常情况下,对方的IP地址是已知的,但是我们不知道它的MAC地址。分两种情况
- 如果两台主机不在同一个子网络,那么事实上没有办法得到对方的MAC地址,只能把数据包传送到两个子网络连接处的”网关”(gateway),让网关去处理。
- 如果两台主机在同一个子网络,那么我们可以用ARP协议,得到对方的MAC地址。
- DHCP请求IP地址,DHCP附送PXE协议安装OS
ICMP 和 ping
ping 是基于 ICMP 工作的,ICMP全称Internet Control Message Protocol,就是互联网控制报文协议。
ICMP 报文是封装在 IP 包里面的。因为传输指令的时候,肯定需要源地址和目标地址。它本身非常简单。
查询报文
- ping 就是ICMP的查询报文,是一种主动请求,并获得主动应答的ICMP协议,不过ping在ICMP后面增加了自己的格式
- ping 主动请求的发送:
ICMP ECHO REQUEST
- ping 主动请求的回复:
ICMP ECHO REPLY
- 比原生ICMP多了两个字段:标识符、序号,以及可选存放请求的时间值,可计算往返时间
差错报文
- 终点不可达:3,分为网络不可达、主机不可达、协议不可达、端口不可达、需要进行分片但设置了不分片等
- 源抑制:4,让源站放慢发送速度
- 超时:11,超过网络包的生存时间还没到达目标
- 重定向:5,让下次发给另一个路由器
ping 的发送和接收过程
- 可以看出ping是使用了ICMP里的
ECHO REQUEST
和ECHO REPLY
- 可以看出ping是使用了ICMP里的
TTL (TimeToLive)
- 在IPv4中, TTL是IP协议的一个8个二进制位的值(0-255),这个值可以被认为是数据包在internet系统中可以跳跃的次数上限。
- TTL是由数据包的发送者设置的, 在前往目的地的过程中, 每经过一台主机或设备, 这个值就要减少一点。
- 如果在数据包到达目的地前, TTL值被减到了0,那么这个包将作为一个ICMP错误的数据包被丢弃。
差错报文的应用
Traceroute
- Traceroute 故意设置特殊的TTL,来追踪去往目的地时沿途经过的路由器
- 对目的IP地址发送UDP包,设置TTL为1,即到达第一个路由就挂掉了,然后返回一个ICMP差错报文包,类型是超时
- 然后设置TTL为2,继续到第二个路由就挂了,返回超时,逐步迭代这个过程取得整个路径上的路由IP和延迟
- 有些路由不会回复这个ICMP,这就是Traceroute返回内容部分空白的原因
- 另外一个作用是故意设置不分片,从而确定路径的MTU
TCP 和 UDP 的区别
TCP 面向连接,UDP 面向无连接
TCP 提供可靠交付,UDP 不可靠
- TCP 连接传输的数据,无差错,不丢失,不重复,按序到达
- UDP 继承 IP 包的特性,不保证不丢失,不保证按顺序到达
TCP 基于字节流,UDP 基于数据报
- TCP 发送的时候是一个流,没头没尾
- 虽然 IP 包是一个个的 IP 包,TCP 自己维护成了流
- UDP 继承了 IP 的特性,基于数据报,一个个地发,一个个地收
TCP 有拥塞控制, UDP 没有
- TCP 意识到包丢失或者网络环境差后,根据情况调整行为,调节发送速度
- UDP 没有意识,应用叫发包就发
TCP 是有状态服务,UDP 是无状态服务
- TCP 精确记录了发送、接收状态
- UDP 不记录状态
UDP
UDP 包头格式
- 源端口号 | 目的端口号
- 16位UDP长度 | 16位UDP校验和
- 数据
UDP 特点
沟通简单,认为网络通路默认就是很容易送达的,不容易被丢弃的
轻信他人, 不会建立连接,虽然有端口号,但是监听后任何人都能发数据给他,他也可以发数据给任何、任意多人
愣头青,不会根据网络情况进行发包的拥塞控制,无论丢包成什么样还是该怎么发就怎么发
UDP 使用场景
需要资源少,网络环境好的内网,或对丢包不敏感的应用。如 DCHP 和 TFTP 就是基于 UDP 协议的,因为BIOS资源少不适合维护TCP那种复杂的状态机
不需要一对一沟通建立连接,而是可以广播的应用。UDP 面向无连接的功能可以承载广播或者多播的协议,基于 UDP 的 DHCP 就是广播的形式
需要处理速度快,时延低,可容忍少数丢包,但即便网络拥塞也要不停歇发包的应用。比如网络直播,用户不关心过时数据,老数据丢了也就丢了,但不能因为拥塞而停歇了
UDP 使用例子
网页或者App的访问
- QUIC协议
- 移动互联,网络经常变换,TCP会经常重连很耗时,基于UDP的QUIC就比较合适
流媒体协议
- TCP 的严格顺序传输不适合直播,用户不在意老数据而是要实时性,宁可丢包也不要卡顿
- 很多直播应用基于 UDP 实现自己的视频传输协议
实时游戏
- 游戏的实时性要求很高,而且服务器需要沟通很多客户端(玩家),在异步IO机制引入之前,UDP是应对海量客户端连接的策略
- TCP的强顺序问题,对战游戏对网络要求简单,如FPS只需传输玩家位置和行为等,客户端解析响应并渲染场景,玩家并不关心过期数据,如果一个数据包丢失导致卡顿,下一秒就被爆头了。用UDP自定义可靠协议,自定义重传策略,能尽量降低延迟,减少网络问题对游戏性能的影响
IoT 物联网
- 物联网设备终端资源少,维护TCP开销太大
- 物联网对实时性要求高,TCP容易导致高延迟
- Google子公司推出的物联网通信协议 Thread 就是基于 UDP 的
移动通信领域
- 4G 网络的数据协议 GTP-U 是基于 UDP 的
UDP 小结
如果将 TCP 比作成熟的社会人,UDP 则是头脑简单的小朋友。TCP 复杂,UDP 简单;TCP 维护连接,UDP 谁都相信;TCP 会坚持知进退;UDP 愣头青一个,勇往直前;
UDP 虽然简单,但它有简单的用法。它可以用在环境简单、需要多播、应用层自己控制传输的地方。例如 DHCP、VXLAN、QUIC 等。
TCP
- TCP的seq是32位的计数器,每4微秒加1,计算可知284分钟即4.73小时重置一次
TCP 包头格式
- 源端口号 | 目标端口号
- 包序号:
seq
- 确认序号:
ack
- 首部长度 | 保留 | URG | ACK | PSH | RST | SYN | FIN | 16位窗口大小
- 校验和 | 紧急指针
- 选项
- 数据本体
- 首先与UDP一样是源端口和目标端口号,用于确定发给哪个应用
seq
序号解决乱序问题,确定每个包的先来后到,是个32位的计数器,每4微秒加1,计算可知284分钟即4.73小时重置一次ack
的值是对方发来的TCP包内seq
的值+1,解决不丢包问题- 状态位,
SYN
是发起连接,ACK
是回复,RST
是重新连接,FIN
是结束连接,这是TCP面向连接的体现,维护连接状态,这些带状态位的包的发送会引起双方的状态变更 - 窗口大小,做流量控制,双方各自声明一个窗口,标识自己当前的处理能力,不要发送得太快也别太慢
TCP 的特征
- 顺序问题,稳重不乱;
- 丢包问题,承诺靠谱;
- 连接维护,有始有终;
- 流量控制,把握分寸;
- 拥塞控制,知进知退。
三次握手四次挥手
三次握手
- ① A:您好,我是 A ② B:您好 A,我是 B ③ A:您好 B
- TCP连接的建立称为三次握手,“请求 -> 应答 -> 应答之应答”,让双方的消息都“有去有回”
- 第三次握手,A应答B的应答包,能解决一个问题:B收到了来自很久之前A的多次申请连接的SYN包,如果两次握手,B这时建立了连接就等于单相思,所以需要“应答之应答”
- 为什么不四次握手?因为就算40次握手也不能真正保证连接就建立了,只要双方都有去有回就基本认为建立了,并且一般A和B连接建立后就开始发数据,一但A方开始发送数据,问题就解决了
- 如果连接建立后A一直不发数据,这个在HTTP有keepalive机制,即使没有真实数据包也有探活包
- B也可以设计对于长时间不发包的A主动关闭连接
- 大写
ACK
与小写ack
的区别:大写的是状态位,当为1时表示包是回复包,小写是确认序号,值是对方的序号seq
+1
一开始,客户端和服务端都处于 CLOSED
状态。先是服务端主动监听某个端口,处于 LISTEN
状态。然后客户端主动发起连接 SYN
,之后处于 SYN-SENT
状态。服务端收到发起的连接,返回 SYN
,并且 ACK
客户端的 SYN
,之后处于 SYN-RCVD
状态。客户端收到服务端发送的 SYN
和 ACK
之后,发送 ACK
的 ACK
,之后处于 ESTABLISHED
状态,因为它一发一收成功了。服务端收到 ACK
的 ACK
之后,处于 ESTABLISHED
状态,因为它也一发一收了。
四次挥手
- 由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送
- 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号
- 服务器B关闭与客户端A的连接,发送一个FIN给客户端A
- 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1
- 上面2与3的过程中,B可能还有要发给A的数据没发送完,就首先ACK了A的FIN后,等处理完数据后再发送FIN给A,所以一共需要4次挥手。
白话解释四次挥手
① A:B 啊,我不想玩了 ② B:哦,你不想玩了啊,我知道了 ③ B:A 啊,好吧,我也不玩了,拜拜 ④ A:好的,拜拜。
A 开始说“不玩了”,B 说“知道了”,这个回合是没什么问题的,因为在此之前,双方还处于合作的状态,如果 A 说“不玩了”,没有收到回复,则 A 会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有一方率先撕破脸。
一种情况是,A 说完“不玩了”之后,直接跑路,是会有问题的,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。另一种情况是,A 说完“不玩了”,B 直接跑路,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。
断开的时候,我们可以看到,当 A 说“不玩了”,就进入 FIN_WAIT_1 的状态,B 收到“A 不玩”的消息后,发送知道了,就进入 CLOSE_WAIT 的状态。
A 收到“B 说知道了”,就进入 FIN_WAIT_2 的状态,如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
如果 B 没有跑路,发送了“B 也不玩了”的请求到达 A 时,A 发送“知道 B 也不玩了”的 ACK 后,从 FIN_WAIT_2 状态结束,按说 A 可以跑路了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个“B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。
A 直接跑路还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都死翘翘,再空出端口来。
等待的时间设为 2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个TTL 域,是 IP 数据报可以经过的最大路由数,每经过一一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这个时候 A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑了。
TCP 状态机
- 把上面两个时序图合并起来就是这个TCP状态机图
- 加黑加粗的部分是主要流程,阿拉伯数字的序号是连接建立的顺序,大写中文数字的序号是连接断开的顺序。加粗实线是客户端A的状态变化过程,加粗虚线是服务端B的状态变化过程。细虚线是其他非主流过程。
TCP 的顺序机制
每个包都有一个ID,按照ID一个个发送
累计确认(累计应答)
- 发送端和接收端分别有缓存保存记录所有发送和接收的包
发送端窗口
发送端的缓存里是按照包ID排列的,有四种类型
- 发送了且已确认
- 发送了且尚未确认
- 还没发送但等待发送
- 还没发送且暂时不会发送
上面3和4的区分是流量控制
- 流量控制使用窗口(
Advertised Window
),窗口大小等于上面2+3部分,即发送未确认和未发送但可发送的 LastByteAcked
:第一部分和第二部分的分界线LastByteSent
:第二部分和第三部分的分界线LastByteAcked + AdvertisedWindow
:第三部分和第四部分的分界线
- 流量控制使用窗口(
接收端窗口
- 接收端的缓存里是按照包ID排列的,有三种类型
- 接收且确认过
- 尚未接收但可接收
- 尚未接收且不能接收
流量控制使用窗口(
AdvertisedWindow
),窗口大小是上面第2部分,需要计算MaxRcvBuffer
:最大缓存的量;LastByteRead
之后是已经接收了,但是还没被应用层读取的;NextByteExpected
是第一部分和第二部分的分界线。
NextByteExpected
和LastByteRead
的差其实是还没被应用层读取的部分占用掉的MaxRcvBuffer
的量,我们定义为A
。AdvertisedWindow
其实是MaxRcvBuffer
减去A
,即第2部分的大小- 也就是:
AdvertisedWindow = MaxRcvBuffer - ((NextByteExpected-1) - LastByteRead)
。 - 第2部分和第3部分的分界线就是
NextByteExpected
+AdvertisedWindow
,即LastByteRead
+MaxRcvBuffer
- 接收端的缓存里是按照包ID排列的,有三种类型
TCP 的丢包重发机制
确认与重发机制
- 超时重试,没有
ACK
的包都有设一个定时器,超过一定时间就重新尝试 - 超时时间需要大于往返时间 RTT,也不宜过长
- 超时重试,没有
自适应重传算法
- TCP 通过采样 RTT 时间和 RTT 的波动范围,进行加权平均,算出一个值,还需要根据新状况不断变化
- 超时触发重传的问题是超时周期可能很长
快速重传机制
- 当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。
- 还有一种方式
SACK
(Selective Acknowledgment),在TCP头里加一个SACK,可以把可以将缓存的地图发送给发送方。
TCP 的流量控制机制
如果发送方发送太猛,接收方处理不过来导致缓存中没有空间,可以通过确认信息修改窗口大小,甚至设为0,使发送方暂时停止发送
当发送方发现自己窗口被调整到0后,会定时发送窗口探测数据包,看是否有机会调整窗口大小
当接收方比较慢时,要防止低能窗口综合征,避免空出一个字节就通知发送方然后又填满窗口。当窗口太小时,可以暂停更新窗口,直到窗口达到一定大小或者缓冲区一半为空,才更新窗口
TCP 的拥塞控制机制
也是通过窗口大小控制,滑动窗口
rwnd
是怕发送方把接收方缓存占满,拥塞窗口cwnd
是怕把网络塞满LastByteSent - LastByteAcked <= min {cwnd, rwnd}
,是拥塞窗口和滑动窗口共同控制发送的速度发送方难以判断网络是否满的方法,在TCP看来网络路径是一个黑盒
TCP 发送包常被比喻为往一个水管里面灌水,而 TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。
TCP 的拥塞控制主要用来避免两种现象:包丢失和超时重传
一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?
慢启动:一开始
cwnd
大小为一个报文段,当收到确认时指数型增大cwnd
大小指数型涨到一个值为65535字节的
ssthresh
时降为线性增长,每8个确认cwnd
增加1,但总有溢出的时候。当出现拥塞即丢包时,需要超时重传,
ssthresh
设为cwnd/2
,cwnd
设为1,重新开始慢启动,一夜回到解放前。
快速重传
当接收端发现丢了一个中间包时,发送三次前一个包的ACK,于是发送端就会快速重传,不必等待超时重传。
此时
cwnd
=cwnd
/ 2,sshthresh
=cwnd
,当三个包返回时,cwnd = sshthresh + 3
也就是不用回到解放前,只是减半而已
这种机制导致延迟高的情况下,反而降低速度,导致本文前面UDP部分里说的TCP的问题
丢包不代表通道满了,可能本身公网就是会丢包的,这时其实并没有拥塞
TCP 的拥塞控制要等到将中间设备都填充满了才发生丢包,从而降低发送速度,此时已经晚了,其实TCP只要通道满了就应该开始控制了,而不应该等到连缓存都满了才控制
解决方法:
BBR
拥塞算法,找到一个平衡点,填满管道但是不填满中间设备的缓存,达到高带宽和低时延的平衡
TCP 小结
- TCP 包头很复杂,但是主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制
- 连接的建立是经过三次握手,断开的时候四次挥手
- 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的
- 拥塞控制是通过拥塞窗口来解决的