• Netty RPC 实现


    1 概念

    RPC,即 Remote Procedure Call(远程过程调用),调用远程计算机上的服务,就像调用本地服务一样。RPC 可以很好的解耦系统,如 WebService 就是一种基于 Http 协议的 RPC。这个 RPC 整体框架如下:

    2 关键技术 

     1. 服务发布与订阅:服务端使用 Zookeeper 注册服务地址,客户端从 Zookeeper 获取可用的服务
    地址。
    2. 通信:使用 Netty 作为通信框架。
    3. Spring:使用 Spring 配置服务,加载 Bean,扫描注解。
    4. 动态代理:客户端使用代理模式透明化服务调用。
    5. 消息编解码:使用 Protostuff 序列化和反序列化消息。

    3 核心流程

    1. 服务消费方(client)调用以本地调用方式调用服务;

    2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
    3. client stub 找到服务地址,并将消息发送到服务端;
    4. server stub 收到消息后进行解码;
    5. server stub 根据解码结果调用本地的服务;
    6. 本地服务执行并将结果返回给 server stub;
    7. server stub 将返回结果打包成消息并发送至消费方;
    8. client stub 接收到消息,并进行解码;
    9. 服务消费方得到最终结果。

            RPC 的目标就是要 2~8 这些步骤都封装起来,让用户对这些细节透明。JAVA 一般使用动态代理方式实现远程调用。

    3.1 消息编解码

     息数据结构 (接口名称 + 方法名 + 参数类型和参数值 + 超时时间 + requestID )
    客户端的请求消息结构一般需要包括以下内容:
    1. 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪
    个接口了;
    2. 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
    3. 参数类型和参数值:参数类型有很多,比如有 bool、int、long、double、string、map、list,
    甚至如 struct(class);以及相应的参数值;
    4. 超时时间:
    5. requestID,标识唯一请求 id,在下面一节会详细描述 requestID 的用处。
    6. 服务端返回的消息 : 一般包括以下内容。返回值+状态 code+requestID

    序列化
    目前互联网公司广泛使用 Protobuf、Thrift、Avro 等成熟的序列化解决方案来搭建 RPC 框架,这
    些都是久经考验的解决方案。

    3.2 通讯过程

    核心问题 ( 线程暂停、 消息乱序 )
            如果使用 netty 的话,一般会用 channel.writeAndFlush()方法来发送消息二进制串,这个方
    法调用后对于整个远程调用(从发出请求到接收到结果)来说是一个异步的,即对于当前线程来说,
    将请求发送出来后,线程就可以往后执行了,至于服务端的结果,是服务端处理完成后,再以消息
    的形式发送给客户端的。于是这里出现以下两个问题:
    1. 怎么让当前线程“暂停”,等结果回来后,再向后执行?
    2. 如果有多个线程同时进行远程方法调用,这时建立在 client server 之间的 socket 连接上
    会有很多双方发送的消息传递,前后顺序也可能是随机的,server 处理完结果后,将结
    果消息发送给 client,client 收到很多消息,怎么知道哪个消息结果是原先哪个线程调用
    的?如下图所示,线程 A 和线程 B 同时向 client socket 发送请求 requestA 和 requestB,
    socket 先后将 requestB 和 requestA 发送至 server,而 server 可能将 responseB 先返
    回,尽管 requestB 请求到达时间更晚。我们需要一种机制保证 responseA 丢给
    ThreadA,responseB 丢给 ThreadB。

     通讯流程
    requestID 生成 -AtomicLong
    1. client 线程每次通过 socket 调用一次远程接口前,生成一个唯一的 ID,即 requestID
    (requestID 必需保证在一个 Socket 连接里面是唯一的),一般常常使用 AtomicLong
    从 0 开始累计数字生成唯一 ID;
    存放回调对象 callback 到全局 ConcurrentHashMap
    2. 将 处 理 结 果 的 回 调 对 象 callback , 存 放 到 全 局 ConcurrentHashMap 里 面
    put(requestID, callback);
    synchronized 获取回调对象 callback 的锁 并 自旋 wait
    3. 当线程调用 channel.writeAndFlush()发送消息后,紧接着执行 callback 的 get()方法试
    图获取远程返回的结果。在 get()内部,则使用 synchronized 获取回调对象 callback 的
    锁,再先检测是否已经获取到结果,如果没有,然后调用 callback 的 wait()方法,释放
    callback 上的锁,让当前线程处于等待状态。

    监听消息的线程收到消息,找到 callback 上的锁并唤醒
    4. 服务端接收到请求并处理后,将response结果(此结果中包含了前面的requestID)发
    送给客户端,客户端 socket 连接上专门监听消息的线程收到消息,分析结果,取到
    requestID,再从前面的 ConcurrentHashMap 里面 get(requestID),从而找到
    callback 对象,再用 synchronized 获取 callback 上的锁,将方法调用结果设置到
    callback 对象里,再调用 callback.notifyAll()唤醒前面处于等待状态的线程。

    1. public Object get() {
    2. synchronized (this) { // 旋锁
    3. while (true) { // 是否有结果了
    4. If (!isDone){
    5. wait(); //没结果释放锁,让当前线程处于等待状态
    6. }else{//获取数据并处理
    7. }
    8. }
    9. }
    10. }
    11. private void setDone(Response res) {
    12. this.res = res;
    13. isDone = true;
    14. synchronized (this) { //获取锁,因为前面 wait()已经释放了 callback 的锁了
    15. notifyAll(); // 唤醒处于等待的线程
    16. }
    17. }

    4 RMI 实现方式

            Java 远程方法调用,即 Java RMI(Java Remote Method Invocation)是 Java 编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使 Java 编程人员能够在网络环境中分布操作。RMI 全部的宗旨就是尽可能简化远程接口对象的使用。

    4.1 实现步骤

    1. 编写远程服务接口,该接口必须继承 java.rmi.Remote 接口,方法必须抛出
    java.rmi.RemoteException 异常;
    2. 编写远程接口实现类,该实现类必须继承 java.rmi.server.UnicastRemoteObject 类;
    3. 运行 RMI 编译器(rmic),创建客户端 stub 类和服务端 skeleton 类;
    4. 启动一个 RMI 注册表,以便驻留这些服务;

    5. 在 RMI 注册表中注册服务;
    6. 客户端查找远程对象,并调用远程方法;

    1. 1:创建远程接口,继承 java.rmi.Remote 接口
    2. public interface GreetService extends java.rmi.Remote {
    3. String sayHello(String name) throws RemoteException;
    4. }
    5. 2:实现远程接口,继承 java.rmi.server.UnicastRemoteObject 类
    6. public class GreetServiceImpl extends java.rmi.server.UnicastRemoteObject
    7. implements GreetService {
    8. private static final long serialVersionUID = 3434060152387200042L;
    9. public GreetServiceImpl() throws RemoteException {
    10. super();
    11. }
    12. @Override
    13. public String sayHello(String name) throws RemoteException {
    14. return "Hello " + name;
    15. }
    16. }
    17. 3:生成 Stub 和 Skeleton;
    18. 4:执行 rmiregistry 命令注册服务
    19. 5:启动服务
    20. LocateRegistry.createRegistry(1098);
    21. Naming.bind("rmi://10.108.1.138:1098/GreetService", new GreetServiceImpl());
    22. 6.客户端调用
    23. GreetService greetService = (GreetService)
    24. Naming.lookup("rmi://10.108.1.138:1098/GreetService");
    25. System.out.println(greetService.sayHello("Jobs"));

  • 相关阅读:
    C++ 重载运算符,语法+示例,非常详细!!!
    PerformanceOne一站式性能测试平台
    Mac 安装 Homebrew
    新版TCGA的突变SNP数据添加临床信息
    微服务框架 SpringCloud微服务架构 16 SpringAMQP 16.1 基本介绍
    Mybatis——Mybatis表之间的关联关系和事务、缓存机制以及ORM
    基于SSM的网上租车系统的设计与实现(论文+源码)_kaic
    CANoe-设置CAN信号时提示没有信号驱动可用的问题
    springboot+jacob实现文字转语音
    C# 深入理解事件(event)机制
  • 原文地址:https://blog.csdn.net/Lj_chuxuezhe/article/details/133756663