上一节我们讲的进程间通信,其实是通过内核的数据结构完成的,主要用于在一台Linux上两个进程之间的通信。但是,一旦超出一台机器的范畴,我们就需要一种跨机器的通信机制。

一台机器将自己想要表达的内容,按照某种约定好的格式发送出去,当另外一台机器收到这些信息后,也能够按照约定好的格式解析出来,从而准确、可靠地获得发送方想要表达的内容。这种约定好的格式就是网络协议(Networking Protocol)。

我们将要讲的Socket通信以及相关的系统调用、内核机制,都是基于网络协议的,如果不了解网络协议的机制,解析Socket的过程中,你就会迷失方向,因此这一节,我们有必要做一个预习,先来大致讲一下网络协议的基本原理。

网络为什么要分层?

我们这里先构建一个相对简单的场景,之后几节内容,我们都要基于这个场景进行讲解。

我们假设这里就涉及三台机器。Linux服务器A和Linux服务器B处于不同的网段,通过中间的Linux服务器作为路由器进行转发。

说到网络协议,我们还需要简要介绍一下两种网络协议模型,一种是OSI的标准七层模型,一种是业界标准的TCP/IP模型。它们的对应关系如下图所示:

为什么网络要分层呢?因为网络环境过于复杂,不是一个能够集中控制的体系。全球数以亿记的服务器和设备各有各的体系,但是都可以通过同一套网络协议栈通过切分成多个层次和组合,来满足不同服务器和设备的通信需求。

我们这里简单介绍一下网络协议的几个层次。

我们从哪一个层次开始呢?从第三层,网络层开始,因为这一层有我们熟悉的IP地址。也因此,这一层我们也叫IP层。

我们通常看到的IP地址都是这个样子的:192.168.1.100/24。斜杠前面是IP地址,这个地址被点分隔为四个部分,每个部分8位,总共是32位。斜线后面24的意思是,32位中,前24位是网络号,后8位是主机号。

为什么要这样分呢?我们可以想象,虽然全世界组成一张大的互联网,美国的网站你也能够访问的,但是这个网络不是一整个的。你们小区有一个网络,你们公司也有一个网络,联通、移动、电信运营商也各有各的网络,所以一个大网络是被分成个小的网络。

那如何区分这些网络呢?这就是网络号的概念。一个网络里面会有多个设备,这些设备的网络号一样,主机号不一样。不信你可以观察一下你家里的手机、电视、电脑。

连接到网络上的每一个设备都至少有一个IP地址,用于定位这个设备。无论是近在咫尺的你旁边同学的电脑,还是远在天边的电商网站,都可以通过IP地址进行定位。因此,IP地址类似互联网上的邮寄地址,是有全局定位功能的

就算你要访问美国的一个地址,也可以从你身边的网络出发,通过不断的打听道儿,经过多个网络,最终到达目的地址,和快递员送包裹的过程差不多。打听道儿的协议也在第三层,称为路由协议(Routing protocol),将网络包从一个网络转发给另一个网络的设备称为路由器。

路由器和路由协议十分复杂,我们这里就不详细讲解了,感兴趣可以去看我写的另一个专栏“趣谈网络协议”里的相关文章

总而言之,第三层干的事情,就是网络包从一个起始的IP地址,沿着路由协议指的道儿,经过多个网络,通过多次路由器转发,到达目标IP地址。

从第三层,我们往下看,第二层是数据链路层。有时候我们简称为二层或者MAC层。所谓MAC,就是每个网卡都有的唯一的硬件地址(不绝对唯一,相对大概率唯一即可,类比UUID)。这虽然也是一个地址,但是这个地址是没有全局定位功能的。

就像给你送外卖的小哥,不可能根据手机尾号找到你家,但是手机尾号有本地定位功能的,只不过这个定位主要靠“吼”。外卖小哥到了你的楼层就开始大喊:“尾号xxxx的,你外卖到了!”

MAC地址的定位功能局限在一个网络里面,也即同一个网络号下的IP地址之间,可以通过MAC进行定位和通信。从IP地址获取MAC地址要通过ARP协议,是通过在本地发送广播包,也就是“吼”,获得的MAC地址。

由于同一个网络内的机器数量有限,通过MAC地址的好处就是简单。匹配上MAC地址就接收,匹配不上就不接收,没有什么所谓路由协议这样复杂的协议。当然坏处就是,MAC地址的作用范围不能出本地网络,所以一旦跨网络通信,虽然IP地址保持不变,但是MAC地址每经过一个路由器就要换一次。

我们看前面的图。服务器A发送网络包给服务器B,原IP地址始终是192.168.1.100,目标IP地址始终是192.168.2.100,但是在网络1里面,原MAC地址是MAC1,目标MAC地址是路由器的MAC2,路由器转发之后,原MAC地址是路由器的MAC3,目标MAC地址是MAC4。

所以第二层干的事情,就是网络包在本地网络中的服务器之间定位及通信的机制。

我们再往下看,第一层,物理层,这一层就是物理设备。例如连着电脑的网线,我们能连上的WiFi,这一层我们不打算进行分析。

从第三层往上看,第四层是传输层,这里面有两个著名的协议TCP和UDP。尤其是TCP,更是广泛使用,在IP层的代码逻辑中,仅仅负责数据从一个IP地址发送给另一个IP地址,丢包、乱序、重传、拥塞,这些IP层都不管。处理这些问题的代码逻辑写在了传输层的TCP协议里面。

我们常称,TCP是可靠传输协议,也是难为它了。因为从第一层到第三层都不可靠,网络包说丢就丢,是TCP这一层通过各种编号、重传等机制,让本来不可靠的网络对于更上层来讲,变得“看起来”可靠。哪有什么应用层岁月静好,只不过TCP层帮你负重前行。

传输层再往上就是应用层,例如咱们在浏览器里面输入的HTTP,Java服务端写的Servlet,都是这一层的。

二层到四层都是在Linux内核里面处理的,应用层例如浏览器、Nginx、Tomcat都是用户态的。内核里面对于网络包的处理是不区分应用的。

从四层再往上,就需要区分网络包发给哪个应用。在传输层的TCP和UDP协议里面,都有端口的概念,不同的应用监听不同的端口。例如,服务端Nginx监听80、Tomcat监听8080;再如客户端浏览器监听一个随机端口,FTP客户端监听另外一个随机端口。

应用层和内核互通的机制,就是通过Socket系统调用。所以经常有人会问,Socket属于哪一层,其实它哪一层都不属于,它属于操作系统的概念,而非网络协议分层的概念。只不过操作系统选择对于网络协议的实现模式是,二到四层的处理代码在内核里面,七层的处理代码让应用自己去做,两者需要跨内核态和用户态通信,就需要一个系统调用完成这个衔接,这就是Socket。

发送数据包

网络分完层之后,对于数据包的发送,就是层层封装的过程。

就像下面的图中展示的一样,在Linux服务器B上部署的服务端Nginx和Tomcat,都是通过Socket监听80和8080端口。这个时候,内核的数据结构就知道了。如果遇到发送到这两个端口的,就发送给这两个进程。

在Linux服务器A上的客户端,打开一个Firefox连接Ngnix。也是通过Socket,客户端会被分配一个随机端口12345。同理,打开一个Chrome连接Tomcat,同样通过Socket分配随机端口12346。

在客户端浏览器,我们将请求封装为HTTP协议,通过Socket发送到内核。内核的网络协议栈里面,在TCP层创建用于维护连接、序列号、重传、拥塞控制的数据结构,将HTTP包加上TCP头,发送给IP层,IP层加上IP头,发送给MAC层,MAC层加上MAC头,从硬件网卡发出去。

网络包会先到达网络1的交换机。我们常称交换机为二层设备,这是因为,交换机只会处理到第二层,然后它会将网络包的MAC头拿下来,发现目标MAC是在自己右面的网口,于是就从这个网口发出去。

网络包会到达中间的Linux路由器,它左面的网卡会收到网络包,发现MAC地址匹配,就交给IP层,在IP层根据IP头中的信息,在路由表中查找。下一跳在哪里,应该从哪个网口发出去?在这个例子中,最终会从右面的网口发出去。我们常把路由器称为三层设备,因为它只会处理到第三层。

从路由器右面的网口发出去的包会到网络2的交换机,还是会经历一次二层的处理,转发到交换机右面的网口。

最终网络包会被转发到Linux服务器B,它发现MAC地址匹配,就将MAC头取下来,交给上一层。IP层发现IP地址匹配,将IP头取下来,交给上一层。TCP层会根据TCP头中的序列号等信息,发现它是一个正确的网络包,就会将网络包缓存起来,等待应用层的读取。

应用层通过Socket监听某个端口,因而读取的时候,内核会根据TCP头中的端口号,将网络包发给相应的应用。

HTTP层的头和正文,是应用层来解析的。通过解析,应用层知道了客户端的请求,例如购买一个商品,还是请求一个网页。当应用层处理完HTTP的请求,会将结果仍然封装为HTTP的网络包,通过Socket接口,发送给内核。

内核会经过层层封装,从物理网口发送出去,经过网络2的交换机,Linux路由器到达网络1,经过网络1的交换机,到达Linux服务器A。在Linux服务器A上,经过层层解封装,通过socket接口,根据客户端的随机端口号,发送给客户端的应用程序,浏览器。于是浏览器就能够显示出一个绚丽多彩的页面了。

即便在如此简单的一个环境中,网络包的发送过程,竟然如此的复杂。不过这一章后面,我们还是会层层剖析每一层做的事情。

总结时刻

网络协议是一个大话题,如果你想了解网络协议的方方面面,欢迎你订阅我写的另一个专栏“趣谈网络协议”。这个专栏重点解析在这个网络通信过程中,发送端和接收端的操作系统都做了哪些事情,对于中间通路上的复杂的网络通信逻辑没有做深入解析。

如果只是为了掌握这一章的内容,这一节我们讲的网络协议的七个层次,你不必每一层的每一个协议都很清楚,只要记住TCP/UDP->IPv4->ARP这一条链就可以了,因为后面我们的分析都是重点分析这条链。

另外,前面那个简单的拓扑图中,网络包的封装、转发、解封装的过程,建议你多看几遍,了熟于心,因为接下来,我们就能从代码层面,看到这个过程。到时候,对应起来,你就比较容易理解。

了解了Socket的基本原理,下一篇文章,我们就来看一看在Linux操作系统里面Socket系统调用的接口是什么样的。

评论