编程十年之那些年我见过和用过的RPC

如果从我在Code::Blocks上写下的第一行代码开始算起,到现在也快十年了。十年之间,经历了互联网的流行,Web 2.0的兴起,社交网络的蹿红,移动互联网的爆发,以及物联网时代的前奏。伴随着每一次IT革命,与编程相关的各种技术也在不断的演进。各种编程语言,各种IDE,各种框架和技术,对于IT从业者,这是一个最好的时代。我希望能够用几篇文章来回顾过去十年,我所经历过的那些技术变迁。

本文是这个系列的第一篇,回顾过去十年,那些我见过和用过的RPC技术。

那些RPC

在互联网还没那么流行的年代,大家的电脑上装的都是单机软件,通信基本靠软盘和光盘。自从有了互联网,计算机的世界就不一样了。计算机上的程序,不再是一个个独立的小东西,它们开始学会和其他计算机上的程序交流。人与人之间交流,我们管它叫做社交;而程序与程序之间交流,我们称之为RPC(Remote Procedure Call,远程过程调用)。

RAW socket

我参加的第一个真正意义上的项目,是在大学里和一群计算机系的学长们,用一个十一长假,做一个叫做KIT的即时聊天软件,这个KIT取的是Keep in Touch的寓意。客户端技术栈是C++/Qt,服务端是C++/Linux。那时候,我只知道电脑插上网线,就可以上网,至于两个电脑之间是如何联接,数据是如何从一个电脑发出去,另一个电脑如何收到数据,我一窍不通(虽然我的专业是电子信息工程,但我们却不学计算机网络)。还好和我一起负责客户端开发的学长知道有Socket这么个东西,照着Qt手册上的例子,我们就这么艰难的开始了。

我们首先和开发服务端的两个同学约定好要传输什么数据,比如:一条消息的前两个字节代表消息来源,后两个字节代表消息目的地,然后就是消息内容;客户端获取在线用户列表,服务端返回的数据就是一个3*N个字节的数据,每一组3字节是由一个2字节的用户ID加上1字节的状态标示。看起来没什么问题,照着这个约定开工写代码。

大概用了两天时间,代码写好,两拨人决定测试发个消息看看。不出所料,失败了。客户端的消息,服务端是接收到了,但是转换到代码里,却有两个小问题:1) 源ID和目的地ID的值不对,2) 消息内容都是乱码。简单来说,就是整条消息没有对的。然后又花了一天时间,大家不停地尝试发送不同的数据,看看到底是什么问题。乱码问题很快确认了,从windows发出的数据是ANSI编码,Linux接收的时候是一UTF8解码,自然就错了。ID错误的问题花的时间更久一些,最后发现其实是大端(Big-endian)和小端(Little endian)的问题。

解决了那两个“小问题”,我们欢欣鼓舞。在继续开发功能时,发现原来设计的数据格式有问题,发送的消息里面没有带上时间信息。很简单,加上就完了嘛。但当时在做服务端端的同学正在做其他功能,于是我们客户端就先做了,在两个ID之后,也就是第5个字节开始,用4个字节表示时间。过了一天,服务端的同学说他们也做完了,我们可以再测一下。于是,不出所料,又失败了。这一次是因为做服务端的同学不知道我们把时间放在了两个ID之后,他们把时间放在了最前面。

除了上面遇到的几个问题,我们后面还遇到了Socket池性能优化,公网穿透,大文件传输等等问题。总的来说,在解决通信问题时,选择用原始Socket真的是需要勇气和能力的。

COM/COM+/DCOM

从上面的例子可以看出,要想让不同应用程序之间能够通信,需要很复杂的工作,最基本的是确保通信双方能够在网络中找到对方,初次之外,还要保证通信双方以同样的方式理解数据。COM(组件对象模型,Component Object Model)技术是在这个方向上的一次成功尝试,其中COM/COM+首先尝试解决后者,他们的分布式升级版本DCOM则解决了前者。

COM技术的目标是让使用不同语言编写、运行在不同进程甚至不同计算机中的程序可以通过组件对象模型来进行统一建模,使得不同程序之间可以用同样的方式来调用彼此。在调用COM接口时,调用者除了需要知道调用的方法和参数,不需要知道任何被调用者的细节。面向对象技术在当时刚刚兴起,微软在设计COM时就非常成功地利用了面向对象强大的抽象能力,将RPC抽象成了简单的方法调用,让RPC用起来就像是本地方法调用一样。尽管COM技术后来逐渐衰落,但它给其继任者指出了一条阳光大道。

COM概念的提出,应该说也是很有超前意识的,但微软的问题就就在于,总是希望用一种技术作为标准,统一所有其他技术。COM本应是一次很好的尝试,但是为了成为大一统技术,可能也受到了当时IT领域追求完全面向对象的过渡煽动,结果结果就导致了过度抽象。

毕业工作后,我参加的第一个项目,是一个用来替换windows原生桌面的应用软件。当时我选择界面部分使用HTML/JavaScript来渲染,底层通过C++和操作系统进行交互。JS引擎和C++程序之间的通信,就是COM。举一个例子,用户在界面上双击一个图标,这个程序就会被打开,双击操作是在JS代码中捕获,打开程序是通过C++调用系统API来执行。按照COM设计的初衷,在JS代码中应该看起来像是在执行本地代码:

1
2
3
icon.on("click", function (app) {
sys.open(app)
})

而实际上,sys.open这个方法是C++提供的,通过COM导入到JS中的。在COM中,所有的方法调用,都被建模成一个对象的invoke方法调用,调用时需要传入方法名,和参数列表。app这个对象包含了要打开的应用程序信息,比如路径,启动参数等等。但问题在于,这个参数是JavaScript对象,在C++这种静态语言中中如何匹配这个动态语言的对象呢?COM中引入了个叫做variant_t的类型,实际上就是在静态语言中对动态类型数据的一种封装,这样就可以在运行时将数据转换成需要的类型。微软的工程师们确实强大,在那个连C++都还没形成标准的年代,就实现了类似现代C++中RTTI的特性。但带来的问题就是,我花费了大量的精力在数据格式映射、接口参数调试等工作上。而最终我放弃了让JS对象直接映射成C++对象的尝试,选择在传递参数时先序列化成字符串,然后在C++中进行字符串解析。

作为COM的分布式版本,DCOM在使用者看起来,和COM没有区别,只是在部署时需要比较复杂的配置,将各个组件注册到各个客户机中。DCOM在刚刚推出时,算是引领潮流的一项技术,甚至很多Unix产品以及IBM大型服务器中都开始使用DCOM。但没过几年,微软就转而去推广其基于XML的分布式解决方案,先是SOAP,接着是WCF,这些我们后文会讲到。

以上对于COM的评价只是基于我个人的使用经验,如果要深入理解COM,可以看看侯捷老师的《COM本质论》。

CORBA

除了COM,在RPC技术领域的另一个先驱,是OMG组织提出的CORBA标准。作为跨主机的分布式解决方案,CORBA比DCOM的出现还要早几年。在那个时代,大家对于计算机之间如何通信还没有一个确定的方案,TCP/IP协议栈也不过是众多协议栈中的一个选择。看看维基百科上CORBA的目标:

CORBA enables communication between software written in different languages and running on different computers.

以当时的技术背景,“different computers”不是我们现在理解的不同计算机,这里的不同,是真的不同,包括架构不同,系统不同,协议栈不同等等。

在解决不同应用程序之间如何找到对方,并可靠地将数据传输过去方面,CORBA做了非常有意义的尝试。CORBA提出了IDL(接口定义语言)的概念,通过一个中间语言来定义接口,屏蔽了不同编程语言的异构性;CORBA的另一个重要概念是ORB(对象请求代理),应用程序只需要向ORB发起本地请求,ORB负责找到目标对象,并完成消息传递。后来的很多使用中间语言来描述通信接口的技术,估计都是受了CORBA的启发;而消息代理模式,在后来的大多数消息中间件中,都成了标配。

关于CORBA,大多是从我的一些同事那里道听途说过的一些故事。同事的项目是一个由40多个不同的节点组成的分布式系统,由CORBA负责节点之间的通信,而描述各种消息格式,文档写了几十页。而每次启动整个系统,需要至少5个人,分别在40多个节点之间穿梭,按照顺序启动每个节点上的程序。

另外一个值得一提的事情是,CORBA的衰亡,和它流行的速度一样快。

The Rise and Fall of CORBA一文中,比较详细的介绍了为什么CORBA会衰落。总结起来有几点:

  1. 标准不够完善,导致各个商业版本的实现不一致(后来的Web其实也有这个问题);
  2. 功能跟不上时代,互联网的快速发展对于安全通信提出了需求,但CORBA并没有提供这方面的支持;
  3. 接口定义复杂,暴露过多细节,对开发人员要求高,容易出现bug;

其实,出现这些问题,并不是CORBA的设计者能力不足,而是因为在那个时代,业务发展快过技术发展,对于分布式系统的通信,大家并没有很好的全局感。在一个通信系统中,哪些是稳定不变的,哪些是随业务不断变化的,该如何抽象和分层,程序员们还没来的及很好地思考这些问题,就已经有客户付钱要买了。所以CORBA作为一个分布式系统框架的半成品,在没有经过足够验证的情况下,就快速推向市场,最终导致了失败。不过,CORBA也有其积极意义,后来的一些分布式系统框架,或多或少都有些CORBA的影子。

SOAP

在学校期间,我就用过SOAP做项目了,但是直到现在,我对于SOAP那些巨大、复杂的XML,仍然望而生畏。

IT技术的发展,总是伴随着一个又一个“银弹”的提出和破灭。SOAP是其中之一。

COM和CORBA在九十年代以及21世纪初都是RPC的主流技术,但是他们配置复杂,运维困难,授权费用高昂,尤其是他们号称能够解决编程语言和平台的异构性,但实际上,即使是同一种编程语言,同一种平台,要使用COM和CORBA进行通信,在开发过程中依然存在数据格式匹配失败的问题。就像我在用COM进行开发,最终放弃二进制数据格式直接映射,而是选择用字符串作为传输媒介一样,大家也逐渐意识到,直接在二进制数据上进行数据转换,太容易出错,而且灵活性非常差。

就在这个时候,XML被提出来,给所有程序员带来除二进制之外的另一种数据描述方式。基于XML的通信标准SOAP也顺势被提出来。可能是类似COM和CORBA这样的技术给大家带来的痛苦过于深刻,也可能是IT从业者从来都是好了伤疤忘了疼,大家根本来不及详细评估SOAP是否真的如其宣传的那么美好,依然迫不及待的送上自己的膝盖,即使它有明显的性能问题。使用基于XML的SOAP协议,不论数据类型,还是方法描述,都有XML来定义;大家再也不用纠结于不同编程语言之间数据如何映射,因为都是XML,各个语言只需要实现各自的XML解析器就可以了;一切都是XML,多么美好。在IBM(Java)和微软(.NET)两大技术体系的强力推动下,再辅以SOA概念的美好愿景,SOAP很快就成了企业级开发的标准实践。

然而,现实并没有想象的那么美好。和CORBA相比,SOAP的简单体现在:

  1. SOAP隔离了传输相关细节和业务相关细节,WSDL中定义的是服务的地址、方法以及参数,关于如何传输,要么通过HTTP直接传输,要么通过ESB传输,但SOAP本身并不关心;
  2. 基于标准化的XML,避免在解析数据时产生歧义,程序员再也不同纠结,到底哪个字节才是数据的开始,或者这个字节到底是什么类型;
  3. SOAP具有自描述能力,程序员只需要阅读WSDL定义,就可以了解服务如何调用,很多框架甚至提供根据WSDL自动生成客户端的工具。

然而,SOAP也不过是解决了CORBA和COM遗留的一些严重问题,作为分布式系统的通信标准,SOAP依然只是个半成品。因为:

  1. SOAP以XML描述数据,虽然XML是文本方式,但对于每一个数据,SOAP还是要为其标记类型,因此SOAP依然存在数据映射的问题,尤其是SOAP还可以定义自定义类型;
  2. 因为SOAP通信构建在HTTP之上,所以只支持点对点通信,不能做广播和通知;
  3. 一个很简单的字段,以XML描述数据,可能会占用十倍以上的空间,导致每一次数据交互,都包含大量的冗余信息,性能非常差;
  4. 尽管SOAP相比CORBA和COM简化了很多,而且利用了HTTP,所以其配置的大部分复杂性都交给Web Server去处理,但SOAP本身用来描述服务的结构,依然很复杂。

SOAP能够在企业级软件开发中生存很久,很重要的一个原因是,大家在工作中用到的OA系统、报表工具、CRM系统等,即使慢如蜗牛,大家也不会太多抱怨,趁着网页卡在那里的时间,正好喝喝茶,聊聊天;而CTO们更不会有抱怨了,因为他们只负责买,又不负责用。

ESB

仅仅一个SOAP,要想卖给企业CTO,看起来还是单薄了一些,毕竟把所有基于COM或者CORBA的系统都换成SOAP服务,仅仅是减轻了程序员的负担,对于经理们没有任何帮助嘛。这时就需要功能更强大,愿景更美好的解决方案来给CTO们一个掏钱的理由。

ESB(企业服务总线)借着SOA概念的流行,在2000年之后快速兴起。很多公司在宣传ESB时都描绘了这样一种未来:公司里的各个业务都做成服务,接到服务总线上,当你需要定制一个新的业务时,只需要拖拖拽拽、勾勾选选,就可以实现。很多公司都被这个美好的大饼所迷惑,不惜巨资,购买ESB,改造IT系统。

ESB确实在一定程度上做到了它所承诺的事情。比如,一个银行IT系统有个人存款查询和转账两个服务,都接入到ESB上,通过ESB提供的图形界面,将两个服务的接口对接,就可以实现转账功能。更神奇的是,这两个服务可以是用不同语言编写,互相不需要知道对方部署在哪里。听起来很不错,不是么?

当年的我们,也是被这个大饼迷住了。我们设想了把成百上千个服务接入到ESB中,让不懂技术的领导们,只需要动动鼠标,就可以想做什么就做什么。但是,随着系统逐渐复杂,服务越来越多,我们最终发现,童话里都是骗人的。为了编排那么多服务,我们甚至用上了BPEL(如果你不知道BPEL是什么,你很幸运,最好永远不要知道它是什么),简直就是灾难。

使用ESB的另一个问题,是协议转换。这本来是ESB的一大卖点,但是最后也成制约我们项目使用ESB的地方。我们使用的ESB有一个中间描述语言,XML格式的,在将开发好的服务对接到ESB之前,需要将服务以这个中间描述语言描述出来。如果服务本身就是SOAP,很简单,因为这个中间描述语言本来就是参照SOAP标准中的WSDL(Web服务描述语言)设计的。但是如果这个服务是一些非标准化的,比如服务本身是基于TCP传输的,描述起来就很困难。导致我们不得不改造很多已有服务,而这些成本,在引入ESB时,供应商并不会告诉你。

ESB的初衷是好的,它希望能够在SOAP的基础上,解决其只能点对点路由的问题,同时还可以兼容不同通信协议。但是,为了解决一个问题,它引入了另一个问题,就是业务逻辑与ESB绑定,导致ESB变得越来越不“单纯”,最后ESB越来越大,越来越复杂,越来越难以维护,最后就成了整个系统的痼疾。

WCF

WCF是微软在实现大一统梦想的路上,推出的又一力作。WCF继承了COM的衣钵,希望以一种统一的方式,将TCP通信、HTTP服务、SOAP服务、REST服务等等,都封装起来,在开发者看来,都是一个本地方法调用。

在学校实验室里,我做过一个小项目,在城市里布置一些带有传感器的嵌入式设备,收集到的数据,通过GSM模块发给中心服务器。为了技术尝鲜,我当时用了WCF来做各个组件之间的通信。和SOAP一样,WCF也是通过XML来描述服务,框架很重,描述文件复杂难懂,但事实上这东西很好用,因为Visual Studio实在是太强大了。Visual Studio会根据服务生产者代码中的标签,自动生成XML描述文件;而在开发服务消费者时,只要指定一个服务端地址,Visual Studio就会自动访问这个地址,获取服务描述,生成客户端代码,甚至还可以选择是生成同步方法,还是异步方法。程序员要做的,就是在需要的地方调用一下方法就可以了。

WCF其实就是更高级好用的SOAP,但本质上还是SOAP。除了可以通过封装TCP通信在某些场景提高了性能外,其他SOAP的问题,它都存在。而WCF还有一个SOAP不存在的问题,就是和.NET平台绑定。像很多微软的其他技术一样,虽然走在时代前列,但非微软系的程序员并没有广泛接受它。在那个以黑微软为乐的年代,大家对微软推出的技术总是嘴上不屑一顾,身体却很诚实的借鉴它的思想,用各自的方式,重新实现一遍。我的同事们做的MicroBuilder是这类尝试中比较成功的一个,类似Visual Studio上对WCF的支持,MicroBuilder是对REST服务提供了自动生成中间语言描述文件,以及自动生成调用客户端代码的能力。

DDS

CORBA的快速陨落,让CORBA的提出者OMG痛定思痛,重新提出了一个标准——DDS(Data Distribution Service,数据分发服务)。这个标准最早在2001年开始成形,2003年DDS 1.0标准正式提出。DDS的基础是以发布订阅(Publishi-subscribe)的方式进行无中心实时数据传输,支持QoS定义,可水平扩展等能力。这是一个工业级的数据协议,早期是用在仪器设备之间进行实时数据通信,在美国有些军用设备上也有使用,后来开始用在金融、航空领域,近些年,随着物联网的流行,DDS已经将重点转向这个领域,2015年发布1.4版本发布,就是针对物联网应用做了很多改进。

作为实时数据传输协议,商用的DDS实现可以做到微秒级别的延时,同时还可以支撑大规模并发通信。时至今日,互联网技术圈中以性能著称的消息服务Kafka,在性能指标上依然难以望其项背。其实互联网看起来很热闹,其实很多时候都是在重新发明/发现那些在其他领域早已成熟的技术。DDS之所以能够做到这么快,是因为它的RTPS协议(Real-Time Publish-Subscribe Protocol,实时发布订阅协议),这个协议是基于UDP的,通过ACK心跳和消息接收回执来保证消息的送达,避免了TCP协议漫长的三次握手。但用UDP也有一个问题,就是不能跨网段,于是有公司提出通过一个中心的桥接器,以TCP连接将两个私有网络连接起来,私有网络内部采用UDP通信。

DDS看起来如此强大,为什么没有火起来呢?首先,DDS在工业领域其实很火,在互联网领域只是大家没有听说;其次,目前大部分DDS的实现都是商用版本,要想使用,需要付高额的授权费用,之前我们在考虑用RTI的DDS来代替CORBA,所以了解过一点它的价格,真心不是一般公司用得起的;最后,DDS因为需要在计算机上安装RTPS协议栈,对于服务器的侵入性比较强。对于喜欢在通用硬件上使用开源软件的互联网公司,这几点都是比较致命的,而且其带来的从毫秒级别到微秒级别的提升,对于互联网公司的服务对象(主要是人)来说,感受并不明显,所以DDS没有进入消费级互联网的技术视野也是可以理解的。不过可以预期的是,在物联网领域,以Kafka为首的互联网数据通信解决方案和以DDS为首的工业级数据通信解决方案,会有一战。

REST

最近两年,REST(Representational State Transfer,表述性状态转移)几乎成了Web API的默认标准。SOAP仅仅将HTTP作为一种可选的数据传输方式,REST则是带着HTTP基因出生的。首先,REST将服务间通信进行了一次抽象,所有的服务交互都建模成对资源的CRUD操作;其次,它充分利用了HTTP协议中丰富的动词,遵循约定大于配置的互联网开发哲学,避免了复杂和冗余的方法定义;再次,由于通过HTTP的动词可以明确区分读写操作,对于GET类的读操作,还可以通过缓存或者CDN进行加速;最后,采用JSON或者XML等通用格式来描述数据,简单,直接。基于这几点,REST开始在互联网领域大行其道。我最近两年做过的所有Web项目,几乎都是以REST方式进行通信。

如此说来,REST不就是真正的银弹了么?非也。

REST看起来简单易懂,但是要想设计出合理的REST API,也是需要很多经验和技巧的。很多人觉得,我用JSON作为数据格式了,就是REST API;或者我用POST和GET来区分读写操作了,就是REST了。到底一个REST API设计好坏如何评判?Martin Fowler的Richardson Maturity Model有比较详细的介绍,至今我所见到的REST API中,能够做到这个成熟度模型中最高等级的,寥寥无几。

REST的核心是Resource,对Resource的CRUD操作构成了整个业务系统。对于简单应用,这种抽象足矣。但对于稍微复杂一些的业务,仅仅是对于Resource的CRUD就不够用了。举一个简单的例子,对于一个云计算服务,虚拟机是一个Resource,用户可以对虚拟机执行创建、启动、关机、重启、强制重启、销毁等等操作。其中,创建虚拟机的操作可以通过发起POST请求给/VirtualMachines来实现;查看虚拟机,可以发送GET请求给/VirtualMachines/{vm id}。但是,如果是要启动、重启或是强制重启一个虚拟机,这个请求应该怎么设计?比较直接的想法是,发送一个POST请求到/VirtualMachines/{vm id}?operation=boot|reboot|force-reboot,或是将operation=boot/reboot/force-reboot作为POST请求的body发给/VirtualMachines/{vm id},但是,这样的设计并不符合REST的设计理念。在REST中,所有的行为,都应该是对资源的CRUD,而POST正是对应于资源的创建。上面这种做法,发送了POST请求,却没有创建任何资源,这是违反REST初衷的,对于服务消费者来说,这样的API也可能产生疑惑。如果是用资源的角度来看,不同的操作,其实是资源的状态转换,比如启动操作,最终结果是将虚拟机的状态变成“running”。那发起一个PUT请求给/VirtualMachines/{vm id},将status字段改为running,就可以了。但是,重启和强制重启怎么办呢?因为从状态转移上看,虚拟机最终对变成了running,也许可以通过发送两次请求达到目的,一次将虚拟机状态设置为“stop”,另一次将虚拟机状态设置成“running”,但这种实现任谁看起来都会觉得很荒谬。在这个场景中,通过更新(Update)资源属性来实现复杂功能,会导致丢失很多重要的业务信息。这就是REST的简单带来的局限性,它的思想非常简单,就是对业务中出现的名词建模,因此很难表达复杂业务中出现的动词语义。解决这个问题的一种方案,就是将动词转换为名词,比如上面的bootrebootforce-reboot等操作,可以转换为BootOperationRebootOperationForceRebootOperation等资源,这样就可以给/VirtualMachines/{vm id}/BootOperations/VirtualMachines/{vm id}/RebootOperations/VirtualMachines/{vm id}/ForceRebootOperations等资源发送POST请求,来实现启动虚拟机。

另一个设计REST API的问题,在于多大的粒度才是合适的。粒度大了,服务不够灵活,每次通信发送的数据冗余;粒度小了,客户端要调用多次才能API才能完成一个操作,导致通信次数过多;如果一个本该在同一个事务中的操作被拆分成多个细粒度的API调用,可能会导致一致性为题。比如,一个典型的例子是银行账户进行转账,要从用户A的账户中扣款(修改id为A的UserAccount资源的的money属性),然后转入用户B的账户(修改id为B的UserAccount资源的money属性),如果API粒度设置的比较细,这一个操作就需要分多次API请求才能完成,而且如果在第二次请求时出现错误,数据一致性难以保证。而如果以粗粒度的资源来建模,在增加新的业务时,就不那么灵活了。

对于REST如何进行有效地资源建模,这篇文章有更详细的论述。

通信的本质

上面讲的几种通信方式,每一种的提出,都是为了解决当时的问题,但其实每一种解决方案都带来了新的问题。我们不断的提出新的框架、新的技术,去解决上一个框架和技术的问题,最终,我们都忘了我们最初要解决的问题是什么。

让我们追本溯源,看看RPC的原始问题——通信到底是在干什么?作为通信专业的研究生,我还记得,在通信原理的课本中,是这么定义通信的:通信的本质,是将信息信源有效、可靠的发送给信宿

通信基本模型

但是,信源到信宿这个过程,是需要一个中间媒介来传输信息的,这个媒介叫做信道。

通信基本模型

这里面的信道,可以是同轴电缆、光纤或者是自由空间,因为信道会对信号产生衰减,导致在信宿收到的信息,可能和信源不一致。为了让信息更可靠的传输,就需要在信息进入信道之前,进行预处理,在信宿从信道中取出信息时,再进行逆向处理,得到原始信息。这一步骤称为信道编码。

通信基本模型

信道容量有限,也就是说,传输数据的速度不是无限大的。为了尽可能传输更多的信息,就需要在信源将信息进行压缩处理,去除冗余,这样就可以利用有限的信道容量提高信息传输速率,这一指标也被称为通信系统的有效性。将信源在发送前进行预处理,这一步骤称为信源编码。

通信基本模型

以上,是通信专业对于通信系统建立的一个经典模型,现在我们尝试将这一模型用在计算机的通信系统中:

  1. 信源和信宿是两个应用程序,功能业务逻辑都在这里;
  2. 既然TCP/IP协议栈已经成为默认通信标准,我们就将TCP/IP协议栈的传输层以下,都看做信道;
  3. 在传输层中实际传输的数据,可以看做是经过信道编码处理过的数据;
  4. 而信源编码则是应用程序在发送数据前对数据进行的序列化操作。

将上文中提到的技术,对照这个定义,我们来看看各个通信方式分别对应着通信系统中的哪些部分:

通信基本模型

  • raw socket: 直接在socket上编程,相当于应用程序自己将信源编码与信道编码都处理了,有点像是本节的第二张图;
  • COM/COM+/DCOM: COM技术希望屏蔽技术异构性,让两个程序之间像是在同一个进程中相互调用,数据类型的转换,内存的映射,以及通信方式,都在COM中处理了,所以可以将其归类为实现了信源编码与信道编码的技术;
  • CORBA: CORBA是对信道做了一层封装,虽然也做了些类似信道编码的工作,但其实它暴露了很多底层信道的细节;
  • SOAP: SOAP其实就是XML版的COM,传输在HTTP之上,所以它和COM一样,属于具有信源编码和信道编码能力的技术;
  • ESB: ESB在SOAP的基础上更进一步,还带有一些业务编排能力,所以ESB做的事情,有一部分是属于应用程序的;
  • WCF: WCF算是改良的SOAP,和SOAP分在一样的类别中;
  • DDS: DDS做的事情和CORBA接近,但是比CORBA设计的更合理,它对于信道编码这一层的封装,完全屏蔽了底层细节,让使用DDS的开发者,只需要考虑消息的设计,以及发布订阅关系;
  • REST: REST算是构建在HTTP上的一种约定标准,如果仅仅看REST,实际上它是介于信源编码与信道编码之间的那薄薄一层,因为REST并没有限定其信源编码必须是JSON还是XML,事实上,你可以用YML表示数据,甚至还可以用自定义的数据格式。但是,因为其限定了传输必须是通过HTTP发生,所以传输的信息必须是文本的。如果将HTTP和REST放在一起来看,它就属于信道编码加上一点点的信源编码。

以目前发展的趋势来看,大一统形式的技术,比如ESB,逐渐没落,职责单一的技术越来越受欢迎。

微服务系统中的通信

微服务作为一种架构设计理念,在最近一年多越来越流行,因为其松散耦合的架构特别适合当前IT领域需求变化特别快、技术更新特别快的环境。在进行微服务系统设计时,一个非常重要的问题,就是如何选择服务之间的通信机制。

本着先定义问题,在解决问题的思路,我们先来看看微服务的系统特点,再来决定那种技术合适。

微服务的一个最直接特点是“微”。因为一个服务只负责实现一个特定的业务功能,所以一个服务通常都不会很大,在一个代码量不大的程序中,用于通信的代码通常也不会很多,所以注定用在微服务上的通信技术,必须是轻量级,同时使用简单。所以SOAP这种重量级的技术,以及CORBA这种过于复杂的技术在这里就不是特别适合了。

微服务的提出,是为了应对业务需求的快速变化,而通信本身和业务无关,为了满足业务快速变化导致的服务频繁迭代,微服务的通信必须和业务剥离。因此,ESB和raw socket就不在选择之列了。

微服务的另一大特点,是各个服务可以用完全不同的技术栈来实现,因此对于通信的要求,最好是能够具备平台无关性。于是COM和WCF就不不行了。

如此筛选下来,还剩下两个候选技术,REST和DDS。所以说,REST在微服务盛行的今天,被如此推崇,作为微服务的首选通信技术,也是有其道理的。另外,DDS虽然没有在微服务中广泛使用,但是其后辈们,包括RabbitMQ,ZeroMQ,Kafka等技术确实是紧随REST之后的次选微服务通信技术。

事实上,虽然微服务并不要求实时的通信性能,但是,因为一个系统拆分成了很多个服务,来自用户的每一次请求,都可能会在多个服务之间多次交互,为了保证对用户的快速响应能力,微服务的通信性能应该是越高越好。但是,一方面,基于消息的通信需要从系统架构层面进行设计,而且对于开发者要求比较高;另一方面,对于用来公开的API,基于HTTP的REST协议,对于一般用户来说更容易接受。所以目前一种更合理的微服务通信架构,是系统内部各个服务之间,采用基于消息的技术,而对外提供API则是采用REST。

当然,随着HTTP/2的发展,相信REST的性能也会越来越好,也有可能会出现完全基于HTTP的消息技术,到时候,选择哪一种,就真的是仁者见仁,智者见智了。