如何正确理解软件应用系统中关于系统通信的那些事?

苍穹之边,浩瀚之挚,眰恦之美;悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》

写在开头

随着业务需求的发展和用户数量的激增,对于互联 应用系统或者服务应用程序则提出了新的挑战,也对从事系统研发的开发者有了更高的要求。作为一名IT从业研发人员,我们都知道的事,良好的用户体验是我们和应用系统间快速反馈,一直以来都是我们考量一个系统是否稳定和是否高效的设计目标,但是保证这个目标的关键之一,主要在于如何保证系统间的通信稳定和高效。从而映射出,如何正确理解软件应用系统中关于系统通信的那些事?是我们必须了解和理解的一项关键工作,接下来,我们就一起来总结和探讨一下。

基本概述

要想理解系统服务间的交流,拿我们人与人的交流来做类比是个不错的选择。我们都知道,人与人之间的实现交流的基本元素主要有以下几个方面:

  • 能够相互听懂和理解的交流语言(即双方要基于相同的”协议”之下)
  • 必要的传播介质(实在的物理介质,空气纸张等都行)
  • 约定好的处理信息的方式(常见的一问一答 或是先记录后处理等表现形式)
  • 从而得知,系统服务间的交流的主要表现在以下几个方面:

    1. 相同的通信原语:就像人类互相需要使用相同的语言进行交流,计算机服务也必须使用互相能识别的消息格式进行交互。

    1. 传播信息的介质:人类交流时往往需要某种介质传播信息,如空气、纸张甚至是眼神等。同样的, 络信息的传递也需要物理介质的帮助,以及工作在其上的一系列相关协议。
    2. 处理信息的方式:人类交流时可以是面对面直接问答形式的,也可能是邮件、短信等延时应答形式的,对应的是不同的业务场景,在计算机里进行通信处理方式。

    1. 实现通信方式:根据不同的协议都能实现通信功能的方式,一般基于一种或者至少一种协议实现。

    组成要素

    实现系统间通信主要的三个要素:通信格式,通信协议,通信模型。

    根据人与人的交流的构成要素,抽象成计算机系统服务中对应的概念(行之有效的概念往往是简单且趋同的),系统间通信主要考虑以下三个方面:通信格式,通信协议,通信模型。具体详情如下:

    1. 通信格式(Communication Format): 主要是指实现通信的消息格式(Message Format),是表达消息内容等基本表现形式。常用的消息格式有xml,json,TLV等。

    1. 通信协议(Communication Protocol): 主要是指实现通信的 络协议(Network Protocol)。常见的TCP/IP协议,UDP协议等。
    2. 通信模型(Communication Model): 主要是指实现通信的 络模型(Network Model)。常见的模型主要有阻塞式通信模型,非阻塞式通信模型,同步通信模型,异步通信模型。

    接下来,我们来详细解析这些组成要素:

    1. 对于消息格式来说,是帮助我们识别消息和表达消息内容的基本方式:
  • XML:和语言无关,常用于对系统环境进行描述,如常见的maven仓库配置,或者spring配置等。
  • JSON:轻量级消息格式,和语言无关。携带同样的信息,占用容量比XML小。
  • Protocol Buffer:Google定义的消息格式,只提供了java,c++和python语言的实现。
  • TLV:比JSON更轻量级的数据格式,连JSON中的”{}”都没有了。它是通过字节的位运算来实现序列化和反序列化。
    1. 对于 络协议来说,是帮助我们实现消息传输和传递的表达方式:
  • 数据在 络七层模型中传递的时候,在 络层是”数据包”,在数据链路层被封装成”帧”(数字信 ),在物理层则是”比特”(电信 )。
  • 不同的协议都能实现通信功能,最适合本系统的通信协议才是最好的。
    1. 对于 络模型来说,主要是帮助我们理解和选择适合当前场景的应用框架:
  • 在计算机 路层面来说,常见 络模型主要有OSI 参考模型和TCP/IP 模型两种。
  • 除此之外,还有Linux 络I/O 模型和Java JDK中的I/O 模型
  • 络协议

    我们用手机连接上 的时候,会用到许多 络协议。从手机连接 W i F i 开始, 使用的是 8 0 2 . 11 (即 W L A N ) 协议, 通过 W L A N 接入 络; 手机自动获取 络配置,使用的是 D H C P 协议,获取配置后手机才能正常通信。这时手机已经连入局域 ,可以访问局域 内的设备和资源, 但还不能使用互联 应用,例如:微信、抖音等。想要访问互联 ,还需要在手机的上联 络设备上实现相关协议, 即在无线路由器上配置 N AT、 P P P O E 等功能, 再通过运营商提供的互联 线路把局域 接入到互联 中, 手机就可以上 玩微信、刷抖音了。常见的 络主要有:

    1. 局域 : 小范围内的私有 络, 一个家庭内的 络、一个公司内的 络、一个校园内 的 络都属于局域 。

    1. 广域 : 把不同地域的局域 互相连接起来的 络。运营商搭建广域 实现跨区域的 络互连。
    2. 互联 : 互联全世界的 络。互联 是一个开放、互联的 络, 不属于任何个人和任何机构, 接入互联 后可以和互联 的任何一台主机进行通信。

    简单来说,就是手机、无线路由器等设备通过多种 络协议实现通信。 络协议就是为了通信各方能够互相交流而定义的标准或规则, 设备只要遵循相同的 络协议就能够实现通信。那 络协议又是谁规定的呢? ISO 制定了一个国际标准OSI , 其中的 OSI 参考模型常被用于 络协议的制定。常见的 络协议:

    1. 面向连接协议(TCP协议):在发送数据之前, 在收发主机之间连接一条逻辑通信链路。好比平常打电话,输入完对方电话 码拨出之后, 只有对方接通电话才能真正通话,通话结束后将电话机扣上就如同切断电源。TCP协议是一种面向有连接的传输层协议,能够对自己提供的连接实施控制。适用于要求可靠传输的应用, 例如文件传输。

    1. 面向无连接协议(UDP协议):不要求建立和断开连接。发送端可于任何时候自由发送数据。如同去寄信, 不需要确认收件人信息是否真实存在,也不需要确认收件人是否能收到信件,只要有个寄件地址就可以寄信了。U D P 是一种面向无连接的传输层协议,不会对自己提供的连接实施控制。适用于实时应用, 例如: I P 电话、视频会议、直播等

    络模型

    从计算机 络层面来说,常见 络模型主要有OSI 参考模型和TCP/IP 模型两种,主要表达如下:

    OSI 参考模型:

    O S I 参考模型将 络协议提供的服务分成 7 层,并定义每一层的服务内容, 实现每一层服务的是协议, 协议的具体内容是规则。上下层之间通过接口进行交互,同一层之间通过协议进行交互。 O S I 参考模型只对各层的服务做了粗略的界定, 并没有对协议进行详细的定义,但是许多协议都对应了 7 个分层的某一层。所以要了解 络,首先要了解 O S I 参考模型:

    1. 应用层:O S I 参考模型的第 7 层( 最高层)。应用程序和 络之间的接口, 直接向用户提供服务。应用层协议有电子邮件、远程登录等协议。

    1. 表示层:O S I 参考模型的第 6 层。负责数据格式的互相转换, 如编码、数据格式转换和加密解密等。保证一个系统应用层发出的信息可被另一系统的应用层读出。
    2. 会话层:O S I 参考模型的第 5 层。主要是管理和协调不同主机上各种进程之间的通信(对话),即负责建立、管理和终止应用程序之间的会话。

    1. 传输层:O S I 参考模型的第 4 层。为上层协议提供通信主机间的可靠和透明的数据传输服务, 包括处理差错控制和流量控制等问题。只在通信主机上处理, 不需要在路由器上处理。
    2. 络层:O S I 参考模型的第 3 层。在 络上将数据传输到目的地址, 主要负责寻址和路由选择。

    1. 数据链路层:O S I 参考模型的第 2 层。负责物理层面上两个互连主机间的通信传输, 将由 0、 1 组成的比特流划分成数据帧传输给对端,即数据帧的生成与接收。通信传输实际上是通过物理的传输介质实现的。 数据链路层的作用就是在这些通过传输介质互连的设备之间进行数据处理。 络层与数据链路层都是基于目标地址将数据发送给接收端的,但是 络层负责将整个数据发送给最终目标地址, 而数据链路层则只负责送一个分段内的数据。
    2. 物理层:O S I 参考模型的第 1 层( 最底层)。负责逻辑信 ( 比特流) 与物理信 (电信 、光信 )之间的互相转换,通过传输介质为数据链路层提供物理连接。

    TCP/IP 模型:

    由于 OSI 参考模型把服务划得过于琐碎,先定义参考模型再定义协议,有点理想化。 TCP / IP 模型则正好相反, 通过已有的协议归纳总结出来的模型,成为业界的实际 络协议标准。TCP / IP 是有由 I E T F 建议、推进其标准化的一种协议, 是 IP 、 TCP 、HTTP 等协议的集合。TCP / IP是为使用互联 而开发制定的协议族, 所以互联 的协议就是 TCP / IP。TCP / IP 每层的主要协 议详情如下:

    1. 络接入层:TCP / IP 是以 O S I 参考模型的物理层和数据链路层的功能是透明的为前提制定的,并未对这两层进行定义,所以可以把物理层和数据链路层合并称为 络接入层。 络接入层是对 络介质的管理,定义如何使用 络来传送数据。但是在通信过程中这两层起到的作用不一样, 所以也有把物理层和数据链路层分别称为硬件、 络接口层。 TCP / IP分为四层或者五层都可以,只要能理解其中的原理即可。设备之间通过物理的传输介质互连, 而互连的设备之间使用 M A C 地址实现数据传输。采用 M A C 地址,目的是为了识别连接到同一个传输介质上的设备。

    1. 络层:相当于 OSI 模型中的第 3 层 络层, 使用 I P 协议。 I P 协议基于 I P 地址转发分包数据,作用是将数据包从源地址发送到目的地址。TCP / IP 分层中的 络层与传输层的功能通常由操作系统提供。 路由器就是通过 络层实现转发数据包的功能。
    2. 传输层:相当于 OSI 模型中的第 4 层传输层, 主要功能就是让应用程序之间互相通信,通过端口 识别应用程序, 使用的协议有面向连接的 TCP 协议和面向无连接的 UDP 协议。

    1. 应用层:相当于 OSI 模型中的第 5 – 7 层的集合, 不仅要实现 O S I 模型应用层的功能,还要实现会话层和表示层的功能。 HTTP 、 POP3 、 TELNET 、 SSH、 F T P 、 SNMP 都是应用层协议。

    除此之外,我们还需要知道Linux 络I/O 模型和Java JDK中的I/O 模型:

    Linux 络I/O 模型:

    Linux的内核将所的外部设备看作一个文件来操作,对于一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符(fd,File Descriptor);同时,在面对一个Socket的读写时也会有相应的套接字描述符(socketfd,Socket File Descriptor),描述符是一个数字,它指向内核中的一个结构体,比如文件路径,数据区等。Linux 络I/O 模型是按照UNIX 络编程来定义的,主要有:

    阻塞I/O模型(Blocking I/O ):

    最流行的I/O模型,本书到目前为止的所有例子都使用该模型。默认情形下,所有套接字都是阻塞的。使用UDP而不是TCP为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据 已经收到,要么还没有。对于TCP而言,诸如套接字低水位标记等额外变量开始起作用,道指这个概念复杂。我们把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不管如何实现,一般都会从在应用进程空间中国运行切换到在内核空间中运行,一端时间之后再切换回来。 在上图中,进程调用recvfrom,其系统调用直到数据 到达且被复制到应用进程的缓冲区中或者发送错误才返回。最常见的错误是系统调用被信 中断,我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据 。

    非阻塞I/O模型(NoneBlocking I/O):

    进程把一个套接字设置成非阻塞是在通知内核:当所有请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据 准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。接着处理数据。当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们成为轮询,应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到。

    I/O复用模型(IO Multiplexing):

    I/O复用,我们就可以调用select或者poll,阻塞在这两个系统调用中的某一个,而不是阻塞在真正的I/O系统调用上。我们阻塞与select调用,等待数据 套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所可读数据 复制到应用进程缓冲区。比较上面两图,I/O复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,其优势在于可以等待多个描述符就绪。

    信 驱动I/O复用模型(Signal Driven IO):

    可以用信 ,让内核在描述符就绪时发送SIGIO信 通知我们。称为信 驱动式I/O。我们首先开启套接字的信 驱动式I/O功能,并通过sigaction系统调用安装一个信 处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据 准备好读取时,内核就为该进程产生一个SIGIO信 。我们随后既可以在信 处理函数中调用recvfrom读取数据 ,并通知主循环数据已准备好待处理。也可以立即通知循环,让它读取数据 。无论如何处理SIGIO信 ,这种模型的优势在于等待数据 到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信 处理函数的通知:既可以是数据已准备好被处理,也可以是数据 已准备好被读取。

    异步I/O模型(Asynchronous IO ):

    告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信 驱动模型的主要区别在于:信 驱动I/O是由内核通知我们如何启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。我们调用aio_read函数,给内核传递描述符、缓冲区指针。缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等到I/O完成期间,我们的进程不被阻塞。

    Java JDK中的I/O 模型:

    在Java语言中,应用程序发起 I/O 调用后,会经历两个阶段:

  • 内核等待 I/O 设备准备好数据;
  • 内核将数据从内核空间拷贝到用户空间。
  • 其中,阻塞和非阻塞:

  • 阻塞调用会一直等待远程数据就绪再返回,即上面的阶段1会阻塞调用者,直到读取结束;
  • 而非阻塞无论在什么情况下都会立即返回,虽然非阻塞大部分时间不会被block,但是它仍要求进程不断地去主动询问kernel是否准备好数据,也需要进程主动地再次调用recvfrom来将数据拷贝到用户内存。
  • 而我们常说的同步和异步主要如下:

  • 同步方法会一直阻塞进程,直到I/O操作结束,注意这里相当于上面的阶段1,阶段2都会阻塞调用者。其中BIO,NIO,IO多路复用,信 驱动IO,这四种IO都可以归类为同步IO;
  • 而异步方法不会阻塞调用者进程,即使是从内核空间的缓冲区将数据拷贝到进程中这一操作也不会阻塞进程,拷贝完毕后内核会通知进程数据拷贝结束。
  • BIO模型

    同步阻塞 IO 模型中,服务器应用程序发起 read 系统调用后,会一直阻塞,直到内核把数据拷贝到用户空间。完整的架构应该是 客户端-内核-服务器,客户端发起IO请求,服务器发起系统调用,内核把IO数据从内核空间拷贝到用户空间,服务器应用程序才能使用到客户端发送的数据。一般来说,客户端、服务端其实都属于用户空间,借助内核交流数据。

    当用户进程发起了read系统调用,kernel就开始了IO的第一个阶段:准备数据。对于 络IO来说,很多时候数据在一开始还没有到达内核(比如说客户端目前只是建立了连接,还没有发送数据 或者是 卡等待接收数据),所以kernel就需要要等待足够的数据到来。而在服务器进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除阻塞状态,重新运行起来。

    Java中的JDBC也使用到了BIO技术。BIO在客户端连接数量不高的情况下是没问题的,但是当面对十万甚至百万级连接的时候,无法处理这种高并发情况,因此我们需要一种更高效的 I/O 处理模型来应对。

    NIO模型

    Java 中的 NIO 于 JDK 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的( 络)情况下,应使用 NIO 。

    当服务器进程发出read操作时,如果kernel中数据还没准备好,那么并不会阻塞服务器进程,而是立即返回error,用户进程判断结果是error,就知道数据还没准备好,此时用户进程可以去干其他的事情。一段时间后用户进程再次发read,一直轮询直到kernel中数据准备好,此时用户发起read操作,产生system call,kernel 马上将数据拷贝到用户内存,然后返回,进程就能使用到用户空间中的数据了。

    BIO一个线程只能处理一个IO流事件,想处理下一个必须等到当前IO流事件处理完毕。而NIO其实也只能串行化的处理IO事件,只不过它可以在内核等待数据准备数据时做其他的工作,不像BIO要一直阻塞住。NIO它会一直轮询操作系统,不断询问内核是否准备完毕。但是,NIO这样又引入了新的问题,如果当某个时间段里没有任何客户端IO事件产生时,服务器进程还在不断轮询,占用着CPU资源。所以要解决该问题,避免不必要的轮询,而且当无IO事件时,最好阻塞住(线程阻塞住就会释放CPU资源了)。所以NIO引入了多路复用机制,可以构建多路复用的、同步非阻塞的IO程序。

    AIO模型

    AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是进程操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。用户进程发起read操作之后,立刻就可以开始去做其它的事。

    内核收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

    IO多路复用模型

    Java 中的 NIO ,提供了 Selector(选择器)这个封装了操作系统IO多路复用能力的工具,通过Selector.select(),我们可以阻塞等待多个Channel(通道),知道任意一个Channel变得可读、可写,如此就能实现单线程管理多个Channels(客户端)。当所有Socket都空闲时,会把当前线程(选择器所处线程)阻塞掉,当有一个或多个Socket有I/O事件发生时,线程就从阻塞态醒来,并返回给服务端工作线程所有就绪的socket(文件描述符)。各个操作系统实现方案:

  • linux:select、poll、epoll
  • MacOS/FreeBSD:kqueue
  • Windows/Solaris:IOCP
  • IO多路复用题同非阻塞IO本质一样,只不过利用了新的select系统调用,由内核来负责本来是服务器进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用的开销,不过因为可以支持多路复用IO,即一个进程监听多个socket,才算提高了效率。进程先是阻塞在select/poll上(进程是因为select/poll/epoll函数调用而阻塞,不是直接被IO阻塞的),再是阻塞在读写操作的第二阶段上(等待数据从内核空间拷贝到用户空间)。

    IO多路复用的实现原理:利用select、poll、epoll可以同时监听多个socket的I/O事件的能力,而当有I/O事件产生时会被注册到Selector中。在所有socket空闲时,会把当前选择器进程阻塞掉,当有一个或多个流有I/O事件(或者说 一个或多个流有数据到达)时,选择器进程就从阻塞态中唤醒。通过select或poll轮询所负责的所有socket(epoll是只轮询那些真正产生了事件的socket),返回fd文件描述符集合给主线程串行执行事件。

    ??[特别注意]:

    select和poll每次调用时都需要将fd_set(文件描述符集合)从用户空间拷贝到内核空间中,函数返回时又要拷贝回来(epoll使用mmap,避免了每次wait都要将数组进行拷贝)。

    在实际开发过程中,基于消息进行系统间通信,我们一般会有四种方法实现:

    基于TCP/IP+BIO实现:

    在Java中可基于Socket、ServerSocket来实现TCP/IP+BIO的系统通信。

  • Socket主要用于实现建立连接即 络IO的操作
  • ServerSocket主要用于实现服务器端口的监听即Socket对象的获取
  • 为了满足服务端可以同时接受多个请求,最简单的方法是生成多个Socket。但这样会产生两个问题:

  • 生成太对Socket会消耗过多资源
  • 频繁创建Socket会导致系统性能的不足
  • 声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年6月25日
    下一篇 2022年6月25日

    相关推荐