1 引言
在eos中,一般我们都有client和server两个应用,server端写了服务PersonService,在client中直接可以调用,上代码:
PersonService service = com.sunsharing.eos.client.ServiceContext.getBean(PersonService.class);
Map map = service.exportData(batchNo,key));
这时想起类似的两个场景:
1)随着页面的复杂,服务越来越多,部署在不同的机器上,如何简单方便的让远程服务的调用如同本地服务调用一般?
2)甚至可以联想到前端ajax的调用,浏览器端如何调用后端所写的服务?
上述有几点表现:
客户端与服务端分属不同空间内存区域
透明调用机制让使用者不必显式的区分本地调用和远程调用
说了这么多,其实,我只是想聊聊RPC...
2 何为RPC
RPC(Remote Procedure Call Protocol),即远程过程调用。通俗的讲,两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。RPC让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。
一个简单RPC的过程是这样的:
(图片来源:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html)
这其中包含了以下流程步骤:
client调用以本地调用方式调用服务
client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
client stub找到服务地址,通过网络将消息发送到服务端
server stub收到消息后进行解码
server stub根据解码结果调用本地的服务
本地服务执行并将结果返回给server stub
server stub将返回结果打包成消息并返回给client
client stub接收到消息,并进行解码
client得到最终结果
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明,以适应引言中提出的场景。RPC 在整个过程中,体现了逐层抽象,将复杂的协议编解码和数据传输封装到了一个函数中。
3 RPC过程分析
说起来其实挺简单的,但是做起来,还有很多的细节!
3.1 如何做到本地服务存根调用
对于流程步骤1,client中是没有该服务实现的,我们需要让client stub接收到调用时候,能够执行步骤2的事情。对于java来说就是使用代理。java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码不易维护,大部分公司实现RPC框架时还是选择动态代理方式。
public <T> T getProxy(final Class<T> clazz,final String implClassName) throws RpcException {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = new Object();
//dosomething...
return result;
}
};
T t = (T) Proxy.newProxyInstance(RPCClient.class.getClassLoader(), new Class[]{clazz}, handler);
return t;
}
流程步骤5,server端中解码后需要执行调用实际服务实现。java 中实现代码的动态接口调用除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用。
Method m = obj.getClass().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
Object o = m.invoke(obj, invocation.getArguments());
3.2 怎么对入参和结果进行编码和解码
对于流程步骤2、6、4、8,在client和server之间,我们需要有一个两方都能认识的消息作为交互通讯的媒介。(此部分属于摘抄)
3.2.1 确定消息数据结构
客户端的请求消息结构一般需要包括以下内容:
服务接口名称
方法名
参数类型&参数值
超时时间
requestID,标识唯一请求id
同理服务端返回的消息结构一般包括以下内容
返回值
状态code
requestID
3.2.2 序列化
一旦确定了消息的数据结构后,下一步就是要考虑序列化与反序列化了。
什么是序列化?序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程。
什么是反序列化?将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
为什么需要序列化?转换为二进制串后才好进行网络传输嘛!为什么需要反序列化?将二进制转换为对象才好进行后续处理!
由于Java提供了良好的默认支持,实现基本的对象序列化是件比较简单的事。待序列化的Java类只需要实现Serializable接口即可。Serializable仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。(链接阅读:Java对象序列化与RMI)
现如今序列化的方案越来越多,每种序列化方案都有优点和缺点,它们在设计之初有自己独特的应用场景,那到底选择哪种呢?从RPC的角度上看,主要看三点:
通用性,比如是否能支持Map等复杂的数据结构;
性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;
可扩展性,对互联网公司而言,业务变化快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,删除老的字段,而不影响老的服务,这将大大提供系统的健壮性。
目前国内各大互联网公司广泛使用hessian、protobuf、thrift、avro等成熟的序列化解决方案来搭建RPC框架,这些都是久经考验的解决方案。甚至json的方式也未尝不是一种方式。
3.3 网络通信传输
协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。RPC 的应用场景实质是一种可靠的请求应答消息流,和 HTTP 类似。当然如果你愿意,没有否决你用HTTP的方式。
对于性能的考虑上,需要了解目前有两种IO通信模型:1)BIO;2)NIO。(链接阅读:一个故事讲清楚 NIO)。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
现在很多RPC框架都直接基于netty,比如阿里巴巴的HSF、dubbo等。
3.4 服务发现
一个RPC过程中,简单的话,可能不需要服务发现的动作,配置好服务的IP以及端口就可以了。但是在分布式的应用中,就是一个很重要也很必要的过程。
比如说,当部分服务迁移到另一台机器B上,这时候就需要告诉调用者,部分服务要走哪个地址。如果你是手动的被动去修改,那么当又新增一台机器C挂载一部分服务。这时候调用者又需要手动修改;更复杂的情况下,A和B机器上都同时部署相同的服务做分压,那么我们肯定会想去实现负载均衡,那问题来了,手动的方式需要轮询机器服务是否存活,否则哪天机器A挂了,你会发现服务时好时坏。
分布式的一个很重要的点就是能实现服务发现,即调用者不用事先知道服务在哪,不再需要写死服务提供方地址。机器的增添、剔除对调用方透明。其中,zookeeper是一个很好的选择,它被广泛用于实现服务自动注册与发现功能,相对也比较成熟。
4 finally
思考:回到前言所讲的前端ajax的调用,浏览器端如何调用后端所写的服务(某个java类)?
提示:这区别于前面所提的client stub的存根调用,更像动态调用服务的方式。
client调用:JS调用方式,可以提供统一的封装接口
服务消息的格式数据确定:服务名,调用方法名,入参......
数据序列化、反序列化:json
网络通信:ajax、http
服务执行:java反射
编码:
out.close();
来源:oschina
链接:https://my.oschina.net/u/167767/blog/537629