图解网络基础篇
TCP/IP网络模型有哪几层?
TCP/IP网络通常是由上到下分成 4 层,分别是应用层,传输层,网络层和网络接口层。
- 应用层:负责向用户提供一组应用程序,比如HTTP、DNS、FTP等;
- 传输层:负责端到端的通信,比如TCP、UDP等;
- 网络层:负责网络包的封装、分片、路由、转发,比如IP、ICMP等;
- 网络接口层:负责网络包在物理网络中的传输,比如网络包的封帧、MAC寻址、差错检测,以及通过网卡传输网络帧等。
不过,我们常说的七层和四层负载均衡,是用OSI网络模型来描述的,七层对应的是应用层,四层对应的传输层。
键入网址到网页显示,期间发生了什么?
- 解析URL:分析URL所需要使用的传输协议和请求的资源路径。如果输入的URL中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查URL中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。
- 缓存判断:浏览器缓存→系统缓存(hosts文件)→路由器缓存→ISP的DNS缓存,如果其中某个缓存存在,直接返回服务器的IP地址。
- DNS解析:如果缓存未命中,浏览器向本地DNS服务器发起请求,最终可能通过根域名服务器、顶级域名服务器(.com)、权威域名服务器逐级查询,直到获取目标域名的IP地址。
- 获取MAC地址:当浏览器得到IP地址后,数据传输还需要知道目的主机MAC地址,因为应用层下发数据给传输层,TCP协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的IP地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的MAC地址,本机的MAC地址作为源MAC地址,目的MAC地址需要分情况处理。通过将IP地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用ARP协议获取到目的主机的MAC地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过ARP协议来获取网关的MAC地址,此时目的主机的MAC地址应该为网关的地址。
- 建立TCP连接:主机将使用目标IP地址和目标MAC地址发送一个TCPSYN包,请求建立一个TCP连接,然后交给路由器转发,等路由器转到目标服务器后,服务器回复一个SYN-ACK包,确认连接请求。然后,主机发送一个ACK包,确认已收到服务器的确认,然后TCP连接建立完成。
- HTTPS的TLS 四次握手:如果使用的是HTTPS协议,在通信前还存在TLS的四次握手。
- 发送HTTP请求:连接建立后,浏览器会向服务器发送HTTP请求。请求中包含了用户需要获取的资源的信息,例如网页的URL、请求方法(GET、POST等)等。
- 服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进行相应的处理。例如,如果是请求网页,服务器会读取相应的网页文件,并生成HTTP响应。
Linux系统是如何收发网络包的?
Linux网络协议栈
我们可以把自己的身体比作应用层中的数据,打底衣服比作传输层中的TCP头,外套比作网络层中IP头,帽子和鞋子分别比作网络接口层的帧头和帧尾。
在冬天这个季节,当我们要从家里出去玩的时候,自然要先穿个打底衣服,再套上保暖外套,最后穿上帽子和鞋子才出门,这个过程就好像我们把TCP协议通信的网络包发出去的时候,会把应用层的数据按照网络协议栈层层封装和处理。
你从下面这张图可以看到,应用层数据在每一层的封装格式。
其中:
- 传输层,给应用数据前面增加了TCP头;
- 网络层,给TCP数据包前面增加了IP头;
- 网络接口层,给IP数据包前后分别增加了帧头和帧尾。
这些新增的头部和尾部,都有各自的作用,也都是按照特定的协议格式填充,这每一层都增加了各自的协议头,那自然网络包的大小就增大了,但物理链路并不能传输任意大小的数据包,所以在以太网中,规定了最大传输单元(MTU)是1500
字节,也就是规定了单次传输的最大IP包大小。
当网络包超过MTU的大小,就会在网络层分片,以确保分片后的IP包不会超过MTU大小,如果MTU越小,需要的分包就越多,那么网络吞吐能力就越差,相反的,如果MTU越大,需要的分包就越少,那么网络吞吐能力就越好。
知道了TCP/IP 网络模型,以及网络包的封装原理后,那么Linux 网络协议栈的样子,你想必猜到了大概,它其实就类似于TCP/IP的四层结构:
从上图的网络协议栈,可以看到:
- 应用程序需要通过系统调用,来跟Socket层进行数据交互;
- Socket层的下面就是传输层、网络层和网络接口层。
- 最下面的一层,则是网卡驱动程序和硬件网卡设备。
Linux接收网络包的流程
网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过DMA技术,将网络包写入到指定的内存地址,也就是写入到Ring Buffer,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。
那应该怎么告诉操作系统这个网络包已经到达了呢?
最简单的方法就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。
但是在高性能网络场景下,网络包的数量会非常多,这么频繁地触发中断,CPU则会一直没完没了的处理中断,而导致其他任务可能无法继续前进,从而影响系统的整体效率。
所以为了解决频繁中断带来的性能开销,Linux内核在2.6版本中引入了NAPI机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后poll
的方法来轮询数据。
因此,当有网络包到达时,会通过DMA技术,将网络包写入到指定的内存地址,接着网卡向CPU发起硬件中断,当CPU收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数。
硬件中断处理函数会做如下的事情:
- 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
- 接着,发起「软中断」,然后恢复刚才屏蔽的中断。
至此,硬件中断处理函数的工作就已经完成。
硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了。
软中断的处理
内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。
ksoftirqd线程会从Ring Buffer中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。
网络协议栈
- 首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法这丢弃,合法则会找出该网络包的上层协议的类型,比如是IPv4,还是IPv6,接着再去调帧头和帧尾,然后交给网络层。
- 到了网络层,则取出IP包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给机后,就会从 IP 头里看看上一层协议的类型是TCP还是UDP,接着去掉IP头,然后交给传输层。
- 传输层取出TCP头或UDP头,根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识,找出对应的Socket,并把数据放到Socket的接收缓冲区。
- 最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。
至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。
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。
总结
电脑与电脑之间通常都是通过网卡、交换机、路由器等网络设备连接到一起,那由于网络设备的异构性,国际标准化组织定义了一个七层的OSI网络模型,但是这个模型由于比较复杂,实际应用中并没有采用,而是采用了更为简化的TCP/IP模型,Linux网络协议栈就是按照了该模型来实现的。
TCP/IP 模型主要分为应用层、传输层、网络层、网络接口层四层,每一层负责的职责都不同,这也是Linux网络协议栈主要构成部分。
当应用程序通过Socket接口发送数据包,数据包会被网络协议栈从上到下进行逐层处理后,才会被送到网卡队列中,随后由网卡将网络包发送出去。
而在接收网络包时,同样也要先经过网络协议栈从下到上的逐层处理,最后才会被送到应用程序。