接上一篇《12.手写rpc框架-代码实现(五)》
上一篇我们编写了提供具体服务的rpc-sample-server服务端以及定义公共服务规范的rpc-sample-api工程,实现了Service服务的实现和暴露以及服务接口规范。下面我们来编写接入了rpc-client工程的服务调用客户端实例工程rpc-sample-client。 (作者:黄勇)
在MyEclipse新建名为rpc-sample-client的maven工程,该工程就是以rpc-client作为父工程,利用父级工程的rpc代理类,调用服务端的服务实现:



新建成功之后,在POM中引入依赖,因为需要使用Spring的容器加载,以及Spring的测试test依赖,所以引入了Spring的相关依赖。然后需要引入rpc-sample-api服务接口定义规范,以及rpc-client父级工程来使用rpcProxy进行远程服务调用。和rpc-sample-server一样,打包时在build中需要添加依赖的copy规则插件以及编译相关依赖类为jar的插件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>rpc-sample-client</artifactId>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<!-- RPC Sample API -->
<dependency>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-sample-api</artifactId>
<version>${project.version}</version>
</dependency>
<!-- RPC Client -->
<dependency>
<groupId>com.xxx.rpc</groupId>
<artifactId>rpc-client</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Dependency -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.9</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib</classpathPrefix>
<mainClass>com.xxx.rpc.sample.client.HelloClient</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
然后我们开始编写代码,这里我们调用两个版本的HelloService服务,并传入不同的参数。首先在src/main/java中创建com.xxx.rpc.sample.client包下的HelloClient类:
package com.xxx.rpc.sample.client;
import com.xxx.rpc.client.RpcProxy;
import com.xxx.rpc.sample.api.HelloService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class HelloClient {
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
RpcProxy rpcProxy = context.getBean(RpcProxy.class);
HelloService helloService = rpcProxy.create(HelloService.class);
String result = helloService.hello("World");
System.out.println(result);
HelloService helloService2 = rpcProxy.create(HelloService.class, "sample.hello2");
String result2 = helloService2.hello("世界");
System.out.println(result2);
System.exit(0);
}
}
然后在resource文件夹下创建一个Spring.xml,将rpc-client工程的服务代理类RpcProxy注入到Spring容器中:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:rpc.properties"/>
<bean id="serviceDiscovery" class="com.xxx.rpc.registry.zookeeper.ZooKeeperServiceDiscovery">
<constructor-arg name="zkAddress" value="${rpc.registry_address}"/>
</bean>
<bean id="rpcProxy" class="com.xxx.rpc.client.RpcProxy">
<constructor-arg name="serviceDiscovery" ref="serviceDiscovery"/>
</bean>
</beans>
在spring.xml中,注入了rpc-client工程的服务代理类RpcProx,并且为RpcProx类添加了注册中心服务发现对象serviceDiscovery,而serviceDiscovery是从rpc-registry-zookeeper工程中的ZooKeeperServiceDiscovery类基础上创建的,并指定了其相关注册中心地址参数zkAddress。
在resource文件夹下创建rpc.properties文件,将注册中心地址参数放置其中:
rpc.registry_address=127.0.0.1:2181
日志配置文件log4j.properties就不再赘述,同之前:
log4j.rootLogger=ERROR,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%m%n
log4j.logger.com.xxx.rpc=DEBUG
在上面的HelloClient类中,调用一个远程服务,进行了以下两步操作:
(1)从Spring容器中获取rpc远程服务代理类RpcProxy
(2)通过远程服务代理类的create方法创建相关代理服务Service类
对于(1),因为该工程在POM中引入了rpc-client工程,所以其服务代理类RpcProxy可以被添加到spring.xml,做为一个bean本加载至Spring容器中,供其它类调用。
对于(2),使用RpcProxy的create就可以获得相关代理服务Service类,其原理就是之前编写的利用Netty底层进行服务调用的方法,我们来回顾一下RpcProxy类:
package com.xxx.rpc.client;
import com.xxx.rpc.common.bean.RpcRequest;
import com.xxx.rpc.common.bean.RpcResponse;
import com.xxx.rpc.common.util.StringUtil;
import com.xxx.rpc.registry.ServiceDiscovery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.UUID;
//RPC 代理(用于创建 RPC 服务代理)
public class RpcProxy {
private static final Logger LOGGER = LoggerFactory.getLogger(RpcProxy.class);
private String serviceAddress;
private ServiceDiscovery serviceDiscovery;
public RpcProxy(String serviceAddress) {
this.serviceAddress = serviceAddress;
}
public RpcProxy(ServiceDiscovery serviceDiscovery) {
this.serviceDiscovery = serviceDiscovery;
}
@SuppressWarnings("unchecked")
public <T> T create(final Class<?> interfaceClass) {
return create(interfaceClass, "");
}
@SuppressWarnings("unchecked")
public <T> T create(final Class<?> interfaceClass, final String serviceVersion) {
// 创建动态代理对象
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[]{interfaceClass},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 创建 RPC 请求对象并设置请求属性
RpcRequest request = new RpcRequest();
request.setRequestId(UUID.randomUUID().toString());
request.setInterfaceName(method.getDeclaringClass().getName());
request.setServiceVersion(serviceVersion);
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParameters(args);
// 获取 RPC 服务地址
if (serviceDiscovery != null) {
String serviceName = interfaceClass.getName();
if (StringUtil.isNotEmpty(serviceVersion)) {
serviceName += "-" + serviceVersion;
}
serviceAddress = serviceDiscovery.discover(serviceName);
LOGGER.debug("discover service: {} => {}", serviceName, serviceAddress);
}
if (StringUtil.isEmpty(serviceAddress)) {
throw new RuntimeException("server address is empty");
}
// 从 RPC 服务地址中解析主机名与端口号
String[] array = StringUtil.split(serviceAddress, ":");
String host = array[0];
int port = Integer.parseInt(array[1]);
// 创建 RPC 客户端对象并发送 RPC 请求
RpcClient client = new RpcClient(host, port);
long time = System.currentTimeMillis();
RpcResponse response = client.send(request);
LOGGER.debug("time: {}ms", System.currentTimeMillis() - time);
if (response == null) {
throw new RuntimeException("response is null");
}
// 返回 RPC 响应结果
if (response.hasException()) {
throw response.getException();
} else {
return response.getResult();
}
}
}
);
}
}
可以看到,当时在RpcProxy的create方法中编写了创建动态代理对象的逻辑,为Proxy代理类的的newProxyInstance方法传入相关参数,其中InvocationHandler是我们自定义的,在该自定义类中,当调用代理类的相关方法时,通过RpcRequest创建RPC请求对象,通过RpcClient对象(其中使用Netty进行交互),向远程RPC服务请求相关服务类,获取远程调用结果。
create有两种方法,一个是带服务版本serviceVersion参数的,一个是不带的,结果是会加载带版本的、不带版本的Service服务类调用结果。
执行HelloClient类,我们可以得到以下结果:

可以看到成功调用了之前在rpc-sample-server定义的HelloServiceImpl以及HelloServiceImpl1的hello方法。然后创建HelloClient2类,来试验以下传送Bean作为参数的远程调用:
package com.xxx.rpc.sample.client;
import com.xxx.rpc.client.RpcProxy;
import com.xxx.rpc.sample.api.HelloService;
import com.xxx.rpc.sample.api.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class HelloClient2 {
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
RpcProxy rpcProxy = context.getBean(RpcProxy.class);
HelloService helloService = rpcProxy.create(HelloService.class);
String result = helloService.hello(new Person("Yong", "Huang"));
System.out.println(result);
System.exit(0);
}
}
在该类中,同样的从Spring容器中获取rpc远程服务代理类RpcProxy,然后通过远程服务代理类的create方法创建相关代理服务Service类,不同的是,这里使用的是传递Bean参数的hello方法,传递了一个Person对象,指定了firstName和lastName(本开源工程的作者黄勇老师的姓名拼音),最后得到的结果是:

传递Bean参数至远程Server的原理,就是将Bean进行序列化,传递到服务端后,进行反序列化生成真正的Bean。至此,我们的整个手写RPC的所有工程代码均已编写完毕。让我们来回顾一下之前的手写RPC的工程蓝图:

通过这几篇的手写RPC逻辑的学习,我们可以更加清楚的来理解上面整个蓝图的每一个工程扮演的角色,和使用到的核心技术。回顾一下之前的工程介绍作为版系列的总结:
(1)rpc-client
实现了rpc的服务动态代理(RpcProxy)以及基于Netty封装的一个客户端网络层(RpcClient)
(2)rpc-common
封装了RpcRequest和RpcResponse,即rpc请求和响应的数据结构
基于Netty提供了编解码器
提供了序列化反序列化等工具
(3)rpc-registry
提供了服务发现和注册接口
(4)rpc-registry-zookeeper
基于zookeeper的服务发现和注册接口
(5)rpc-server
rpc服务器(RpcServer)的实现,用来监听rpc请求以及向Zookeeper注册服务地址
rpc服务本地调用
(6)rpc-sample-api
rpc测试公共api服务接口
(7)rpc-sample-client
rpc测试客户端
(8)rpc-sample-server
rpc测试服务启动程序和服务实现
大家手写完RPC远程服务调用的逻辑后,不妨看一下目前使用比较广泛的Dubbo和Spring Cloud,看看它们的源码中,是如何实现RPC远程服务调用的,以便于我们更好的去使用分布式框架来开发分布式应用。