详细剖析分布式微服务架构下网络通信的底层实现原理(图解)
在分布式架构中,网络通信是底层基础,没有网络,也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作,共同完成一件事情。
同样,在大规模的系统架构中,应用吞吐量上不去、网络存在通信延迟、我们首先考虑的都是网络问题,因此网络的重要性不言而喻。
作为现代化应用型程序员,要开发一个网络通信的应用,是非常简单的。不仅仅有成熟的api,还有非常方便的通信框架。
可能大家已经忘记了网络通信的重要性,本篇文章会详细分析网络通信的底层原理!!
1.1 理解通信的本质
如图1-1所示,当我们通过浏览器访问一个网址时,一段时间后该网址会渲染出访问的内容,这个过程是怎么实现的呢?
<center>图1-1</center>
我想站在今天,在做的同学都知道,它是基于http协议来实现数据通信的,这里有两个字很重要,就是“协议”。
两个计算机之间要实现数据通信,必须遵循同一种协议,否则,就像一个中国人和一个外国人交流时,一个讲英语另一个讲解中文,肯定是无法正常交流。在计算机中,协议非常常见。
1.1.1 协议的组成
我们写的Java代码,计算机能够理解并且执行,原因是人和计算机之间遵循了同一种语言,那就是Java,如图1-2所示,.java文件最终编译成.class文件这个过程,也同样涉及到协议。
<center>图1-2 java编译过程</center>
所以,在计算机中,协议是指大家需要共同遵循的规则,只有实现统一规则之后,才能实现不同节点之间的数据通信,从而让计算机的应用更加强大。
组成一个协议,需要具备三个要素:
- 语法,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。
- 语义,就是这一段内容要代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。
- 时序,就是先干啥,后干啥。例如,可以先加上某个数值,然后再减去某个数值。
1.1.2 http协议
理解了协议的作用,那协议是长什么样的呢?
那么再来看图1-3的场景,人们通过浏览器访问网站,用到了http协议。
<center>图1-3 http协议</center>
http协议包含包含几个部分:
- http请求组成
- 状态行
- 请求头
- 消息主体
- http响应组成
- 状态行
- 响应头
- 响应正文
Http响应报文如图1-4所示,那么这个协议的三要素分别是:
- 语法: http协议的消息体由状态、头部、内容组成。
- 语义: 比如状态,200表示成功,404表示请求路径不存在等,通信双方必须遵循该语义。
- 时序: 组成消息体的三部分的排列顺序,必须要有request,才会产生response。
而浏览器按照http协议做好了相关的处理后,才能让大家通过网址访问网络上的各种信息。
<center>图1-4</center>
1.1.3 常用的网络协议
DNS协议、Http协议、SSH协议、TCP协议、FTP协议等,这些都是大家比较常用的协议类型。无论哪种协议,本质上仍然是由协议的三要素组成,只是应用场景不同。
DNS、HTTP、HTTPS 所在的层我们称为应用层。经过应用层封装后,浏览器会将应用层的包交给下一层去完成,通过 socket 编程来实现。下一层是传输层。传输层有两种协议,一种是无连接的协议 UDP,一种是面向连接的协议 TCP。对于通信可靠性要求的场景来说,往往使用 TCP 协议。所谓的面向连接就是,TCP 会保证这个包能够到达目的地。如果不能到达,就会重新发送,直至到达。
1.3 TCP/IP通信原理分析
一次网络通信到底是怎么完成的呢?
涉及到网络通信,那我们一定会提到一个网络模型的概念,如图1-5所示。表示TCP/IP的四层概念模型和OSI七层网络模型,它是一种概念模型,由国际标准化组织提出来的,试图让全世界范围内的计算机能基于该网络标准实现互联。
<center>图1-5</center>
网络模型为什么要分层呢?其实从我们现在的业务分层架构中就不难发现,任何系统一旦变得复杂,就都会采用分层设计。它的主要好处是
- 实现高内聚低耦合
- 每一层有自己单一的职责
- 提高可复用性和降低维护成本
1.2.1 http通信过程的发送数据包
由于我们的课程并不是专门来讲网络,所以只是提及一下网络分层模型,为了让大家更简单的理解网络分层模型的工作原理,我们仍然以一次网络通信的数据包传输为例进行分析,如图1-6所示。
<center>图1-6</center>
图1-6的工作流程描述如下:
-
假设我们要登录某一个网站,此时基于Http协议会构建一个http协议报文,这个报文中按照http协议的规范组装,其中包括要传输的用户名和密码。这个是属于应用层协议。
-
经过应用层封装后,浏览器会把应用层的包交给TCP/IP四层模型中的下一层,也就是传输层来完成,传输层有两种协议:
- TCP协议,可靠的通信协议,该协议会确保数据包能达到目的地
- UDP协议,不可靠通信协议,可能会存在数据丢失
在http通信中使用了TCP协议,TCP协议会有两个端口,一个是浏览器监听的端口,一个是目标服务器进程的端口。操作系统会根据端口来判断这个数据包应该分发给那个进程。
-
传输层封装完成后,该数据包会技术交给网络层来处理,网络层协议是IP协议,IP协议中会包含源IP地址(也就是客户端及其的IP)和目标服务器的IP地址。
-
操作系统知道了目标IP地址后,就开始根据这个IP来寻找目标机器,而目标服务器一定是部署在不同的地方,这种跨网络节点的访问,需要经过网关(所谓网关就是一个网络到另外一个网络的关口)。
所以数据包首先需要先通过自己当前所在网络的网关出去,然后访问到目标服务器,但是在数据包传输到目标服务器之前,需要再组装MAC头信息。
Mac头包含本地的Mac地址和目标服务器的Mac地址,这个MAC地址怎么获得的呢?
-
获取本机MAC地址的方法是,操作系统会发送一个广播消息询问网关地址(192.168.1.1)是谁?收到该广播消息的网关会回应一个MAC地址。这个广播消息是基于ARP协议实现的(这个协议简单来说就是已知目标机器的ip,需要获得目标机器的mac地址。(发送一个广播消息,这个ip是谁的,请来认领。认领ip的机器会发送一个mac地址的响应))。
为了避免每次都用 ARP 请求,机器本地也会进行 ARP 缓存。当然机器会不断地上线下线,IP 也可能会变,所以 ARP 的 MAC 地址缓存过一段时间就会过期。
-
获取远程机器的MAC地址的方法也同样是基于ARP协议实现的。
-
完成MAC地址组装后,一个完整的数据包就构成了。这个时候会把这个数据包给到网卡,网卡再把这个数据包发出去,由于这个数据包中包含MAC地址,因此它能够到达网关进行传输。网关收到包之后,会根据路由信息,判断下一步应该怎么走。网关往往是一个路由器,到某个 IP 地址应该怎么走,这个叫作路由表。
1.2.2 http通信过程中的接收数据包
当数据包发送到网关后,会根据网关的路由信息判断该数据包要传输到那个网段上。数据从客户端发送到目标服务器,可能会经过多个网关,所以数据包根据网关路由进入到下一个网关后,继续根据下一个网关的MAC地址寻找下下一个网关,直到到达目标网络服务器上。
这个时候服务器收到包之后,最后一个网关知道这个网络包就是要去当前局域网的,于是拿着目标IP通过ARP协议大喊一声这是谁? 目标服务器就会给网关回复一个MAC地址。 然后网络包在最后那个网关修改目标的MAC地址,通过这个MAC地址,网络包找到了目标服务器。
当目标服务器和MAC地址对上后,开始取出MAC头信息,接着把数据包发送给操作系统的网络层。网络层会取出IP头信息,IP头里面会写上一层封装的是TCP协议,于是交给传输层来处理,实现过程如图1-7所示。
在这一层中,对于收到的每个数据包都会有一个回复,表示服务器端已经收到了该数据包。如果过一段时间客户端没有收到该确认包,发送端的 TCP 层会重新发送这个包,还是上面的过程,直到最终收到回复。
这个重试是TCP协议层来实现的,不需要我们应用来主动发起。
<center>图1-7</center>
为什么有了MAC层还要走IP层呢?
之前我们提到,mac地址是唯一的,那理论上,在任何两个设备之间,我应该都可以通过mac地址发送数据,为什么还需要ip地址?
mac地址就好像个人的身份证号,人的身份证号和人户口所在的城市,出生的日期有关,但是和人所在的位置没有关系,人是会移动的,知道一个人的身份证号,并不能找到它这个人,mac地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的mac,并不能在网络中将数据发送给它,除非它和发送方的在同一个网络内。
所以要实现机器之间的通信,我们还需要有ip地址的概念,ip地址表达的是当前机器在网络中的位置,类似于城市名+道路号+门牌号的概念。通过ip层的寻址,我们能知道按何种路径在全世界任意两台Internet上的的机器间传输数据。
1.4 详解TCP可靠性通信特性
我们知道,TCP协议是属于可靠性通信协议,它能够确保数据包不被丢失。首先我们先了解一下TCP的三次握手和四次挥手。
1.4.1 TCP的三次握手
两个节点需要进行数据通信,首先得先建立连接。而在建立连接时,TCP采用了三次握手来实现连接建立。如图1-8所示。
<center>图1-8</center>
第一次握手(SYN=1, seq=x)
客户端发送一个 TCP的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,**保存在包头的序列号(Sequence Number)**字段里。发送完毕后,客户端进入 SYN_SEND 状态。
第二次握手(SYN=1, ACK=1, seq=y, ACK num=x+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
第三次握手(ACK=1,ACK num=y+1)
客户端再次发送确认包(ACK),SYN标志位为0,ACK标志位为1,并且把服务器发来 ACK的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN发完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP握手结束。
1.4.2 TCP为什么是三次握手?
TCP是全双工,如果没有第三次的握手,服务端不能确认客户端是否ready,不知道什么时候可以往客户端发数据包。三次的握手刚好两边都互相确认对方已经ready。
我们假设网络的不可靠性,
A发起一个连接,当发起一个请求没有得到反馈的时候,会有很多可能性,比如请求包丢失,或者超时,或者B没有响应
由于A不能确认结果,于是再发,当有一个请求包到了B之后,A并不知道这个数据包已经到了B,所以可能还会重试。
所以B收到请求之后,知道了A的存在并且要和我建立连接,这个时候B会发送ack给到A,告诉A我收到了请求包。
对于B来说,这个应答包也是一个网络通信,我怎么知道能不能到达A呢?所以这个时候B不能很主观的认为连接已经建立好了,还需要等到A再次发送应答包来确认。
1.4.3 TCP的四次挥手
如图1-9所示,TCP的连接断开,会通过所谓的四次挥手完成。
四次挥手表示TCP断开连接的时候,需要客户端和服务端总共发送4个包以确认连接的断开;客户端或服务器均可主动发起挥手动作(因为TCP是一个全双工协议),在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。
<center>图1-9</center>
上述交互过程如下:
-
断开的时候,我们可以看到,当A客户端说说“我要断开连接”,就进入 FIN_WAIT_1 的状态。
-
B 服务端收到“我要断开连接”的消息后,发送"知道了"给到A客户端,就进入 CLOSE_WAIT 的状态。
-
A 收到“B 说知道了”,就进入 FIN_WAIT_2 的状态,如果这个时候 B 服务器挂掉了,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
-
如果 B 服务器正常,则发送了“B 要关闭连接”的请求到达 A 时,A 发送“知道 B 也要关闭连接”的 ACK 后,从 FIN_WAIT_2 状态结束。
-
按说这个时候 A 可以退出了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个“B 要关闭连接”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。
这个等待实现是2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃(此时A直接进入CLOSE状态)。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。
第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
第三次挥手(FIN=1,seq=w)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN置为1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
第四次挥手(ACK=1,ACKnum=w+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:三次握手是因为因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET(因为可能还有消息没处理完),所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
1.4.4 TCP协议的报文传输
连接建立好之后,就开始进行数据包的传输了。那TCP作为一个可靠的通信协议,如何保证消息传输的可靠性呢?
TCP采用了消息确认的方式来保证数据报文传输的安全性,也就是说客户端发送了数据包到服务端后,服务端会返回一个确认消息给到客户端,如果客户端没有收到确认包,则会重新再发送。
为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)
如图1-10所示,为了记录所有发送的包和接收的包,TCP协议在发送端和接收端分别拿会有发送缓冲区和接收缓冲区,TCP的全双工的工作模式及TCP的滑动窗口就是依赖于这两个独立的Buffer和该Buffer的填充状态。
接收缓冲区把数据缓存到内核,若应用进程一直没有调用Socket的read方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket的内核接收缓冲区。
read所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的Buffer里。进程调用Socket的send发送数据的时候,一般情况下是将数据从应用层用户的Buffer里复制到Socket的内核发送缓冲区,然后send就会在上层返回。换句话说,send返回时,数据不一定会被发送到对端。
<center>图1-10</center>
发送端/接收端的缓冲区中是按照包的 ID 一个个排列,根据处理的情况分成四个部分。
- 第一部分:发送了并且已经确认的。
- 第二部分:发送了并且尚未确认的。需要等待确认后,才能移除。
- 第三部分:没有发送,但是已经等待发送的。
- 第四部分:没有发送,并且暂时还不会发送的。
这里的第三部分和第四部分之所以做一个区分,其实是因为TCP采用做了流量控制,这里采用了滑动窗口的方式来实现流量整形,避免出现数据拥堵的情况。
<center>图1-11</center>
为了更好的理解数据包的通信过程,我们通过下面这个网址来演示一下
1.4.5 滑动窗口协议
上述地址中动画演示的部分,其实就是数据包发送和确认机制,同时还涉及到互动窗口协议。
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题;发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口
发送窗口
就是发送端允许连续发送的幀的序号表。
发送端可以不等待应答而连续发送的最大幀数称为发送窗口的尺寸。
接收窗口
接收方允许接收的幀的序号表,凡落在 接收窗口内的幀,接收方都必须处理,落在接收窗口外的幀被丢弃。
接收方每次允许接收的幀数称为接收窗口的尺寸。
1.5 理解阻塞通信的本质
理解了TCP通信的原理后,在Java中我们会采用Socket套接字来实现网络通信,下面这段代码演示了Socket通信的案例。
public class ServerSocketExample { public static void main(String[] args) throws IOException { final int DEFAULT_PORT = 8080; ServerSocket serverSocket = null; serverSocket = new ServerSocket(DEFAULT_PORT); System.out.println("启动服务,监听端口:" + DEFAULT_PORT); while (true) { Socket socket = serverSocket.accept(); System.out.println("客户端:" + socket.getPort() + "已连接"); new Thread(new Runnable() { Socket socket; public Runnable setSocket(Socket s){ this.socket=s; return this; } @Override public void run() { try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String clientStr = null; //读取一行信息 clientStr = bufferedReader.readLine(); System.out.println("客户端发了一段消息:" + clientStr); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bufferedWriter.write("我已经收到你的消息了"); bufferedWriter.flush(); //清空缓冲区触发消息发送 } catch (IOException e) { e.printStackTrace(); } } }.setSocket(socket)).start(); } } }
在我们讲Redis的专题中详细讲到过,上述通信是BIO模型,也就是阻塞通信模型,阻塞主要体现的点是
- accept,阻塞等待客户端连接
- io阻塞,阻塞等待客户端的数据传输。
相信大家和我一样有一些以后,这个阻塞和唤醒到底是怎么回事,下面我们简单来了解一下。
1.5.1 阻塞操作的本质
阻塞是指进程在等待某个事件发生之前的等待状态,它是属于操作系统层面的调度,我们通过下面操作来追踪Java程序中有多少程序,每一个线程对内核产生了哪些操作。
strace,Linux操作系统中的指令
-
把ServerSocketExample.java,去掉package导入头,拷贝到linux服务器的 /data/app目录下。
-
使用javac ServerSocketExample.java进行编译,得到.class文件
-
使用下面这个命令来追踪(打开一个新窗口)
按照strace官网的描述, strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
strace -ff -o out java ServerSocketExample
- -f 跟踪目标进程,以及目标进程创建的所有子进程
- -o 把strace的输出单独写到指定的文件
-
上述指令执行完成后,会在/data/app目录下得到很多out.*的文件,每个文件代表一个线程。因为Java本身是多线程的。
[root@localhost app]# ll total 748 -rw-r--r--. 1 root root 14808 Aug 23 12:51 out.33320 //最小的表示主线程 -rw-r--r--. 1 root root 186893 Aug 23 12:51 out.33321 -rw-r--r--. 1 root root 961 Aug 23 12:51 out.33322 -rw-r--r--. 1 root root 917 Aug 23 12:51 out.33323 -rw-r--r--. 1 root root 833 Aug 23 12:51 out.33324 -rw-r--r--. 1 root root 819 Aug 23 12:51 out.33325 -rw-r--r--. 1 root root 23627 Aug 23 12:53 out.33326 -rw-r--r--. 1 root root 1326 Aug 23 12:51 out.33327 -rw-r--r--. 1 root root 1144 Aug 23 12:51 out.33328 -rw-r--r--. 1 root root 1270 Aug 23 12:51 out.33329 -rw-r--r--. 1 root root 8136 Aug 23 12:53 out.33330 -rw-r--r--. 1 root root 8158 Aug 23 12:53 out.33331 -rw-r--r--. 1 root root 6966 Aug 23 12:53 out.33332 -rw-r--r--. 1 root root 1040 Aug 23 12:51 out.33333 -rw-r--r--. 1 root root 445489 Aug 23 12:53 out.33334
-
打开out.33321这个文件(主线程后面的一个文件),shift+g到该文件的尾部,可以看到如下内容。
下面这些方法,都是属于系统调用,也就是调用操作系统提供的内核指令触发相关的操作。
# 创建socket fd socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5 .... # 绑定8888端口 bind(5, {sa_family=AF_INET6, sin6_port=htons(8888), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0 # 创建一个socket并监听申请的连接, 5表示sockfd,50表示等待队列的最大长度 listen(5, 50) = 0 mprotect(0x7f21d00df000, 4096, PROT_READ|PROT_WRITE) = 0 write(1, "\345\220\257\345\212\250\346\234\215\345\212\241\357\274\214\347\233\221\345\220\254\347\253\257\345\217\243\357\274\23288"..., 34) = 34 write(1, "\n", 1) = 1 lseek(3, 58916778, SEEK_SET) = 58916778 read(3, "PK\3\4\n\0\0\10\0\0U\23\213O\336\274\205\24X8\0\0X8\0\0\25\0\0\0", 30) = 30 lseek(3, 58916829, SEEK_SET) = 58916829 read(3, "\312\376\272\276\0\0\0004\1\367\n\0\6\1\37\t\0\237\1 \t\0\237\1!\t\0\237\1\"\t\0"..., 14424) = 14424 # poll, 把当前的文件指针挂到等待队列,文件指针指的是fd=5,简单来说就是让当前进程阻塞,直到有事件触发唤醒 * events: 表示请求事件,POLLIN(普通或优先级带数据可读)、POLLERR,发生错误。 poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
从这个代码中可以看到,Socket的accept方法最终是调用系统的poll函数来实现线程阻塞的。
通过在linux服务器输入 man 2 poll
man: 帮助手册
2: 表示系统调用相关的函数
DESCRIPTION poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O.
poll类似于select函数,它可以等待一组文件描述符中的IO就绪事件
-
通过下面命令访问socket server。
telnet 192.168.221.128 8888
这个时候通过tail -f out.33321这个文件,发现被阻塞的poll()方法,被POLLIN事件唤醒了,表示监听到了一次连接。
poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}]) accept(5, {sa_family=AF_INET6, sin6_port=htons(53778), inet_pton(AF_INET6, "::ffff:192.168.221.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 6
1.5.2 阻塞被唤醒的过程
如图1-12所示,网络数据包通过网线传输到目标服务器的网卡,再通过2所示的硬件电路传输,最终把数据写入到内存中的某个地址上,接着网卡通过中断信号通知CPU有数据到达,操作系统就知道当前有新的数据包传递过来,于是CPU开始执行中断程序,中断程序的主要逻辑是
- 先把网卡接收到的数据写入到对应的Socket接收缓冲区中
- 再唤醒被阻塞在poll()方法上的线程
<center>图1-12</center>
1.5.3 阻塞的整体原理分析
操作系统为了支持多任务处理,所以实现了进程调度功能,运行中的进程表示获得了CPU的使用权,当进程(线程)因为某些操作导致阻塞时,就会释放CPU使用权,使得操作系统能够多任务的执行。
当多个进程是运行状态等待CPU调度时,这些进程会保存到一个可运行队列中,如图1-13所示。
<center>图1-13</center> 当进程A执行创建Socket语句时,在Linux操作系统中会创建一个由文件系统管理的Socket对象,这个Socket对象包含发送缓冲区、接收缓冲区、等待队列等,其中等待队列是非常重要的结构,它指向所有需要等待当前Socket事件的进程,如图1-14所示。
当进程A调用poll()方法阻塞时,操作系统会把当前进程A从工作队列移动到Socket的等待队列中(将进程A的指针指向等待队列,后续需要进行唤醒),此时A被阻塞,CPU继续执行下一个进程。
<center>图1-14</center>
当Socket收到数据时,等待该Socket FD的进程会收到被唤醒,如图1-15所示,计算机通过网卡接收到客户端传过来的数据,网卡会把这个数据写入到内存,然后再通过中断信号通知CPU有数据到达,于是CPU开始执行中断程序。
当发生了中断,就意味着需要操作系统的介入,开展管理工作。由于操作系统的管理工作(**如进程切换、分配IO设备)需要使用特权指令,因此CPU要从用户态转换为核心态。**中断就可以使CPU从用户态转换为核心态,使操作系统获得计算机的控制权。因此,有了中断,才能实现多道程序并发执行。
此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中。
<center>图1-15</center>
1.5 Linux中的select/poll模型本质
前面在1.4节中讲的其实是Recv()方法,它只能监视单个Socket。而在实际应用中,这种单Socket监听很明显会影响到客户端连接数,所以我们需要寻找一种能够同时监听多个Socket的方法,而select/poll就是在这个背景下产生的,其中poll方法在前面的案例中就讲过,默认情况下使用poll模型。
先来了解一下select模型,由于在前面的分析中我们知道Recv()只能实现对单个socket的监听,当客户端连接数较多的时候,会导致吞吐量非常低,所以我们想,能不能实现同时监听多个socket,只要任何一个socket连接存在IO就绪事件,就触发进程的唤醒。
如图1-16所示,假设程序同时监听socket1和socket2这两个socket连接,那么当应用程序调用select方法后,操作系统会把进程A分别指向这连个个socket的等待队列中。当任何一个Socket收到数据后,中断程序会唤醒对应的进程。
当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。
<center>图1-16</center>
select模式有二个问题,
- 就是每次调用select都需要将进程加入到所有监视器socket的等待队列,每次唤醒都需要从等待队列中移除,这里涉及到两次遍历,有一定的性能开销。
- 进程被唤醒后,并不知道哪些socket收到了数据,所以还需要遍历一次所有的socket,得到就绪的socket列表
由于这两个问题产生的性能影响,所以select默认规定只能监视1024个socket,虽然可以通过修改监视的文件描述符数量,但是这样会降低效率。而poll模式和select基本是一样,最大的区别是poll没有最大文件描述符限制。
1.6 Linux中的epoll模型
有没有更加高效的方法,能够减少遍历也能达到同时监听多个fd的目的呢?epoll模型就可以解决这个问题。
epoll 其实是event poll的组合,它和select最大的区别在于,epoll会把哪个socket发生了什么样的IO事件通知给应用程序,所以epoll实际上就是事件驱动,具体原理如图1-17所示。
在epoll中提供了三个方法分别是epoll_create、epoll_ctl、epoll_wait。具体执行流程如下
- 首先调用epoll_create方法,在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,它是一个红黑树结构。这个集合简单理解成fd集合。
- 接着调用epoll_ctl函数将当前fd封装成epitem加入到eventpoll对象中,并给这个epitem加入一个回调函数注册到内核。当这个fd收到网络IO事件时,会把该fd对应的epitem加入到eventpoll中的就绪列表rdlist(双向链表)中。同时再唤醒被阻塞的进程A。
- 进程A继续调用epoll_wait方法,直接读取epoll中就绪队列rdlist中的epitem,如果rdlist队列为空,则阻塞等待或者等待超时。
从epoll的原理中可以得知,由于rdlist的存在,使得进程A被唤醒后知道哪些Socket(fd)发生了IO事件,从而在不需要遍历的情况下获取所有就绪的socket连接。
<center>图1-17</center>
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构
! 如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
基于软件分析的智能化开发新型服务与技术
摘要:从云服务厂商的角度来给大家介绍一下,当前业界围绕该领域要做哪些事情。 本文分享自华为云社区《基于软件分析的智能化开发新型服务与技术》,作者:敏捷的小智 。 本文以技术文章的方式回顾梁老师在 SIG-程序分析技术沙龙上的分享,分享的主题是基于软件分析的智能化开发新型服务与技术。 技术趋势分析 先来看一下业界相关的技术趋势。我从云服务厂商的角度来给大家介绍一下,当前业界围绕该领域要做哪些事情。 # 缺陷检测与修复领域 首先看一下缺陷检测与修复 领域。 ## Amazon - CodeGuru Amazon 推出了一个叫 CodeGuru [1] 的 offering,其主要功能是通过全方位的缺陷检测和扫描,帮助程序员更快地识别出有问题的代码,并提供智能建议以进行代码修复。 CodeGuru: 自动执行代码审查、持续监控性能并识别低效代码,提供智能建议以提高代码质量,降低代码维护及运行总体成本 [1] 该 offering 提供了两个子服务: CodeGuru Reviewer 顾名思义,Reviewer 子服务其实就是用机器学习的方式,通过自动化推理的算法提供智能化代码审查的服务,帮...
- 下一篇
手机短信登录、邮箱登录、QQ 登录都想要,咋办?
@[toc] 今天想和大家聊一聊 Shiro 中的多 Realm 认证策略问题~ 在项目中,如果我们想手机验证码登录、第三方 QQ 登录、邮箱登录等多种登录方式共存,那么就可以考虑通过 Shiro 中的多 Realm 来实现,具体操作中,一个 Realm 刚好就对应一种登录方式。 多 Realm 登录的用法并不难,松哥之前也专门发过相关的文章和大家分享,传送门: 其实我不仅会 Spring Security,Shiro 也略懂一二! 今天我不想聊用法,主要是想和大家聊一聊这里相关的源码。因此本文需要大家有一定的 Shiro 使用经验,若无,可以参考上面的链接恶补一下。 1. ModularRealmAuthenticator 1.1 Realm 去哪了? 我们配置的 Realm,可以直接配置给 SecurityManager,也可以配置给 SecurityManager 中的 ModularRealmAuthenticator。 如果我们是直接配置给 SecurityManager,那么在完成 Realm 的配置后,会自动调用 afterRealmsSet 方法,在该方法的中,会将我们...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7设置SWAP分区,小内存服务器的救世主
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker使用Oracle官方镜像安装(12C,18C,19C)