V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
codehole
V2EX  ›  程序员

大厨小鲜——基于 Netty 自己动手实现 RPC 框架

  •  
  •   codehole ·
    pyloque · 2018-04-16 11:10:28 +08:00 · 2365 次点击
    这是一个创建于 2424 天前的主题,其中的信息可能已经有所发展或是发生改变。

    今天我们要来做一道小菜,这道菜就是 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 服务器

    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 客户端

    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
    ...
    
    

    牢骚

    本以为是小菜一碟,但是编写完整的代码和文章却将近花费了一天的时间,深感写码要比做菜耗时太多了。因为只是为了教学目的,所以在实现细节上还有好多没有仔细去雕琢的地方。如果是要做一个开源项目,力求非常完美的话。至少还要考虑一下几点。

    1. 客户端连接池
    2. 多服务进程负载均衡
    3. 日志输出
    4. 参数校验,异常处理
    5. 客户端流量攻击
    6. 服务器压力极限

    如果要参考 grpc 的话,还得实现流式响应处理。如果还要为了节省网络流量的话,又需要在协议上下功夫。这一大堆的问题还是抛给读者自己思考去吧。

    关注公众号「码洞」,发送「 RPC 」即可获取以上完整菜谱的 GitHub 开源代码链接

    codehole
        1
    codehole  
    OP
       2018-04-16 11:12:04 +08:00
    怕文章太长,看起来难受,后面的菜谱详情略去了,愿意细读的去掘金看看吧

    [大厨小鲜——基于 Netty 自己动手实现 RPC 框架]( https://juejin.im/post/5ad2a99ff265da238d51264d)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5355 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 09:15 · PVG 17:15 · LAX 01:15 · JFK 04:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.