TTCP/IP 网络模型有几层
为什么要有 TCP/IP 网络模型?
在同一个设备上进行通信有几种方式:管道,消息队列,共享内存
但是在不同设备上想要通信,就需要网络。不同类型的设备通信的具体实现不同,要兼容多种设备,就需要共同遵守一套协议,也就是 TCP/IP 网络协议
应用层
应用软件就在应用层,关注的是给用户实现什么功能。比如 HTTP 就是提供浏览器请求网页的功能,SMTP 就是提供发送邮件的功能,DNS 就是提供域名解析的功能。
应用层获取到要通信的数据,就会将数据移交给传输层,并不关心消息具体是怎么传输的。
应用层工作在 OS 用户态,传输层以下工作在内核态。因为应用软件必然不可能有内核权限,传输层以下要调用 OS 底层功能(利用网卡接收信息,封装数据包等)所以会在内核态
传输层
传输层为应用层提供网络支持,最重要就是 TCP 和 UDP 协议
TCP(transmission control protocol),是大部分应用层协议对应的传输层协议(如 HTTP),有很多特性来保证可靠传输,如流量控制,超时重传,拥塞控制等
UDP(user datagram protocol),不保证可靠传输,实时性更好,传输效率高,所以是 DNS 协议对应的传输层协议。UDP 想要实现可靠传输,就需要将可靠的功能在应用层实现。
应用传输的数据可以非常大,直接传输很大的数据不好控制(包太大了,占带宽,一丢全丢等问题),所以定义了 MSS(TCP 最大报文段长度)。将数据包分块,称为 TCP 段,分别传输,这样丢了哪个就重传哪个即可。
当传输层要将收到的数据包交给应用层时,就要根据端口号来知道给哪个应用。80 就是 Web 服务器,22 就是远程登录。浏览器每个标签页都对应一个独立的进程,都需要分配端口号
网络层
传输层只对应用层负责,只考虑怎么从本机到目标主机。在实际的网络传输中,有各种各样的路由器、交换机要考虑。从一个设备到另一个设备的数据传输,就需要网络层负责
网络层最常使用的协议是 IP 协议,IP 也会分片,当 IP 报文大小超过 MTU(一个网络包的最大长度,以太网一般是 1500 字节)就会分片。
在网络世界中,我们需要通信就需要知道跟谁通信;那么就需要地址这个定义,所以有 IP 地址(IPv4 和 IPv6),以 IPv4 为例,32 位的 IP 地址有两部分定义:
- 网络号:标识 IP 地址,属于哪个子网
- 主机号:标识子网下,IP 地址对应哪个主机
用子网掩码来标识前多少位表示子网,192.168.100.1/24,表示前 24 位是子网,后 8 位是主机
IP 协议还有另一个功能,那就是路由,决定一个数据包从路由器的哪个端口转发出去。
网络接口层
首先要搞清以太网的概念,以太网是在一个区域内,把区域内的设备连接起来,使他们能通信的技术。那么以太网关系的就不是 IP 地址,而是 MAC 地址。
网络接口层主要是为网络层提供链路级别的服务,负责在以太网,WIFI 这样的底层网络上发送原始数据包。每个设备的 MAC 地址是唯一的。
总结
层次 | 数据包名称 |
---|---|
网络接口层 | 帧(frame) |
网络层 | 包(packet) |
传输层 | 段(segment) |
应用层 | 报文(message) |
键入网址到网页显示,期间发生了什么
第一步封装 HTTP 请求报文
首先浏览器会解析 URL
没有路径名时,就代表访问目录下的默认文件。确定 Web 服务器和文件名后,就要生成 HTTP 消息
第二步 DNS 域名解析
在生成的 HTTP 报文中,是没有 IP 地址的,所以 TCP 报文段 4 元组并没有封装完成,所以要先通过 DNS 进行域名解析
DNS 服务器就记录了域名和其对应 IP 地址。
域名是按照句号分割的,每个句号分割出不同层次的界限,越右边层级越高。www.server.com.这个域名的层级就是
- 根域名服务器(.):IP 地址保存在所有的 DNS 服务器中,保证一定能找到
- 顶级域名服务器(.com)
- 权威域名服务器(server.com)
域名解析流程
- 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
- 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
- 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
- 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
- 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
- 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
- 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
- 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。
协议栈
协议栈属于操作系统提供的网络传输功能,用于辅助网络通信
- ICMP:告知网络包传送过程中产生的错误以及各种控制信息
- ARP:根据 IP 地址查询相应的以太网 MAC 地址
可靠传输 TCP
TCP 报文头部格式
- 源端口,目标端口:指明对应的应用
- 序号:解决乱序问题
- 确认号:解决丢包问题
- 状态位:SYN 是发起连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等,TCP 是面向连接的,要维护状态,记录连接信息。
- 窗口:用于流量控制,表明自己还可以处理的包大小
查看 TCP 连接状态:Linux 下通过 netstat -napt 命令查看
IP 定位
IP 头部格式
因为 HTTP 是经过 TCP 传输的,所以在 IP 包头的协议号,要填写为 06
(十六进制),表示协议为 TCP。
客户端是可以有多张网卡的,也就可以有多个 IP 地址和 MAC 地址。这时候要确定源 IP 地址就需要路由表规则(Linux 中使用 route -n 查看路由表)
举个例子,根据路由表,我们假设 Web 服务器的目标地址是 192.168.10.200
。
也就是说,对于路由表的每一个条目按顺序进行比较,直到比较出相同的子网条目。否则就发送给默认网关
两点传输 MAC 地址
一般在 TCP/IP 通信里,MAC 包头的协议类型只使用:
0800
: IP 协议0806
: ARP 协议
发送方的 MAC 地址获取就比较简单了,MAC 地址是在网卡生产时写入到 ROM 里的,只要将这个值读取出来写入到 MAC 头部就可以了。
接收方的 MAC 地址就有点复杂了,只要告诉以太网对方的 MAC 的地址,以太网就会帮我们把包发送过去,那么很显然这里应该填写对方的 MAC 地址。
所以先得搞清楚应该把包发给谁,这个只要查一下路由表就知道了。在路由表中找到相匹配的条目,然后把包发给 Gateway
列中的 IP 地址就可以了。
ARP 协议会在以太网中以广播的形式,对以太网所有的设备喊出:“这个 IP 地址是谁的?请把你的 MAC 地址告诉我”。
然后就会有人回答:“这个 IP 地址是我的,我的 MAC 地址是 XXXX”。
如果对方和自己处于同一个子网中,那么通过上面的操作就可以得到对方的 MAC 地址。然后,我们将这个 MAC 地址写入 MAC 头部,MAC 头部就完成了。
在查询到 MAC 地址后,ARP 协议会进行缓存(Linux 中用 arp -a 查看 ARP 缓存内容)
整个报文结构如图所示
交换机:端口不具备 MAC 地址
交换机属于二层网络设备,内部会将电信号转化成数字信号,然后跟自己内部的 MAC 地址表进行匹配,找出对应的转发端口(没找到就从所有端口转发出去,收到回应就记录 MAC 地址与对应端口)在网络中,如果设备收到一个数据包会先判断是不是发给自己的,不是的就会自动忽略
如果接收方 MAC 地址是一个广播地址,那么交换机会将包发送到除源端口之外的所有端口。以下两个属于广播地址:
- MAC 地址中的
FF:FF:FF:FF:FF:FF
- IP 地址中的
255.255.255.255
路由器:端口有自己的 IP 和 MAC 地址
路由器属于三层网络设备,收到一个数据包后会先判断 MAC 地址是不是自己端口的 MAC 地址,不是就丢弃。是就会根据 IP 地址查找对应转发端口(同样有默认网关)
互相扒皮 —— 服务器 与 客户端
- 数据包抵达服务器后,服务器会先扒开数据包的 MAC 头部,查看是否和服务器自己的 MAC 地址符合,符合就将包收起来。
- 接着继续扒开数据包的 IP 头,发现 IP 地址符合,根据 IP 头中协议项,知道自己上层是 TCP 协议。
- 于是,扒开 TCP 的头,里面有序列号,需要看一看这个序列包是不是我想要的,如果是就放入缓存中然后返回一个 ACK,如果不是就丢弃。TCP 头部里面还有端口号, HTTP 的服务器正在监听这个端口号。
- 于是,服务器自然就知道是 HTTP 进程想要这个包,于是就将包发给 HTTP 进程。
- 服务器的 HTTP 进程看到,原来这个请求是要访问一个页面,于是就把这个网页封装在 HTTP 响应报文里。
Linux 系统是如何收发网络包的?
OSI 七层网络模型
- 应用层,负责给应用程序提供统一的接口;
- 表示层,负责把数据转换成兼容另一个系统能识别的格式;
- 会话层,负责建立、管理和终止表示层实体之间的通信会话;
- 传输层,负责端到端的数据传输;
- 网络层,负责数据的路由、转发、分片;
- 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
- 物理层,负责在物理网络中传输数据帧;
Linux 网络协议栈
Linux 接收网络包的流程
网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址,也就是写入到 Ring Buffer ,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。
最简单的一种方式就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll
的方法来轮询数据。
因此,当有网络包到达时,会通过 DMA 技术,将网络包写入到指定的内存地址,接着网卡向 CPU 发起硬件中断,当 CPU 收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数。
硬件中断处理函数会做如下的事情:
- 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
- 接着,发起「软中断」,然后恢复刚才屏蔽的中断。
至此,硬件中断处理函数的工作就已经完成。
硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了。
内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。
ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理(就是不停地去掉各层头部,还原数据部分)。
Linux 发送网络包的流程
首先,应用程序会调用 Socket 发送数据包的接口,由于这个是系统调用,所以会从用户态陷入到内核态中的 Socket 层,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。
接下来,网络协议栈从 Socket 发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。
如果使用的是 TCP 传输协议发送数据,那么先拷贝一个新的 sk_buff 副本 ,这是因为 sk_buff 后续在调用网络层,最后到达网卡发送完成的时候,这个 sk_buff 会被释放掉。而 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 sk_buff 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 sk_buff 的一个拷贝,等收到 ACK 再真正删除。
接着,对 sk_buff 填充 TCP 头。这里提一下,sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。
你可能会好奇,为什么全部数据包只用一个结构体来描述呢?协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率。
于是,为了在层级之间传递数据时,不发生拷贝,只用 sk_buff 一个结构体来描述所有的网络包,那它是如何做到的呢?是通过调整 sk_buff 中 data
的指针,比如:
- 当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部。
- 当要发送报文时,创建 sk_buff 结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值来增加协议首部。
你可以从下面这张图看到,当发送报文时,data 指针的移动过程。
至此,传输层的工作也就都完成了。
然后交给网络层,在网络层里会做这些工作:选取路由(确认下一跳的 IP)、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片。处理完这些工作后会交给网络接口层处理。
网络接口层会通过 ARP 协议获得下一跳的 MAC 地址,然后对 sk_buff 填充帧头和帧尾,接着将 sk_buff 放到网卡的发送队列中。
这一些工作准备好后,会触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。
当数据发送完成以后,其实工作并没有结束,因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。
最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff 。
发送网络数据的时候,涉及几次内存拷贝操作?
第一次,调用发送数据的系统调用的时候,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。
第二次,在使用 TCP 传输协议的情况下,从传输层进入网络层的时候,每一个 sk_buff 都会被克隆一个新的副本出来。副本 sk_buff 会被送往网络层,等它发送完的时候就会释放掉,然后原始的 sk_buff 还保留在传输层,目的是为了实现 TCP 的可靠传输,等收到这个数据包的 ACK 时,才会释放原始的 sk_buff 。
第三次,当 IP 层发现 sk_buff 大于 MTU 时才需要进行。会再申请额外的 sk_buff,并将原来的 sk_buff 拷贝为多个小的 sk_buff。