文章目录
前言
为什么要有 TCP/IP 网络模型?
对于同一台设备上的进程间通信,有很多种方式,比如有管道、消息队列、共享内存、信号等方式,而对于不同设备上的进程间通信,就需要网络通信,而设备是多样性的,所以要兼容多种多样的设备,就协商出了一套通用的网络协议。
这个网络协议是分层的,每一层都有各自的作用和职责,接下来就根据「 TCP/IP 网络模型」分别对每一层进行介绍。
一、TCP/IP 网络模型有哪几层
1.1 应用层
最上层的,也是我们能直接接触到的就是应用层(Application Layer),我们电脑或手机使用的应用软件都是在应用层实现。那么,当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。所以,应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。
应用层是不用去关心数据是如何传输的,就类似于,我们寄快递的时候,只需要把包裹交给快递员,由他负责运输快递,我们不需要关心快递是如何被运输的。
应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。
1.2 传输层
应用层的数据包会传给传输层,传输层(Transport Layer)是为应用层提供网络支持的。
在传输层会有两个传输协议,分别是 TCP 和 UDP。
TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。
UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。(UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 传输协议,也不是一件简单的事情。)
应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 TCP 段(TCP Segment)。
当设备作为接收方时,传输层则要负责把数据包传给应用,但是一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是端口。
比如 80 端口通常是 Web 服务器用的,22 端口通常是远程登录服务器用的。而对于浏览器(客户端)中的每个标签栏都是一个独立的进程,操作系统会为这些进程分配临时的端口号。
由于传输层的报文中会携带端口号,因此接收方可以识别出该报文是发送给哪个应用。
1.3 网络层
传输层可能大家刚接触的时候,会认为它负责将数据从一个设备传输到另一个设备,事实上传输层并不负责传输。
实际场景中的网络环节是错综复杂的,中间有各种各样的线路和分叉路口,如果一个设备的数据要传输给另一个设备,就需要在各种各样的路径和节点进行选择,而传输层的设计理念是简单、高效、专注,如果传输层还负责这一块功能就有点违背设计原则了。
也就是说,我们不希望传输层协议处理太多的事情,只需要服务好应用即可,让其作为应用间数据传输的媒介,帮助实现应用到应用的通信,而实际的传输功能就交给下一层,也就是网络层(Internet Layer)。
网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文。
网络层负责将数据从一个设备传输到另一个设备,世界上那么多设备,又该如何找到对方呢?因此,网络层需要有区分设备的编号。
我们一般用 IP 地址给设备进行编号,对于 IPv4 协议, IP 地址共 32 位,分成了四段(比如,192.168.100.1),每段是 8 位。只有一个单纯的 IP 地址虽然做到了区分设备,但是寻址起来就特别麻烦,全世界那么多台设备,难道一个一个去匹配?这显然不科学。
因此,需要将 IP 地址分成两种意义:
一个是网络号,负责标识该 IP 地址是属于哪个「子网」的;
一个是主机号,负责标识同一「子网」下的不同主机;
怎么分的呢?这需要配合子网掩码才能算出 IP地址 的网络号和主机号。
举个例子,比如 10.100.122.2 /24,后面的/24表示就是 255.255.255.0 子网掩码,则可以得
网络号:0.100.122.0
主机号:将 255.255.255.0 取反后与IP地址10.100.122.2进行进行按位与运算,就可以得到主机号。
1.4 网络接口层
生成了 IP 头部之后,接下来要交给网络接口层(Link Layer)在 IP 头部的前面加上 MAC 头部,并封装成数据帧(Data frame)发送到网络上。
IP 头部中的接收方 IP 地址表示网络包的目的地,通过这个地址我们就可以判断要将包发到哪里,但在以太网的世界中,这个思路是行不通的。
什么是以太网呢?电脑上的以太网接口,Wi-Fi接口,以太网交换机、路由器上的千兆,万兆以太网口,还有网线,它们都是以太网的组成部分。以太网就是一种在「局域网」内,把附近的设备连接起来,使它们之间可以进行通讯的技术。
以太网在判断网络包目的地时和 IP 的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而 MAC 头部就是干这个用的,所以,在以太网进行通讯要用到 MAC 地址。
MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们可以通过 ARP 协议获取对方的 MAC 地址。
所以说,网络接口层主要为网络层提供「链路级别」传输的服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标识网络上的设备。
总结
综上所述,TCP/IP 网络通常是由上到下分成 4 层,分别是应用层,传输层,网络层和网络接口层。
二、使用步骤
1.2 键入网址到网页显示原理
(1)解析URL,获得服务器和文件名称
(2)请求服务器里的文件资源,发出http请求消息
(3)查询服务器域名(DNS)对应的IP地址
(4)通过 DNS 获取到 IP 后,就可以把 HTTP 的传输工作交给操作系统中的协议栈。
(5)在 HTTP 传输数据之前,首先需要 TCP 建立连接,TCP 连接的建立,通常称为三次握手。
(6)TCP 模块在执行连接、收发、断开等各阶段操作时,都需要委托IP模块将数据封装成网络包发送给通信对象。
(7)生成了 IP 头部之后,接下来网络包还需要在 IP 头部的前面加上 MAC 头部。
(8)网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。因此,我们需要将数字信息转换为电信号,才能在网线上传输,也就是说,这才是真正的数据发送过程(网卡)。
(9)首先,电信号到达网线接口,交换机里的模块进行接收,接下来交换机里的模块将电信号转换为数字信号。
(10)网络包经过交换机之后,现在到达了路由器,并在此被转发到下一个路由器或目标设备。这一步转发的工作原理和交换机类似,也是通过查表判断包转发的目标。
(11)客户端收到了服务器的响应数据包后,同样也非常的高兴,客户能拆快递了!于是,客户端开始扒皮,把收到的数据包的皮扒剩 HTTP 响应报文后,交给浏览器去渲染页面,一份特别的数据包快递,就这样显示出来了!最后,客户端要离开了,向服务器发起了 TCP 四次挥手,至此双方的连接就断开了。
(HTTP => DNS => 协议栈 => TCP => IP => MAC => 网卡 => 交换机 => 路由器 => 服务器与客户端)
三、Linux 系统是如何收发网络包的?
3.1 网络模型
为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了开放式系统互联通信参考模型(Open System Interconnection Reference Model),
也就是 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。
每一层负责的职能都不同,如下:
应用层,负责给应用程序提供统一的接口;
表示层,负责把数据转换成兼容另一个系统能识别的格式;
会话层,负责建立、管理和终止表示层实体之间的通信会话;
传输层,负责端到端的数据传输;
网络层,负责数据的路由、转发、分片;
数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
物理层,负责在物理网络中传输数据帧;
由于 OSI 模型实在太复杂,提出的也只是概念理论上的分层,并没有提供具体的实现方案。
事实上,我们比较常见,也比较实用的是四层模型,即 TCP/IP 网络模型,Linux 系统正是按照这套网络模型来实现网络协议栈的。
TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层,每一层负责的职能如下:
应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
传输层,负责端到端的通信,比如 TCP、UDP 等;
网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;
TCP/IP 网络模型相比 OSI 网络模型简化了不少,也更加易记,它们之间的关系如下图:
3.2 Linux 网络协议栈
我们可以把自己的身体比作应用层中的数据,打底衣服比作传输层中的 TCP 头,外套比作网络层中 IP 头,帽子和鞋子分别比作网络接口层的帧头和帧尾。
在冬天这个季节,当我们要从家里出去玩的时候,自然要先穿个打底衣服,再套上保暖外套,最后穿上帽子和鞋子才出门,这个过程就好像我们把 TCP 协议通信的网络包发出去的时候,会把应用层的数据按照网络协议栈层层封装和处理。
传输层,给应用数据前面增加了 TCP 头;
网络层,给 TCP 数据包前面增加了 IP 头;
网络接口层,给 IP 数据包前后分别增加了帧头和帧尾;
这些新增的头部和尾部,都有各自的作用,也都是按照特定的协议格式填充,这每一层都增加了各自的协议头,那自然网络包的大小就增大了,但物理链路并不能传输任意大小的数据包,所以在以太网中,规定了最大传输单元(MTU)是 1500 字节,也就是规定了单次传输的最大 IP 包大小。
当网络包超过 MTU 的大小,就会在网络层分片,以确保分片后的 IP 包不会超过 MTU 大小,如果 MTU 越小,需要的分包就越多,那么网络吞吐能力就越差,相反的,如果 MTU 越大,需要的分包就越少,那么网络吞吐能力就越好。
知道了 TCP/IP 网络模型,以及网络包的封装原理后,那么 Linux 网络协议栈的样子,你想必猜到了大概,它其实就类似于 TCP/IP 的四层结构:
从上图的的网络协议栈,你可以看到:
应用程序需要通过系统调用,来跟 Socket 层进行数据交互;
Socket 层的下面就是传输层、网络层和网络接口层;
最下面的一层,则是网卡驱动程序和硬件网卡设备;
3.3 Linux 接收网络包的流程
网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址,也就是写入到 Ring Buffer ,这个是一个环形缓冲区,接着就会告诉操作系统这个网络包已经到达。
DMA补充:DMA(Direct Memory Access) 功能,它可以使得设备在 CPU 不参与的情况下,能够自行完成把设备 I/O 数据放入到内存。这部分知识位于操作系统——设备管理。
那应该怎么告诉操作系统这个网络包已经到达了呢?
最简单的一种方式就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。
但是,这存在一个问题,在高性能网络场景下,网络包的数量会非常多,那么就会触发非常多的中断,要知道当 CPU 收到了中断,就会停下手里的事情,而去处理这些网络包,处理完毕后,才会回去继续其他事情,那么频繁地触发中断,则会导致 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 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。
至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。
3.4 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 指针的移动过程。
3.5 发送网络数据的时候,涉及几次内存拷贝操作?
第一次,调用发送数据的系统调用的时候,内核会申请一个内核态的 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。
四、Socket套接字
服务器端会产生一个syn队列和accept队列,存储五元组
每一个Socket由:
fd(文件描述符)+协议栈(远程ip,远程端口,本地ip,本地端口,proto)五元组组成
总结
电脑与电脑之间通常都是通过网卡、交换机、路由器等网络设备连接到一起,那由于网络设备的异构性,国际标准化组织定义了一个七层的 OSI 网络模型,但是这个模型由于比较复杂,实际应用中并没有采用,而是采用了更为简化的 TCP/IP 模型,Linux 网络协议栈就是按照了该模型来实现的。
TCP/IP 模型主要分为应用层、传输层、网络层、网络接口层四层,每一层负责的职责都不同,这也是 Linux 网络协议栈主要构成部分。
当应用程序通过 Socket 接口发送数据包,数据包会被网络协议栈从上到下进行逐层处理后,才会被送到网卡队列中,随后由网卡将网络包发送出去。
而在接收网络包时,同样也要先经过网络协议栈从下到上的逐层处理,最后才会被送到应用程序。