HDFS
作为分布式存储系统,各个节点之间的通信必不可少,
HadoppRPC
是HDFS节点间的通信的基础框架。本文首先将介绍
RPC
基本概念及简单的原生实现,随后将基于Hadoop源码分析HadoopRPC的实现细节。
RPC
(Remote Procedure Call
)即远程过程调用,是一种通过网络从远程计算机程序上请求服务的协议。RPC允许本地程序像调用本地方法一样调用远程计算机上的应用程序,其使用常见的网络传输协议(如TCP
或UDP
)传递RPC请求以及相应信息,使得分布式程序的开发更加容易。
RPC
采用客户端/服务器模式,请求程序就是客户端,服务提供程序就是服务器。RPC框架工作原理如图1-1
所示,工作流程依次见图中标号①
~⑩
,其结构主要包含以下部分:
①
),然后接受stub程序的响应信息(如图中⑩
)。②
);客户端stub程序也会等待服务器的响应信息,将响应信息反序列化并返回给请求程序(如图中⑨⑩
)。RPC
请求和响应的网络通信模块,可以基于TCP
或UDP
协议(如图中的③⑧
)。④
)并反序列化,根据调用信息触发对应的服务程序(如图⑤
),然后将服务程序的响应信息序列化并发回给客户端(如图中⑥⑦
)。⑤
),执行对应的逻辑并返回执行结果(如图中⑥
)。要实现RPC,基本上要解决三大问题:
下文将使用JDK动态代理、JDK自带的序列化及反序列化以及Java原生Socket通信实现基本demo,初见java全原生代码实现的雏形RPC风采。
我们将maven项目分为3个模块:API、Client、Server。API用于放公共类,Client和Server分别做客户端及服务端。
目标
通过客户端调用服务端的sayHello、getPerson方法。
1.API代码
该模块存放三部分内容:①方法接口的声明;②Java Bean(用于封装网络通信的Entity及测试实例Bean);③序列化及反序列化。主要部分代码如下:
方法的接口声明:
package org.Simple.API;
/*
公共接口类
*/
public interface HelloService {
String sayHello(String name);
Person getPerson(String name);
}
JavaBean如下:
package org.Simple.API;
/*
公共网络通信类,通过序列化该类,将客户端调用接口、方法、参数、参数类型封装,
然后服务端反序列化,再通过反射,调取相应实现类的方法。
*/
public class NetModel implements Serializable{
private static final long serialVersionUID = 1L;
private String className;
private String method;
private Object[] args ;
private String[] types;
// getter and setter ...
}
package org.Simple.API;
/*
普通公共bean
*/
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
// getter and setter...
}
通过JDK序列化和反序列化部分代码如下:
package org.Simple.API;
/**
* JDK自带序列化
*/
public class SerializeUtils {
/**
* 序列化
*/
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(object);
outputStream.flush();
byte[] byteArray = os.toByteArray();
outputStream.close();
os.close();
return byteArray;
}
/**
* 反序列化
*/
public static Object deSerialize(byte[] buf) throws IOException, ClassNotFoundException {
ByteArrayInputStream is = new ByteArrayInputStream(buf);
ObjectInputStream inputStream = new ObjectInputStream(is);
Object object = inputStream.readObject();
inputStream.close();
is.close();
return object;
}
}
2.RPCClient代码
Client端给服务端发送数据,并反序列化服务端返回的数据。作为RPC的核心部分,代码如下:
package org.Simple.C;
/*
RPC客户端,也是用Socket与服务端进行通信
*/
public class RPCClient {
public static Object send(byte[] bs) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bs);
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
in.read(buf);
Object formatDate = SerializeUtils.deSerialize(buf);
socket.close();
return formatDate;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
HelloService helloService = ProxyFactory.getInstance(HelloService.class);
System.out.println("say:"+helloService.sayHello("whb"));
System.out.println("Person:"+helloService.getPerson("whb"));
}
}
其中,客户端执行的时候调用动态代理方法,代码如下:
package org.Simple.C;
public class ProxyFactory {
private static InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
NetModel netModel = new NetModel();
Class<?>[] classes = proxy.getClass().getInterfaces();
String className = classes[0].getName();
netModel.setClassName(className);
netModel.setArgs(args);
netModel.setMethod(method.getName());
String [] types = null;
if(args!=null) {
types = new String [args.length];
for (int i = 0; i < types.length; i++) {
types[i] = args[i].getClass().getName();
}
}
netModel.setTypes(types);
byte[] byteArray = SerializeUtils.serialize(netModel);
Object send = RPCClient.send(byteArray);
return send;
}
};
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(),
new Class[]{clazz}, handler );
}
}
主要是利用Proxy.newProxyInstance 实现一个代理对象,其中必须要实现InvocationHandler接口,这个接口中的invoke会拦截到你所调用的任何方法。在invoke方法中得到代理接口的全限定名、调用的方法、参数和参数类型,然后将这些参数封装在NetModel中,再将其序列化成byte数组,通过Client端的send方法将信息发送至Server端。
Server端对于收到的byte数组,经过一系列处理,通过method.invoke
执行(具体见RPCServer部分),再将结果返回给Client。
至此,整个过程结束。
3.PRCServer代码
Server端主要实现对收到的数据进行网络传输结构化封装处理并返回给客户端。
核心代码中,处理接收到的byte数组数据,通过反序列化得到传过来的通信类NetModel,然后得到接口名、方法名、参数、参数类型,最后通过JDK反射来调取实现类的方法,并将调取结果序列化为byte数组,然后返回。
package org.Simple.S;
public class RPCServer {
public static void main(String[] args)
{
try {
openServer();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void openServer() throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
try {
System.out.println("服务开启");
while(true) {
Socket socket = serverSocket.accept();
System.out.println(socket.getInetAddress()+"-connected");
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
in.read(buf);
byte[] formatDate = formatData(buf);
OutputStream out = socket.getOutputStream();
out.write(formatDate);
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
serverSocket.close();
}
}
public static byte[] formatData(byte[] bs){
try {
//将收到的byte数组反序列化为NetModel类型,然后通过反射调用HelloServiceImpl实现类的方法
NetModel netModel = (NetModel)SerializeUtils.deSerialize(bs);
String className = netModel.getClassName();
String[] types = netModel.getTypes();
Object[] args = netModel.getArgs();
//获取实现方法中的详细信息
Class<?> clazz = Class.forName(getPropertyValue("org.Simple.S.HelloServiceImpl"));
Class<?> [] typeClazzs = null;
if(types!=null) {
typeClazzs = new Class[types.length];
for (int i = 0; i < typeClazzs.length; i++) {
typeClazzs[i] = Class.forName(types[i]);
}
}
Method method = clazz.getMethod(netModel.getMethod(),typeClazzs);
Object object = method.invoke(clazz.newInstance(), args);
byte[] byteArray = SerializeUtils.serialize(object);
return byteArray;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Server对接口实现如下:
package org.Simple.S;
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "hello,"+name;
}
public Person getPerson(String name) {
Person person = new Person();
person.setName(name);
person.setAge(18);
return person;
}
}
我们分别启动RPCServer、RPCClient,可以看到在Client端能正确得到方法返回结果。
say:hello,whb
Person:Person [name=whb, age=18]
附上该项目github:https://github.com/whbing/RPC 感谢WuBinBin提供博客参考。
上文通过简单的原生代码直观地感受了RPC运行过程。事实上,现有的用于生产的的RPC框架也有很多,如gRPC
,及dubbo
、spring cloud
中对RPC的实现,它们底层使用的序列化及网络通信组件各有不同,但基本原理类似,在此不再赘述。