今天我们要来做一道小菜,这道菜就是 RPC 通讯框架。它使用 netty 作为原料,fastjson 序列化工具作为调料,来实现一个极简的多线程 RPC 服务框架。
我们暂且命名该 RPC 框架为 rpckids。
在告诉读者完整的制作菜谱之前,我们先来试试这个小菜怎么个吃法,好不好吃,是不是吃起来很方便。如果读者觉得很难吃,那后面的菜谱就没有多大意义了,何必花心思去学习制作一门谁也不爱吃的大烂菜呢?
例子中我会使用 rpckids 提供的远程 RPC 服务,用于计算斐波那契数和指数,客户端通过 rpckids 提供的 RPC 客户端向远程服务传送参数,并接受返回结果,然后呈现出来。你可以使用 rpckids 定制任意的业务 rpc 服务。
斐波那契数输入输出比较简单,一个 Integer,一个 Long。 指数输入有两个值,输出除了计算结果外还包含计算耗时,以纳秒计算。之所以包含耗时,只是为了呈现一个完整的自定义的输入和输出类。
// 指数 RPC 的输入
public class ExpRequest {
private int base;
private int exp;
// constructor & getter & setter
}
// 指数 RPC 的输出
public class ExpResponse {
private long value;
private long costInNanos;
// constructor & getter & setter
}
public class FibRequestHandler implements IMessageHandler<Integer> {
private List<Long> fibs = new ArrayList<>();
{
fibs.add(1L); // fib(0) = 1
fibs.add(1L); // fib(1) = 1
}
@Override
public void handle(ChannelHandlerContext ctx, String requestId, Integer n) {
for (int i = fibs.size(); i < n + 1; i++) {
long value = fibs.get(i - 2) + fibs.get(i - 1);
fibs.add(value);
}
// 输出响应
ctx.writeAndFlush(new MessageOutput(requestId, "fib_res", fibs.get(n)));
}
}
public class ExpRequestHandler implements IMessageHandler<ExpRequest> {
@Override
public void handle(ChannelHandlerContext ctx, String requestId, ExpRequest message) {
int base = message.getBase();
int exp = message.getExp();
long start = System.nanoTime();
long res = 1;
for (int i = 0; i < exp; i++) {
res *= base;
}
long cost = System.nanoTime() - start;
// 输出响应
ctx.writeAndFlush(new MessageOutput(requestId, "exp_res", new ExpResponse(res, cost)));
}
}
RPC 服务类要监听指定 IP 端口,设定 io 线程数和业务计算线程数,然后注册斐波那契服务输入类和指数服务输入类,还有相应的计算处理器。
public class DemoServer {
public static void main(String[] args) {
RPCServer server = new RPCServer("localhost", 8888, 2, 16);
server.service("fib", Integer.class, new FibRequestHandler())
.service("exp", ExpRequest.class, new ExpRequestHandler());
server.start();
}
}
RPC 客户端要链接远程 IP 端口,并注册服务输出类(RPC 响应类),然后分别调用 20 次斐波那契服务和指数服务,输出结果
public class DemoClient {
private RPCClient client;
public DemoClient(RPCClient client) {
this.client = client;
// 注册服务返回类型
this.client.rpc("fib_res", Long.class).rpc("exp_res", ExpResponse.class);
}
public long fib(int n) {
return (Long) client.send("fib", n);
}
public ExpResponse exp(int base, int exp) {
return (ExpResponse) client.send("exp", new ExpRequest(base, exp));
}
public static void main(String[] args) {
RPCClient client = new RPCClient("localhost", 8888);
DemoClient demo = new DemoClient(client);
for (int i = 0; i < 20; i++) {
System.out.printf("fib(%d) = %d\n", i, demo.fib(i));
}
for (int i = 0; i < 20; i++) {
ExpResponse res = demo.exp(2, i);
System.out.printf("exp2(%d) = %d cost=%dns\n", i, res.getValue(), res.getCostInNanos());
}
}
}
先运行服务器,服务器输出如下,从日志中可以看到客户端链接过来了,然后发送了一系列消息,最后关闭链接走了。
server started @ localhost:8888
connection comes
read a message
read a message
...
connection leaves
再运行客户端,可以看到一些列的计算结果都成功完成了输出。
fib(0) = 1
fib(1) = 1
fib(2) = 2
fib(3) = 3
fib(4) = 5
...
exp2(0) = 1 cost=559ns
exp2(1) = 2 cost=495ns
exp2(2) = 4 cost=524ns
exp2(3) = 8 cost=640ns
exp2(4) = 16 cost=711ns
...
本以为是小菜一碟,但是编写完整的代码和文章却将近花费了一天的时间,深感写码要比做菜耗时太多了。因为只是为了教学目的,所以在实现细节上还有好多没有仔细去雕琢的地方。如果是要做一个开源项目,力求非常完美的话。至少还要考虑一下几点。
如果要参考 grpc 的话,还得实现流式响应处理。如果还要为了节省网络流量的话,又需要在协议上下功夫。这一大堆的问题还是抛给读者自己思考去吧。
关注公众号「码洞」,发送「 RPC 」即可获取以上完整菜谱的 GitHub 开源代码链接
1
codehole OP 怕文章太长,看起来难受,后面的菜谱详情略去了,愿意细读的去掘金看看吧
[大厨小鲜——基于 Netty 自己动手实现 RPC 框架]( https://juejin.im/post/5ad2a99ff265da238d51264d) |