Hadoop-RPC机制

一. RPC 概念
RPC(Remote Procedure Call) 即远程调用过程。
它允许一个计算机的程序远程调用另外一个计算机的子程序,而不用去关心底层的网络细节,对于我们使用者来说是透明的,所以他常用于分布式中。
RPC采用的是Client/Server(客户端/服务器)模式 ,Hadoop引入了RPC框架,客户端和NameNode,DateNode和NameNode,DataNode与DataNode之间,还有Job与task之间通讯都是基于RPC,所以RPC是hadoop框架的基础。
二. 特点
(1)透明 ,远程调用其他机器上的服务,不同应用之间的调用就向调用本地方法一样。
(2)高性能: 服务端能够处理多个来自客户端的请求
(3)可控性: 虽然JDK提供了一套RPC框架,但是太重又不可控,Hadoop实现了自己的RPC框架。
三。 Hadoop-RPC的简单使用

  1. 首先需用定义一个协议,他描述了服务对外提供了哪些接口或者功能。 该协议实际上是一个接口,并且继承自
public interface MyInterface extends VersionedProtocol {
    long versionID = 1L;
    int add(int number1, int number2);
}

其中versionID表示了版本号,必须和服务器端的版本保存一致,否则调用失败,而且该字段必须要有,否则客户端会出错

  1. server端 需要实现协议接口,并返回版本号:
public class MyInterfaceImpl  implements MyInterface {
    //实现加法
    @Override
    public int add(int number1, int number2){
        System.out.println("number1 = " + number1 + " number2 = " + number2);
        return number1 + number2;
    }
    //返回版本号
    @Override
    public long getProtocolVersion(String protocol, long clientVersion) throws IOException {
        return MyInterface.versionID;
    }

    @Override
    public ProtocolSignature getProtocolSignature(String protocol, long clientVersion, int clientMethodsHash) throws IOException {
        return new ProtocolSignature(getProtocolVersion(protocol, clientVersion), null);
    }
}
  1. 构建Server,绑定协议的实现类,并启动server
public static void main(String[] args) {
    RPC.Builder builder = new RPC.Builder(new Configuration());
    //服务器Ip 地址
    builder.setBindAddress("127.0.0.1");
    //端口号
    builder.setPort(12345);
    
    builder.setProtocol(MyInterface.class);
    builder.setInstance(new MyInterfaceImpl());

    try {
        RPC.Server server = builder.build();
        server.start();
    } catch (IOException e) {
        e.printStackTrace();
    }

}
  1. 构建客户端,并访问add服务
public static void main(String[] args) {
    try {
        MyInterface proxy = RPC.getProxy(MyInterface.class,1L, new InetSocketAddress("127.0.0.1", 12345), new Configuration());
        int res = proxy.add(1, 2);
        System.out.println(res);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

各自运行即可,非常简单

四. RPC调用流程和原理
在这里插入图片描述

  1. Client和Server端的通过Socket连接进行通讯。
  2. 客户端会得一个代理对象RPC.getProxy,代理对像拦截调用的方法,拿到方法名称,参数序列化之后通过Socket发给server,Server反序列化得到相应的参数调用具体的实现对象。所以如果不使用基本类型,自定义对象需要实现Writeable

动态代理:

动态代理理论是一种设计模式,指的的是不直接访问对象,而是访问这个对象的经纪人,由这个经纪人来调用对象或者干脆直接就不访问对像,由这个经纪人完全决定怎么办,这就是代理模式。
动态代理模式很常用,各个地方都能看到他的身影,用的了动态代理的地方那么基本上跑不了反射机制。

Java中的动态带来很简单:

 MyInterfaceImpl im = new MyInterfaceImpl();
        MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(MyRPCClient.class.getClassLoader(),
        im.getClass().getInterfaces(), 
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName());
                if(args != null && args.length > 0){
                    for (int i = 0; i <args.length; i++){
                        System.out.println(args[i]);
                    }
                }
//                return  method.invoke(method, args);
                return 10;
            }
        });

        int add = myInterface.add(1, 2);
        System.out.println(add);

主要是利用Proxy.newProxyInstance 实现一个代理对象,其中必须要实现InvocationHandler接口,这个接口中的invoke会拦截到你所调用的任何方法,在本例中,其中method指的是add方法,args[] 指的是1, 和2两个参数,如果不直接返回10的话,调用 method.invoke(method, args) 那么就会直接调用到真正的实现方法里面去。 所以 这个代理类中拿到了方法,参数那你就想干啥就干啥了。

RPC 的实现机制中最主要的就是代理,在RPC的源代码中一层一层的找下去就会先发现:

public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
                       InetSocketAddress addr, UserGroupInformation ticket,
                       Configuration conf, SocketFactory factory,
                       int rpcTimeout, RetryPolicy connectionRetryPolicy,
                       AtomicBoolean fallbackToSimpleAuth)
  throws IOException {    

  if (connectionRetryPolicy != null) {
    throw new UnsupportedOperationException(
        "Not supported: connectionRetryPolicy=" + connectionRetryPolicy);
  }

//代理机制
  T proxy = (T) Proxy.newProxyInstance(protocol.getClassLoader(),
      new Class[] { protocol }, new Invoker(protocol, addr, ticket, conf,
          factory, rpcTimeout, fallbackToSimpleAuth));
          
  return new ProtocolProxy<T>(protocol, proxy, true);
}

其中 Invoker 实现了InvocationHandler 这个接口

private static class Invoker implements RpcInvocationHandler {
  private Client.ConnectionId remoteId;
  private Client client;
  private boolean isClosed = false;
  private final AtomicBoolean fallbackToSimpleAuth;

  public Invoker(Class<?> protocol,
                 InetSocketAddress address, UserGroupInformation ticket,
                 Configuration conf, SocketFactory factory,
                 int rpcTimeout, AtomicBoolean fallbackToSimpleAuth)
      throws IOException {
    this.remoteId = Client.ConnectionId.getConnectionId(address, protocol,
        ticket, rpcTimeout, conf);
    this.client = CLIENTS.getClient(conf, factory);
    this.fallbackToSimpleAuth = fallbackToSimpleAuth;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    long startTime = 0;
    if (LOG.isDebugEnabled()) {
      startTime = Time.now();
    }
    TraceScope traceScope = null;
    if (Trace.isTracing()) {
      traceScope = Trace.startSpan(RpcClientUtil.methodToTraceString(method));
    }
    ObjectWritable value;
    try {
      value = (ObjectWritable)
        client.call(RPC.RpcKind.RPC_WRITABLE, new Invocation(method, args),
          remoteId, fallbackToSimpleAuth);
    } finally {
      if (traceScope != null) traceScope.close();
    }
    if (LOG.isDebugEnabled()) {
      long callTime = Time.now() - startTime;
      LOG.debug("Call: " + method.getName() + " " + callTime);
    }
    return value.get();
  }

invoke这方法中,就会将受到的method和参数通过socket发送给服务端,有兴趣的可以看看new Invocation(method, args) 这个实现类,里面实现了字节传送的细节。

结语:

在阅读源码的过程中,发现了一个很有意思的细节, 客户端RPC.getProxy(MyInterface.class,1L, new InetSocketAddress("127.0.0.1", 12345), new Configuration()); 第二个参数版本号,1L 这个其实没啥卵用,就算你随便写一个数字也可以调用成功,那是因为在源码中使用的VersionID,实际上是用的接口中定义的versionID,是通过反射拿到的,白白让我跟踪了半天最后发现没用,也是醉了:

static public long getProtocolVersion(Class<?> protocol) {
  if (protocol == null) {
    throw new IllegalArgumentException("Null protocol");
  }
  long version;
  ProtocolInfo anno = protocol.getAnnotation(ProtocolInfo.class);
  if (anno != null) {
    version = anno.protocolVersion();
    if (version != -1)
      return version;
  }
  try {
    Field versionField = protocol.getField("versionID");
    versionField.setAccessible(true);
    return versionField.getLong(protocol);
  } catch (NoSuchFieldException ex) {
    throw new RuntimeException(ex);
  } catch (IllegalAccessException ex) {
    throw new RuntimeException(ex);
  }
}

这也就解释了 为什么接口中 没有定义versionID 会报错的原因。 当人根据源码,如果不想写versionID这个字段,添加ProtocolInfo 这个注解也是可以的。

欢迎关注我的公众号: 北风中独行的蜗牛
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟红尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值