1.前言
这篇文章主要结合KRPC(我自己开源的一个RPC框)代码详细分析一下RPC客户端具体实现。在一篇文章了解RPC框架原理文中,我们主要讲述了一次调用RPC调用中各流程,这篇文章就结合KRPC的代码仔细讲解一下
开始前,我先说一下KRPC的网络传输中的内容:
1.服务实现名字。server端需要你服务实现的名字,才能知道你调用的是哪个实现的方法,跟web项目中的controller写的路径一样。
2.方法名字。知道了调用的是哪个类,接下来就需要调用的哪个方法了,无需多言。
3.方法参数名字。KRPC会获取每个方法参数的class的全路径(原因在下面展开讲)
4.方法中传入的值。
2.源码分析
2.1 客户端端初始化
KRPC在调用时,必须要先执行
KRPC.init("/opt/krpc/client/client.xml");
该方法内部解析该配置文件,把配置文件中的服务及其地址解析后并放入内存缓存中,供后面TCP请求时,快速获取到服务地址。
2.2 动态代理
可以看到,我们调用的都是接口,并且我们没有引入接口的实现包,调用后怎么能获取到server端的数据呢?
这就引入了动态代理,由代理类替我们处理接口调用的动作。这里使用的是jdk的动态代理实现方式。
调用者接口代理获取方法
UserService service = ProxyFactory.create(UserService.class, "user", "userService");
ProxyFactory#create方法
public static <T> T create(Class<?> type, String serviceName,String serviceImpleName) {
ProxyHandler handler = new ProxyHandler(serviceName,serviceImpleName);
return (T) handler.bind(new Class<?>[]{type});
ProxyHandler.java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 构造请求request
Request request = new Request();
request.setMethodName(method.getName());
request.setServiceImplName(serviceImplName);
request.setParamsValues(Arrays.asList(args));
Class[] sourceTypes = method.getParameterTypes();
List<String> paramsTypeName = new ArrayList<String>();
for (int i = 0; i < args.length; i++) {
paramsTypeName.add(sourceTypes[i].getName());
}
request.setParamsTypesName(paramsTypeName);
Class returnClass = method.getReturnType();
return RequestHandler.request(serviceName, request, returnClass);
}
上面放了3段代码,是动态代理相关的全部代理,是的,就这么简单。通过ProxyFactory将接口绑定,获取接口代理,这样调用接口中的方法,直接交给ProxyHandler#invoke方法来执行。
invoke中主要是构建网络请求的参数Request类,然后调用请求控制类request。
我们在前言中有讲到Request的内容。这里说明一下对于参数Class为什么传递类的全路径名字,而不是Class类。
1.因为服务端的类是通过URLClassLoader动态加载进来的,客户端直接传递客户端这边的Class会报ClassNotFound异常
2.传递起类路径数据长度更小
服务端只需要根据类名,通过Class.forname加载进来就ok了。
2.3. 序列化
序列化是RPC框架中重要的一个环结,KRPC采用了Hessian序列化方式,common中的序列化工具
因为在服务端的各service的类都是动态加载进来的
public static Object deserialize(byte[] by, ClassLoader classLoader) throws IOException {
if (by == null)
throw new NullPointerException();
ByteArrayInputStream is = new ByteArrayInputStream(by);
ClassLoader old = null;
if (classLoader != null) {
old = Thread.currentThread().getContextClassLoader();
// 切换当前线程classloader,保证动态加载的类不会报CNF
Thread.currentThread().setContextClassLoader(classLoader);
}
HessianInput hi = new HessianInput(is);
Object obj = hi.readObject();
if (classLoader != null) {
Thread.currentThread().setContextClassLoader(old);
}
return obj;
}
所以在凡序列中,server端需要传入动态加载的classLoader。
Hessian在凡序列化时,会获取当前线程的ClassLoader,所以我们在外面修改了当前线程的classloader(这也是迭代的目标,这样做有些不稳妥)。
同时我也引入了压缩功能,这样让传输的字节更少。
2.4.TCP请求
传输的数据准备好了,也从配置文件中获取到服务的地址,接下来就要进行TCP请求了。
public static byte[] send(byte[] sendData,String host,int port,int timeout) throws UnknownHostException, IOException {
Socket socket = new Socket(host,port);
socket.setSoTimeout(timeout);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
byte resultArray[] = null;
try {
os.write(sendData);
os.flush();
socket.shutdownOutput();
resultArray = IOUtils.toByteArray(is);
} catch (Exception e) {
e.printStackTrace();
} finally {
os.close();
is.close();
socket.close();
}
return resultArray;
}
嗯。。这个TCP请求写的还是相当简单的。以后会迭代这一块的,可能会采用netty的方式实现客户端,可以能用连接池,这个我会深入对比,选择一个好的方案,到时候在来更新博客。
2.5.数据反序列化
通过TCP请求,获取的服务端返回的字节,这时候使用Hessian凡序列就行。
因为客户端会引入服务的接口包,使用AppClassloader加载,所以在客户端无需修改当前线程的classloaer。
3.总结
对于写一个RPC框架,主要是先构造出网络传输的数据格式(协议),客户端的难点主要在TCP请求这一块把。因为用户会引入接口包,所以序列化这一块客户端比较好实现。
对于KRPC在高并发压测下,表现还有要改进的地方,我也会持续迭代更新,争取能让期用于生产环境。关于改进的点,我也会在博客中持续更新。
欢迎对RPC框架原理感兴趣的同学与我交流。https://github.com/yangzhenkun/krpc/ KRPC源码地址,欢迎star,issues.