当前位置: 首页 > 科技观察

如何手写一个RPC框架

时间:2023-03-14 14:21:42 科技观察

简介在开发单个项目的时候,想必大家都写过类似的代码。即服务提供者和服务调用者在一个服务中HelloServicehelloService=newHelloServiceImpl();Stringmsg=helloService.sayHello("world");//helloworldSystem.out.println(msg);}}但是由于单一服务的诸多弊端,现在很多公司已经将不相关的功能拆分到不同的服务中服务。如何像调用本地服务一样调用远程服务?这时候就不得不提到RPC框架(RemoteProcedureCall,远程过程调用)。他帮我们屏蔽了网络通信、序列化等操作的实现,真正让调用远程服务像调用本地服务一样方便。知名的RPC框架有SpringCloud、阿里巴巴的Dubbo、Facebook的Thrift、Google的grpc等。一个RPC调用过程的RPC调用过程如下:调用者发送请求后,代理类调用方法和参数进入网络传输调用者的消息体将消息体发送给提供者提供者对消息进行解码,反射获取调用者提供者的参数执行相应的方法,并返回结果。我们来分析下rpc框架是如何实现的?有哪些地方?可以延长。为了让大家有一个更形象的认识,我写了一个github项目,由简单到难实现了一个rpc框架。欢迎starhttps://github.com/erlieStar/simple-rpc生成代理类。前面我们说过,调用方执行完方法后,实际上执行的是代理类的方法,代理类帮我们进行序列化和编解码操作。那么如何生成代理类呢?让我们来看看主流的做法。Facebook的Thrift和Google的grpc都定义了一个schema文件,然后执行程序帮你生成客户端代理类和接口。调用者直接使用生成的代理类进行请求,提供者可以继承生成的接口。这种方式最大的优点是可以进行多种语言的交流,即一个schema文件可以生成Java程序或者Python程序。调用者是Java程序,提供者是Python程序,两者可以正常通信。而且是二进制协议,通信效率比较高。Java中生成代理类的方式有如下几种:JDK动态代理(实现了InvocationHandler接口)字节码操作库(如cglib、Javassist)在Dubbo中提供了两种生成代理类的方式,jdk动态代理和Javassist,默认是javassist,至于原因?当然javassist效率更高。为什么协议需要协议?SpringCloud通过Http协议进行通信,那么Dubbo使用什么协议进行通信呢?为什么需要协议?这东西?因为数据在网络中是以二进制形式传输的,所以RPC请求数据并不是作为一个整体发送给提供者的,而是可能被拆分成多个数据包发送出去。提供商如何识别它?数据呢?比如一个文本ABCDEF,provider依次收到的数据可能是ABCDEF,也可能是ABCDEF。提供商应该如何处理这些数据?很简单,定个规矩。这个规则有很多种。以下是固定长度协议的三个示例。协议内容的长度是固定的。如果读取了50个字节,则解码操作开始。可以参考Netty的FixedLengthFrameDecoder专用终结符来定义消息结束的分隔符。如果读到\n,说明已经读到一条数据。如果您不阅读它,请继续阅读。可以参考Netty的DelimiterBasedFrameDecoder变长协议(协议头+协议体),用固定长度来表示消息体的长度,剩下的消息内容就是消息体。如果需要,协议标头还将包含一些常用属性。Http协议的Header就是协议头,比如content-type,content-length等。可以参考Netty的DelimiterBasedFrameDecoderDubbo,通过自定义协议进行通信。协议头的格式如下。每一位的含义如下。为什么Dubbo需要自定义协议,而不是现有的Http协议?主要原因是自定义协议可以提高Http的性能协议的请求包比较大,包含很多无用的内容。自定义协议可以简化很多内容。Http协议是无状态的,每次都要重新建立连接。响应完成后,连接将被关闭。序列化后的协议头内容用位表示,协议体将在应用程序中显示。封装成一个对象,比如Dubbo把request封装成Request,把response封装成Response。前面我们说过,通过网络传输的数据一定是二进制数据,但是调用者的输入参数和提供者的返回值都是对象,所以需要进行序列化和反序列化过程序列化方法如下JDK原生序列化JSONProtobufKryoHessian2MessagePack我们在选择序列化方式的时候主要考虑以下因素:效率、空间开销、通用性、兼容性、安全性、通用通信IO模型有以下四种同步阻塞IO(BlockingIO)同步非阻塞IO(Non-blockingIO)IO多路复用(IOMultiplexing)异步IO(AsynchronousIO)因为RPC一般用在高并发场景,所以我们选择IO多路复用的模型。Netty的IO多路复用是基于Reactor开发模型实现的。在后续的文章中,我会分析这种开发模式是如何支持高并发注册中心的。注册中心和电话簿的作用是类似的。保存服务名称与具体服务地址的映射关系。当我们要与某个服务进行通信时,只需要根据服务名找到该服务的地址即可。更重要的是,这个电话簿是动态的。当服务的地址发生变化时,电话簿中的地址也会发生变化。当服务不可用时,电话簿中的地址将消失。这个动态电话本就是注册中心。注册中心的实现方式有很多种。Zookeeper、Redis、Nocas等都可以实现。下面介绍一下用Zookeeper实现注册中心的方法。Zookeeper有两种类型的节点,持久节点和临时节点。我们在zookeeper上注册服务时,会使用一个临时节点,这样当服务断开时,可以删除节点,即使客户端和创建该节点的服务器之间的会话关闭了,该节点仍然不会被删除。持久时序节点在持久节点的基础上增加了节点的有序特征。临时节点创建节点为临时节点,数据不会一直保存在zookeeper服务器上。当临时节点被创建客户端会话关闭时,该节点在对应的zookeeper服务器上被删除。临时时序节点在临时节点的基础上增加了有序节点的特性。注册中心全部挂掉怎么通信?当一个zookeeper挂掉后,会自动切换到另一个zookeeper。都挂了也没关系,因为dubbo在本地保存了一份映射关系。这种映射关系可以保存在Map中,也可以保存在文件中。当有新服务注册到注册中心时,本地缓存会不会更新?已注册如果您监控,当然会更新。当被监控的节点或子节点发生变化时,相应的内容会被推送到监听客户端,你可以通过如下方式更新Zookeeper中本地缓存的事件。你可以把这个监控理解为一个分布式观察者模型总结当然,一个成熟的RPC框架要考虑很多东西,比如路由策略,异常重试,监控,异步调用等等,这些都是和主流程无关的,所以我就不多介绍了。本文转载自微信公众号》《Java知堂》,可通过以下二维码关注。转载请联系Java视堂公众号。