总的来说一个rpc实现和kafka的api实现类似,为每个调用包含一个唯一id
,然后序列化调用参数为byte[]
,把id
和byte[]
组成通用request
,服务端接收request
后解析id
和参数byte[]
,根据id查找方法,然后调用方法,最后序列化返回值后转为response
写回客户端。
在这个基础上做一些改动就可以实现简单的rpc框架。
- 首先要用到
Proxy.newProxyInstance(...)
为接口生成代理,在InvocationHandler
中可以拿到调用的method
和args
。 - 为
method
生成唯一id供服务端匹配,使用:interfaceName + methodName + parameterTypes
可以确保id唯一。 - 序列化
method
的id
和args
组成request
发送请求。 - 服务端解析
id
查找method
,使用args
调用方法,产生返回值,以response
的形式写回。 - 客户端读取
response
并解析返回值,返回给方法调用即可。
这里在请求和响应部分做了改动。
使用已有的HttpClient
库可以轻易实现http
协议的requst
和response
,但是支持其他类似Dubbo
的协议并不容易。
使用netty
可以轻松实现新的协议,可以采用类似HttpClient
的实现,每次请求创建或者从连接池中取出一个channel
,然后执行写入request
,一直等到channel
读取到一个完整的响应关闭或者释放channel
,这样就产生了一个问题,慢连接或者慢方法调用会导致channel
频繁创建。
所以这里采用多个调用复用单个channel
的实现,因为channel.write
总是在eventloop
中线性的执行,多个request
对象被一个挨一个的写出,然后服务端处理request
再把response
一个一个的写回即可。在服务端串行调用方法的效率不高,如果改为线程池并发调用则response
的写回顺序可能和request
的写出顺序不同,为了匹配request
和response
,这里对request
进行唯一seqNumber
标识,response
写回时携带seqNumber
即可。
具体实现如下:
- 每个
request
对象都包含一个负责保存响应值的CompletableFuture
对象,每次写出request
,以seqNumber
为key保存request
到Map<Long,Request> map
中。 client.write(request)
调用channel.write(request)
,然后返回request
包含的CompletableFuture
对象。channel.read(...)
读取到response
对象时,根据response
的seqNumber
取出匹配的request
对象,然后填充CompletableFuture
即可。
这部分的实现主要是依赖Eureka
等已有的实现。
整体的实现类似FilterChain
,在调用链的末端是实际调用,在调用过程中,可以修改向下传递的参数来控制最后的调用,也可以直接返回结果中断后续的调用。前者通过修改向下传递的server
来实现负载均衡,后者则可以在出现熔断时中止调用直接返回错误。