RPC客户端如何实现-KRPC源码解析

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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值