计算机网络基础概念

2024-05-20

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 字节)就会分片。

img

在网络世界中,我们需要通信就需要知道跟谁通信;那么就需要地址这个定义,所以有 IP 地址(IPv4 和 IPv6),以 IPv4 为例,32 位的 IP 地址有两部分定义:

  • 网络号:标识 IP 地址,属于哪个子网
  • 主机号:标识子网下,IP 地址对应哪个主机

用子网掩码来标识前多少位表示子网,192.168.100.1/24,表示前 24 位是子网,后 8 位是主机

IP 协议还有另一个功能,那就是路由,决定一个数据包从路由器的哪个端口转发出去。

网络接口层

首先要搞清以太网的概念,以太网是在一个区域内,把区域内的设备连接起来,使他们能通信的技术。那么以太网关系的就不是 IP 地址,而是 MAC 地址。

网络接口层主要是为网络层提供链路级别的服务,负责在以太网,WIFI 这样的底层网络上发送原始数据包。每个设备的 MAC 地址是唯一的。

总结

img

img

层次 数据包名称
网络接口层 帧(frame)
网络层 包(packet)
传输层 段(segment)
应用层 报文(message)

键入网址到网页显示,期间发生了什么

第一步封装 HTTP 请求报文

首先浏览器会解析 URL

URL 解析

没有路径名时,就代表访问目录下的默认文件。确定 Web 服务器和文件名后,就要生成 HTTP 消息

HTTP 的消息格式

第二步 DNS 域名解析

在生成的 HTTP 报文中,是没有 IP 地址的,所以 TCP 报文段 4 元组并没有封装完成,所以要先通过 DNS 进行域名解析

DNS 服务器就记录了域名和其对应 IP 地址。

域名是按照句号分割的,每个句号分割出不同层次的界限,越右边层级越高。www.server.com.这个域名的层级就是

  • 根域名服务器(.):IP 地址保存在所有的 DNS 服务器中,保证一定能找到
  • 顶级域名服务器(.com)
  • 权威域名服务器(server.com)

DNS 树状结构

域名解析流程

  1. 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
  2. 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
  3. 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
  4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
  5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
  6. 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
  7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  8. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。

域名解析的工作流程

协议栈

img

协议栈属于操作系统提供的网络传输功能,用于辅助网络通信

  • ICMP:告知网络包传送过程中产生的错误以及各种控制信息
  • ARP:根据 IP 地址查询相应的以太网 MAC 地址

可靠传输 TCP

TCP 报文头部格式

TCP 包头格式

  • 源端口,目标端口:指明对应的应用
  • 序号:解决乱序问题
  • 确认号:解决丢包问题
  • 状态位:SYN 是发起连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等,TCP 是面向连接的,要维护状态,记录连接信息。
  • 窗口:用于流量控制,表明自己还可以处理的包大小

查看 TCP 连接状态:Linux 下通过 netstat -napt 命令查看

IP 定位

IP 头部格式

IP 包头格式

因为 HTTP 是经过 TCP 传输的,所以在 IP 包头的协议号,要填写为 06(十六进制),表示协议为 TCP。

客户端是可以有多张网卡的,也就可以有多个 IP 地址和 MAC 地址。这时候要确定源 IP 地址就需要路由表规则(Linux 中使用 route -n 查看路由表)

举个例子,根据路由表,我们假设 Web 服务器的目标地址是 192.168.10.200

路由规则判断

也就是说,对于路由表的每一个条目按顺序进行比较,直到比较出相同的子网条目。否则就发送给默认网关

两点传输 MAC 地址

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 地址表

如果接收方 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 网络协议栈

img

Linux 接收网络包的流程

网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址,也就是写入到 Ring Buffer ,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。

最简单的一种方式就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll 的方法来轮询数据。

因此,当有网络包到达时,会通过 DMA 技术,将网络包写入到指定的内存地址,接着网卡向 CPU 发起硬件中断,当 CPU 收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数。

硬件中断处理函数会做如下的事情:

  • 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
  • 接着,发起「软中断」,然后恢复刚才屏蔽的中断。

至此,硬件中断处理函数的工作就已经完成。

硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了。

内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。

ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理(就是不停地去掉各层头部,还原数据部分)。

img

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 指针的移动过程。

img

至此,传输层的工作也就都完成了。

然后交给网络层,在网络层里会做这些工作:选取路由(确认下一跳的 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。