一篇讲解“服务调用”的良心之作

这篇文章将我所了解到的解决“服务调用”相关的技术演进历史简述一下,纯粹一篇科普文 。
图片来自 Pexels
本文专注于演化过程中每一步的为什么(Why)和是什么(What)上面,尽量不在技术细节(How)上面做太多深入 。
服务的三要素
一般而言,一个网络服务包括以下的三个要素:
地址:调用方根据地址访问到网络接口 。地址包括以下要素:IP地址、服务端口、服务协议(TCP、UDP,etc) 。协议格式:指的是该协议都有哪些字段,由接口提供者与协议调用者协商之后确定下来 。协议名称:或者叫协议类型,因为在同一个服务监听端口上面,可能同时提供多种接口服务于调用方,这时候需要协议类型(名称)来区分不同的网络接口 。需要说明在服务地址中:
IP 地址提供了在互联网上找到这台机器的凭证 。协议以及服务端口提供了在这台机器上找到提供服务的进程的凭证 。
这都属于 TCP/IP 协议栈的知识点,不在这里深入详述 。这里还需要对涉及到服务相关的一些名词做解释:
服务实例:服务对应的 IP 地址加端口的简称 。需要访问服务的时候,需要先寻址知道该服务每个运行实例的地址加端口,然后才能建立连接进行访问 。服务注册:某个服务实例宣称自己提供了哪些服务,即某个 IP 地址+端口都提供了哪些服务接口 。服务发现:调用方通过某种方式找到服务提供方,即知道服务运行的 IP 地址加端口 。基于 IP 地址的调用

最初的网络服务,通过原始的 IP 地址暴露给调用者 。这种方式有以下的问题:
IP 地址是难于记忆并且无意义的 。另外,从上面的服务三要素可以看到,IP 地址其实是一个很底层的概念,直接对应了一台机器上的一个网络接口,如果直接使用IP地址进行寻址,更换机器就变的很麻烦 。“尽量不使用过于底层的概念来提供服务”,是这个演化流程中的重要原则,好比在今天已经很少能够看到直接用汇编语言编写代码的场景了 。
取而代之的,就是越来越多的抽象,本文中就展现了服务调用这一领域在这个过程中的演进流程 。
在现在除非是测试阶段,否则已经不能直接以 IP 地址的形式将服务提供出去了 。
域名系统
前面的 IP 地址是给主机做为路由器寻址的数字型标识,并不好记忆 。
此时产生了域名系统,与单纯提供 IP 地址相比,域名系统由于使用有意义的域名来标识服务,所以更容易记忆 。另外,还可以更改域名所对应的 IP 地址,这为变换机器提供了便利 。
有了域名之后,调用方需要访问某个网络服务时,首先到域名地址服务中,根据 DNS 协议将域名解析为相应的 IP 地址,再根据返回的 IP 地址来访问服务 。

从这里可以看到,由于多了一步到域名地址服务查询映射 IP 地址的流程,所以多了一步解析,为了减少这一步带来的影响,调用方会缓存解析之后的结果,在一段时间内不过期,这样就省去了这一步查询的代价 。
协议的接收与解析
以上通过域名系统,已经解决了服务 IP 地址难以记忆的问题,下面来看协议格式解析方面的演进 。
一般而言,一个网络协议包括两部分:
协议包头:这里存储协议的元信息(meta infomation),其中可能会包括协议类型、报体长度、协议格式等 。需要说明的是,包头一般为固定大小,或者有明确的边界(如 结束符),否则无法知道包头何时结束 。
协议包体:具体的协议内容 。无论是 HTTP 协议,又或者是自定义的二进制网络协议,大体都由这两部分组成 。
由于很多时候不能一口气接收完毕客户端的协议数据,因此在接收协议数据时,一般采用状态机来做协议数据的接收:
接收完毕了网络数据,在协议解析方面却长期停滞不前 。一个协议,有多个字段(field),而这些不同的字段有不同的类型,简单的 raw 类型(如整型、字符串)还好说,但是遇到复杂的类型如字典、数组等就比较麻烦 。
当时常见的手段有以下几种:
使用 json 或者 xml 这样的数据格式 。好处是可视性强,表达起上面的复杂类型也方便,缺陷是容易被破解,传输过去的数据较大 。自定义二进制协议 。每个公司做大了,在这一块难免有几个类似的轮子 。笔者见过比较典型的是所谓的 TLV 格式(Type-Length-Value) 。自定义二进制格式最大的问题出现在协议联调与协商的时候,由于可视性比较弱,有可能这边少了一个字段那边多了一个字段,给联调流程带来麻烦 。
上面的问题一直到 Google 的 Protocol Buffer(以下简称 PB)出现之后才得到很大的改善 。

推荐阅读