分布式微服务架构下网络通信的底层实现原理
在分布式架构中,网络通信是底层基础,没有网络,也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作,共同完成一件事情。
同样,在大规模的系统架构中,应用吞吐量上不去、网络存在通信延迟、我们首先考虑的都是网络问题,因此网络的重要性不言而喻。
作为现代化应用型程序员,要开发一个网络通信的应用,是非常简单的。不仅仅有成熟的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;}@Overridepublic 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: 表示系统调用相关的函数
DESCRIPTIONpoll() performs a similar task to select(2): it waits for one of a set of filedescriptors 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连接。
相关文章:
分布式微服务架构下网络通信的底层实现原理
在分布式架构中,网络通信是底层基础,没有网络,也就没有所谓的分布式架构。只有通过网络才能使得一大片机器互相协作,共同完成一件事情。 同样,在大规模的系统架构中,应用吞吐量上不去、网络存在通信延迟、我…...
进大厂必备的Java面试八股文大全(2023最新精简易懂版,八股文中的八股文)
为什么同样是跳槽,有些人薪资能翻三倍?” 最近一个粉丝发出了灵魂拷问,类似的问题我收到过很多次,身边也确实有认识的同事、朋友们有非常成功的跳槽经历和收益,先说一个典型例子: 学弟小 A 工作一年半&am…...
都说测试行业饱和了,为什么我们公司给初级测试开到了12K?
故事起因: 最近我有个刚毕业的学生问我说:我感觉现在测试行业已经饱和了,也不是说饱和了,是初级的测试根本就没有公司要,哪怕你不要工资也没公司要你,测试刚学出来,没有任何的项目经验和工作经验…...
解决Idea启动项目失败,提示Error running ‘XXXApplication‘: Command line is too long
IDEA版本为:IntelliJ IDEA 2018.2 (Ultimate Edition)一、问题描述有时当我们使用IDEA,Run/Debug一个SpringBoot项目时,可能会启动失败,并提示以下错误。Error running XXXApplication: Command line is too long. Shorten comman…...
GB/T28181-2022针对H.265、AAC的说明和技术实现
GB/T28181-2022规范说明GB/T28181-2022相对来GB/T28181-2016针对H.265、AAC的更新如下:——更改了“联网系统通信协议结构图”,媒体流通道增加了 H.265、G.722.1、AAC(见 4.3.1,2016 年版的 4.3.1)。——增加了对 H.26…...
开关电源环路稳定性分析(11)——观察法找零极点
大家好,这里是大话硬件。 这篇文章主要是分享如何用观察法直接写出补偿网络中的零极点的表达式。 在前面的文章中,我们分别整理了OTA和OPA型的补偿网络,当时有下面的结论。 针对某个固定的补偿网络,我们可以用数学的方法推导补偿…...
焕新启航,「龙蜥大讲堂」2023 年度招募来了!13 场技术分享先睹为快
龙蜥大讲堂是龙蜥推出的系列技术直播活动,邀请龙蜥社区的开发者们分享围绕龙蜥技术展开,包括但不限于内核、编译器、机密计算、容器、储存等相关技术领域。欢迎社区开发者们积极参与,共享技术盛宴。往期回顾龙蜥社区技术系列直播截至目前已举…...
推广传单制作工具
临近节日如何制作推广活动呢?没有素材制作满减活动宣传单怎么办?小编教你如何使用在线设计工具乔拓云,轻松设计商品的专属满减活动宣传单,不仅设计简单,还能自动生成活动分享链接,只需跟着小编下面的设计步…...
软件设计(十一)数据结构(上)
线性结构 线性表 线性表是n个元素的有限序列,通常记为(a1,a2....an),特点如下。 存在唯一的一个称作“第一个”的元素。存在位移的一个称作“最后一个”的元素。除了表头外,表中的每一个元素均只有唯一的直接前趋除了表尾外&…...
https协议
文章目录对称加密方案非对称加密方案对称加密方案非对称加密方案对称加密方案非对称加密方案数字证书因为HTTP是明文传输,所以会很有可能产生中间人攻击(获取并篡改传输在客户端及服务端的信息并不被人发觉),HTTPS加密应运而生。 …...
深入浅出C语言——数据在内存中的存储
文章目录一、数据类型详细介绍1. C语言中的内置类型2. 类型的基本归类:二. 整形在内存中的存储1. 原码、反码、补码2. 大小端三.浮点数存储规则一、数据类型详细介绍 1. C语言中的内置类型 C语言的内置类型有char、short、int、long、long long、float、double&…...
在 Centos 上在线安装 GitLab
作为程序员,其中一个愿望是拥有一个自己的代码存储库。在支持私有部署的代码存储库产品中,GitLab 是比较著名的了,所以今天我总结了一下在 Centos 上安装 GitLab 的过程。 依赖 基础依赖 首先,需要安装部分基础的依赖ÿ…...
模型解释性:SHAP包的使用
本篇博客介绍另一种事后可解释性方法:SHAP(SHapley Additive exPlanation)方法。 1. Shapley值理论 Shapley值是博弈论中的一个概念,通过衡量联盟中各成员对联盟总目标的贡献程度,从而根据贡献程度来进行联盟成员的利益分配,避免…...
算法训练营 day45 动态规划 0-1背包理论 分割等和子集
算法训练营 day45 动态规划 0-1背包理论 分割等和子集 0-1背包理论 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 在下面的讲解中&…...
SSM框架
1.mybatis的底层原理 本质上就是使用反射和动态代理来实现对应的映射关系 2.日志级别 3.传递参数 单个参数的传递和多个参数的传递 Emp selectOne(Param(“xingming”) String name); List selectByCondition(Param(“name”) String name,Param(“sal”) double sal); 4.#和…...
教育行业需要什么样的客服系统?
某教育公司拥有素质教育、成人教育、智慧教育等多个业务板块,日常通过电商、线上媒体、线上线下授课等方式进行业务开展和品牌宣传,取得了非常不错的成绩,受到了很多人的好评反馈。 对于这样一个教育公司,客户来源广泛࿰…...
花房集团任命新首席财务官:已跌破IPO发行价,活跃用户下滑
上市刚满2个月,花椒母公司花房集团(HK:03611)的高管就发生了变更。2023年2月12日,花房集团披露的公告显示,董事会宣布赵磊为该公司首席财务官(CFO),自2023年2月10日起生效。 据贝多…...
儿童绘本馆图书借阅租赁知识付费小程序源码交流
1.分类图书 2.书单推荐 4.会员卡次、期限购买 5.借阅时间选择 6.积分签到 7.优惠Q领取 前端uniapp开发 后端thinkphp开发 完全开源 <template> <view class"sp-section sp-index"> <!-- search --> <view class&qu…...
Vue3 中 axios 的安装及使用
目录前言:一、什么是 axios ?二、Axios 的配置项三、Axios 的请求方式四、自定义创建实例五、Axios 请求错误处理六、Axios 解决跨域问题七、Axios 请求案例随机笑话大全总结:前言: 在编写vue里的项目时,必须要用和后台…...
Django设计模式以及模板层介绍
MVC和MTV 传统的MVC作用:降低模块间的耦合度(解耦)Django的MTV模式 作用:降低模块间的耦合度(解耦)什么是模板 1、模板是可以根据字典数据动态变化的html网页2、模板可以根据视图中传递的字典数据动态生成相…...
Linux信号一门搞定
1.信号是什么? 信号其实就是一个软件中断。 例: 输入命令,在Shell下启动一个前台进程。用户按下Ctrl-C,键盘输入产生一个硬件中断。如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,…...
手撸一个动态Feign,实现一个“万能”接口调用
Feign,在微服务框架中,是的服务直接的调用变得很简洁、简单,而不需要再编写Java Http调用其他微服务的接口。 动态feign 对于fegin调用,我们一般的用法:为每个微服务都创建对应的feignclient接口,然后为每…...
Linux Capabilities 入门
目录 Linux capabilities 是什么? capabilities 的赋予和继承 线程的 capabilities Permitted Effective Inheritable Bounding Ambient 文件的 capabilities Permitted Inheritable Effective 运行 execve() 后 capabilities 的变化 案例 Linux capab…...
驱动 day6
关于设备树的理解: 设备树(Device Tree)是一种用于特定硬件设备的解释语法树。它用来表示存储有关主板硬件和CPU架构信息的数据在内核中的传递格式,使内核可以更好地了解硬件并支持它们,而不必编写固定的代码。设备节点…...
附录2-tensorflow目标检测
源码来自作者Bubbliiiing,我对参考链接的代码略有修改,网盘地址 链接:百度网盘 请输入提取码 提取码:dvb1 目录 1 参考链接 2 环境 3 数据集准备 3.1 VOCdevkit/VOC2007 3.2 model_data/voc_classes.txt 3.3 voc_an…...
常见的EMC问题
电磁兼容设计的目的就在于满足产品功能要求、减少调试时间,使产品满足电磁兼容标准的要求,并且使产品不会对系统中的其它设备产生电磁干扰。 电磁兼容设计中常见的问题有哪些? 1、电磁兼容设计可以从电路设计(包括器件选择&…...
Redis内存存储效率问题
目录 内存碎片是如何形成的? 如何判断是否有内存碎片? 如何清理内存碎片? INFO命令 面向 Prometheus 的 Redis-exporter 监控 实习期间,了解到,企业级开发中多个项目使用Redis,运行Redis实例的有可能是…...
3.28 haas506 2.0开发教程-example-蓝牙多设备扫描(仅支持M320,HD1)
haas506 2.0开发教程-example-蓝牙多设备扫描案例说明蓝牙信息克隆1.手机蓝牙改名信息克隆代码测试案例说明 开发板扫描蓝牙设备,获取并打印蓝牙设备mac地址。mac地址每个设备不同,且不能更改。本案例仅适用于M320开发板和HD1-RTU。案例使用手机与iBeac…...
C语言经典编程题100例(41~60)
目录41、习题4-4 特殊a串数列求和42、习题4-6 水仙花数43、习题4-7 最大公约数和最小公倍数44、习题7-5 找鞍点45、练习5-1 求m到n之和46、练习5-2 找两个数中最大者47、练习5-3 数字金字塔48、习题5-1 符号函数49、习题5-2 使用函数求奇数和50、习题5-3 使用函数计算两点间的距…...
git日常使用命令
实习这段时间使用了很多git指令来提交代码,简单记录一下日常使用的指令: 提交代码通常顺序: 1.git status 查看本地修改项 2.git add . 提交全部文件 (这个 .是全部文件)到暂存区 3.git commit -m ‘本次提交的说明’…...
wordpress 安装后必装/网店seo是什么意思
excel文件的工作表保护,可以保护文件内容数据的正确性,不会轻易被修改,但是如果需要编辑文件也只需要将工作表保护撤销即可。 撤销步骤也很简单,打开excel文件,点击上方工具栏,找到【审阅】-【撤销工作表保…...
wordpress安装主题后不够/seo首页网站
一 创建私有仓库 # 1、拉取私有仓库镜像 docker pull registry # 2、启动私有仓库容器 docker run -id --nameregistry -p 5000:5000 registry # 3、打开浏览器 输入地址http://私有仓库服务器ip:5000/v2/_catalog,看到{"repositories":[]} 表示私有仓…...
防伪网站模板/为企业推广
把自己使用到的ldap调用的代码分享出来,希望大家可以参考#!/usr/bin/python# -*- coding: utf-8 -*-"""Time : 2019/11/14 5:37 PMAuthor : NoSongFile : LdapBaseApi.pySoftware: PyCharm# 接口文档: https://ldap3.readthedocs.io/# https://ldap3…...
甘肃省第九建设集团网站首页/志鸿优化设计答案
朋友门!等待终于结束了! 我们的社区在短短一周内已经发展到了超过10,000名Twitter粉丝,12,000名Discord成员,以及7,000名Telegram成员!为了感谢您的大力支持,我们将继续努力。为了感谢您的大力支持,我们很高…...
门户网站建设要求/制作网页设计公司
从成立到借壳上市,有赞用了5年多时间。这期间,它有好几次机会死掉,有很多的理由活不到今天,白鸦曾经说,每一次度过难关最关键都是靠团队的力量。谢天谢地,它活了下来。 那么,这个在To B领域敢打…...
仿爱范儿网wordpress主题/seo服务外包公司
以前写过一个沉浸式状态栏 的实现方式 Android 沉浸式状态栏 实现方式一 现在有个更为简单的实现方式 。 相关链接 http://www.apkbus.com/forum.php?modviewthread&tid255929&extrapage%3D3%26filter%3Dsortid%26orderby%3Ddateline%26sortid%3D12 1、效果图 demo 的…...