缘起
最近在项目使用过程中需要使用到较简便的客户端/服务器网络通信模型,对数据进行实时拉取。要求并不多(貌似也不少),主要有以下几点即可:
- 简单:使用操作应该简便快捷
- 稳定:毋庸置疑,虽然目前不考虑并发使用(采集客户端相对较少)
- 通用:支持多语言多平台
- 支持老设备(目前我们需要通信的些许设备都是xp及其以前的Win2k)
通过前期资料收集,要么自己定义协议(简单来说就是实现一个简易版本的HTTP协议,发送请求返回相应),要么就是主流的RPC(Remote Procedure call)协议,即远过程调用。
不搜不知道,一搜索才发现RPC里面又是一个大世界,下面就先介绍下RPC的运行原理
RPC
什么是RPC
百度百科的定义如下:
RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
目的
通过像本地服务一样远程调用另外一台服务器上的服务来完成需求。既然说到是远程过程调用那么先至少得明白什么是本地函数调用这样才能更好理解下面的工作原理
本地函数调用过程
下面我们以最简单的一个求和方法调用进行说明
int result = Sum(2,4);
本地进程函数调用
执行这段代码的时候,传入参数[2,4]调用了本地代码段中的方法Sum,并将计算的结果返回。整个流程就是:输入参数,返回结果
跨进程函数调用
上例是在同一进程中进行调用,那么跨进程调用呢?跨进程就分两种了,本机进程间通信和跨机器进程间通信(该种方式就更符合我们需求了)
对于这种,我们就很容易想到,通过协定一个协议,然后用网络Socket通信,来传输参数,再在远端机器调用指定方法Sum,最后再把返回值原路返回
比如这个地方我们定一个协议,指定方法名称和参数如下图,服务端调用后再将结果返回
至此,应该对RPC应该也明白了大概,下文从网上盗图一张,进行更加详尽的讲解
工作原理
一图胜千言:
有了图,那么我们就看图说话,理理Client和Server之间整个数据通讯路径,对于C/S模型,客户端发送请求,服务端返回结果。就像我们访问百度一样,先输入一个搜索关键字,百度把它知道的相关数据返回给你,就完成了一次数据通讯。那么RPC的通讯路径如何呢?为了方便说明,我先理定一个场景:
-
服务端实现了一个当前时间的方法:CurrentTimeStamp()获取当前时间,我们暂定该时间是一个标准的时间戳。
-
客户端通过定期更新该时间作为本地系统时间
此时客户端要更新本机系统时间的流程,那么需要走哪些路径才能成功获取到数据,请看下文(此处可结合下文demo演示进行对照):
- 首先Computer 01作为Client,要更新系统时间,先要发起一次RPC调用,告诉服务端:“我要调用方法CurrentTimeStamp,请把时间给我吧!”。通过以调用本地服务的方式调用远程API。比如:proxy.CurrentTimeStamp()
- 此时Client就会调用Client Stub(客户端句柄),通过传入参数并进行数据封装(编码)成可以进行网络传送的结构体。包括:调用方法,方法参数(如果有的话)
- Client Stub调用本地系统的网络服务,发送封装好的结构体信息到服务端
- 通过网络传送消息到达服务端主机
- Server Stub(服务端句柄)收到发送过来的信息后,对信息进行解码,取出传送过来的参数
- 在Computer 02作为Server执行函数调用,获取结果信息
- 将结果信息发送给Server Stub
- Server Stub将消息封装,通过本地系统网络服务,发送网络信息到客户端
- 消息传送回Client主机所在网络系统
- Client Stub对收到信息进行处理解码
- 将返回信息返回给Client应用程序
至此就完成了一个消息调用过程,对客户端来说,就像调用本地方法CurrentTimeStamp一样获取到了服务器时间戳。对于用户来说根本不需要要关系这个时间是怎么来的,通过何种形式来的。反正我获得了我需要的结果。
为什么用RPC
那么为什么要用RPC?因为如上文这么一讲解,发现调用方太复杂了,需要关注许多底层细节,比如:
- 首先方法名、参数等得序列化吧,服务端还要反序列化
- 再通过网路协议进行传输,获取最终结果。
不用担心,RPC框架解决了这些中间繁琐的调用,我们只需要关系业务逻辑,针对业务写接口即可
RPC框架包含两个重要部分:
- 传输协议; 比如gRPC 基于http2协议,Xml-Rpc、Json-Rpc都是通过HTTP传输,而其他多数基于TCP设计的自定义协议
- 编码协议;说白了就是对数据的序列号和反序列化。对于编码协议有基于文本编码的xml、json,也有基于二进制编码的protobuf。从性能来说当然二进制编码比基于xml和json相对好些,能减少带宽,更加安全,但是撇开这些,其他方式也还是可用
目前市面上这么多RPC框架,主要还是在不断实践中自己提炼和总结实现了一套更符合自己业务员场景或者更加通用的框架供大家使用。就像目前我们使用的语言,官方语言是普通话,我们还有各个地方的方言。目的就是一个沟通。
RPC主要是用在大型企业里面,因为大型企业里面系统繁多,业务线复杂,而且效率优势也是非常重要的一块,同时目前服务器分布式部署、多机器、多机房等,这个时候RPC的优势就比较明显了 。在项目实现上,只需要定义好接口,服务端实现该接口,客户端直接调用即可。
XmlRPC
什么是XmlRPC
每每对于下定义,我还是觉得百科能说得更加清楚,维基百科定义如下:
Xml-RPC是一个远程过程调用(remote procedure call,RPC)的分布式计算协议,通过XML将调用函数封装,并使用HTTP协议作为传送机制。
而在XmlRPC中数据使用XML格式的。那么为什么用XML而不用二进制呢?我想一方面应该是为了兼容更多的语言,因为这个世界上除了C/C++、Java、C#等编译语言, 还有很多脚本语言(Python、JavaScript等)。XmlRPC协议规定发送请求时,通过统一规则发送命令和参数,更好兼容多平台、多语言。任何事情都有两面性,兼容性好那么性能相对就会慢下来
对于XmlRPC的命令、参数的协定请参考维基百科中XML-RPC
工作原理
XmlRPC客户端工作原理
- Client根据指定URL找到服务端地址
- 然后编码请求数据
- 调用服务端上的指定服务的方法
- 接收到服务端的返回,解析响应包,拿出调用的返回结果
XmlRPC服务端工作原理
- 启动一个服务程序, 注册每个能提供的服务,每个服务对应一个Handler类
- 进入服务监听状态
- 等待Client的请求
优缺点
优点:
- 简单、轻量
- XML编码,可读性强
- 支持多语言多平台
缺点:
- 对字符的编码较弱,中文编码可能得需要通过base64了
- 浪费带宽,比如就传递两个参数,需要发送一大堆无用的xml节点
- 复杂数据结构支持不够好
孰优孰劣,具体看应用场景,对于目前我的需求来说,简单使用足矣
服务端实现
下面我们就通过C++、C#和Python来实现一个简单的RPC调用
人生苦短,我用Python!那么我们就先用Python来实现一个时间戳服务器,短短几行代码就可完成
# -*- coding: utf-8 -*-
"""
@author: iceman
"""
import xmlrpc.server
import time
def CurrentTimeStamp():
cur_time = time.time()
loc_time = time.localtime(cur_time)
return time.strftime('%Y-%m-%d %H:%M:%S', loc_time)
def main(port = 8000):
with xmlrpc.server.SimpleXMLRPCServer(('localhost', port)) as server:
server.register_introspection_functions()
server.register_function(CurrentTimeStamp)
server.serve_forever()
if __name__ == "__main__":
main()
客户端实现
Python测试:
# -*- coding: utf-8 -*-
"""
@author: iceman
"""
import xmlrpc.client
def main(port = 8000):
with xmlrpc.client.ServerProxy('http://localhost:' + str(port)) as proxy:
cur_time = proxy.CurrentTimeStamp()
print (cur_time)
if __name__ == "__main__":
count = 100
while(count > 0):
main()
count = count-1
C++测试
C++我这使用了一个三方库XmlRpc++ ,该库使用简单就不做过多解释,拿来即用
#include "XmlRpc.h"
#include "XmlRpcValue.h"
#include <iostream>
using namespace std;
using namespace XmlRpc;
#pragma comment(lib, "xmlrpc.lib")
#pragma comment(lib, "ws2_32.lib")
int main()
{
XmlRpcClient client("127.0.0.1", 8000);
XmlRpcValue numbers;
XmlRpcValue noArgs, result;
if (client.execute("system.listMethods", noArgs, result))
cout << "Methods:\n " << result << endl;
else
cout << "Error calling 'listMethods'" << endl;
if (client.execute("CurrentTimeStamp", noArgs, result))
cout << "Current time: " << result << endl;
else
cout << "Error calling " << endl;
return 0;
}
//Output:
Methods:
{CurrentTimeStamp,system.listMethods,system.methodHelp,system.methodSignature}
Current time: 2018-05-19 11:38:40
C#测试
使用C#时,本文引入第三方库CookComputing.XmlRpcV2 ,相应库较多,可自行测试
using CookComputing.XmlRpc;
using System;
using System.Collections.Generic;
namespace XmlRpcTestClientCSharp
{
public interface ICurrentTimeStamp: IXmlRpcProxy
{
[XmlRpcMethod("CurrentTimeStamp")]
string CurrentTimeStamp();
}
class Program
{
static void Main(string[] args)
{
string url = "http://localhost:8000";
ICurrentTimeStamp proxy = XmlRpcProxyGen.Create<ICurrentTimeStamp>();
proxy.Url = url;
proxy.Timeout = 4000;
string[] methodlist = proxy.SystemListMethods();
Console.WriteLine(string.Join(", ", methodlist));
string time = proxy.CurrentTimeStamp();
Console.WriteLine("Current time : {0}", time);
}
}
}
欢迎关注交流共同进步