一. 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的简单使用
- 首先需用定义一个协议,他描述了服务对外提供了哪些接口或者功能。 该协议实际上是一个接口,并且继承自
public interface MyInterface extends VersionedProtocol {
long versionID = 1L;
int add(int number1, int number2);
}
其中versionID表示了版本号,必须和服务器端的版本保存一致,否则调用失败,而且该字段必须要有,否则客户端会出错
- 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);
}
}
- 构建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();
}
}
- 构建客户端,并访问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调用流程和原理
- Client和Server端的通过Socket连接进行通讯。
- 客户端会得一个代理对象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 这个注解也是可以的。
欢迎关注我的公众号: 北风中独行的蜗牛