1.前言
这篇文章主要结合(我自己开源的一个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 staticT 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(); ListparamsTypeName = new ArrayList (); 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序列化方式,
因为在服务端的各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.