前言: 相信做开发的小伙伴们应该都使用过类似Dubbo,Feign的远程方法调用框架,那么有人了解其中的原理吗?本文就用通俗易懂的demo来实现RPC。
前置知识:
动态代理,socket(TCP/IP协议,也可以使用http等其他协议),反射
1.代码实现
- 服务端:
/**
* 服务端
*
* @author wrx
* @date 2022/5/13 22:30
*/
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
SpringSinglePool.initInstance();
ServerSocket serverSocket = new ServerSocket(8888);
while (running) {
Socket socket = serverSocket.accept();
System.out.println("链接进来了" + JSONUtil.toJsonStr(socket));
process(socket);
}
System.out.println("服务端关闭");
serverSocket.close();
}
private static void process(Socket socket) {
try {
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
//客户端按顺序传了类名,方法名,参数类型,参数值。服务端这边顺序读取
String clazzName = inputStream.readUTF();
String methodName = inputStream.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) inputStream.readObject();
Object[] args = (Object[]) inputStream.readObject();
//从服务注册表获取实例,例如spring的ContextLoader.getCurrentWebApplicationContext().getBean(
Object implObj = SpringSinglePool.singletonObjects.get(clazzName);
Method method = implObj.getClass().getMethod(methodName, parameterTypes);
Object returnObj = method.invoke(implObj, args);
outputStream.writeObject(returnObj);
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 用户服务实现类
*
* @author wrx
* @date 2022/5/13 0:31
*/
public class UserServiceImpl implements IUserService {
@Override
public User findUserById(Integer id) {
//实际是从db查
return new User(id, "Alice");
}
}
/**
* 假装我是spring池 ☺
* @author wrx
* @date 2022/5/14 23:30
*/
public class SpringSinglePool {
/**
* key:beanName value:实例
*/
public static Map<String, Object> singletonObjects = new HashMap<>();
/**
* 初始化实例
*/
public static void initInstance(){
singletonObjects.put("IUserService",new UserServiceImpl());
}
}
- 服务端暴露的接口
/**
* 服务接口 (服务端提供了实现,客户端只要通过stub调用即可)
*
* @author wrx
* @date 2022/5/13 0:31
*/
public interface IUserService {
User findUserById(Integer id);
}
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
- 客户端:
/**
* 远程对象在客户端的代理
* 为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象
*
* @author wrx
* @date 2022/5/13 22:54
*/
public class Stub {
/**
* 生成代理类
*
* @param: clazz 服务端暴露出来的接口(服务端提供了实现)
* @return: 返回代理后的实例
* @author: wrx
* @date: 2022/5/13 23:15
*/
public static <T> T getStub(Class<T> clazz) {
InvocationHandler handler = new InvocationHandler() {
/*
* 生成的代理实例调用任意方法都会进入此逻辑
*
* @param: proxy 代理对象
* @param: method 调用的方法
* @param: args 方法参数
* @return: 调用代理方法后返回的对象
* @author: wrx
* @date: 2022/5/13 22:55
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8888);
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
//将类名,方法名,参数类型(方法可能重载,需要根据类型判断),参数值传给服务端
//若服务端有多个实现类,这边还要传版本号过去version
String clazzName = clazz.getSimpleName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
outputStream.writeUTF(clazzName);
outputStream.writeUTF(methodName);
outputStream.writeObject(parameterTypes);
outputStream.writeObject(args);
outputStream.flush();
//接收服务端的响应
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
Object readObject = inputStream.readObject();
System.out.println("服务端响应的对象:" + JSONUtil.toJsonStr(readObject));
//关闭逻辑应在finally中,此处为代码简洁直接在方法抛异常
socket.close();
outputStream.close();
inputStream.close();
return readObject;
}
};
Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler);
//将代理实例转换为class对应的类
return clazz.cast(proxyInstance);
}
}
/**
* 测试远程调用
*
* @author wrx
* @date 2022/5/13 23:02
*/
public class Test {
public static void main(String[] args) {
IUserService stub = Stub.getStub(IUserService.class);
//服务端不需要知道接口的实现,就能获取返回值,实现了远程调用
System.out.println(stub.findUserById(66));
}
}
2.说明
以Feign举例: 当我们需要远程调用时,通过服务端暴露一个接口 IUserService,并提供它的实现 IUserServiceImpl。客户端只要引入 接口 IUserService,不需要考虑实现,直接调用 IUserService中的方法即可。
流程: 通过动态代理生成一个代理类,并在代理类调用任意方法时,执行代理后的方法(即 invoke 中的逻辑),此时会建立socket连接至服务端,并将类元信息发送给服务端(服务端才能根据信息判断要调用哪个接口服务)。服务端解析后,将方法实现的返回值写回给客户端,实现了远程调用。
3.对序列化的改进
序列化: 网络传输中都需要序列化;把对象转为二进制字节数组称为序列化。
目前demo中使用的是ObjectOutputStream,即JDK自带的 Serializable,这种方式只支持java,而且性能较差(序列化后的字节长度比较长,无用信息多)
。
解决: 使用序列化框架 Hessian
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.38</version>
</dependency>
/**
* 序列化工具
*
* @author wrx
* @date 2022/5/15 0:48
*/
public class HessianUtil {
/**
* 序列化
*
* @param: o 要转为字节数组的对象
* @return: 字节数组
* @author: wrx
* @date: 2022/5/15 0:23
*/
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(o);
hessian2Output.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
hessian2Output.close();
return bytes;
}
/**
* 反序列化
*
* @param: bytes 字节数组
* @return: 反序列化为对象
* @author: wrx
* @date: 2022/5/15 0:23
*/
public static Object deserialize(byte[] bytes) throws IOException {
ByteInputStream byteInputStream = new ByteInputStream();
Hessian2Input hessian2Input = new Hessian2Input(byteInputStream);
Object o = hessian2Input.readObject();
byteInputStream.close();
hessian2Input.close();
return o;
}
}
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
SpringSinglePool.initInstance();
ServerSocket serverSocket = new ServerSocket(8888);
while (running) {
Socket socket = serverSocket.accept();
System.out.println("链接进来了" + JSONUtil.toJsonStr(socket));
process(socket);
}
System.out.println("服务端关闭");
serverSocket.close();
}
private static void process(Socket socket) {
try {
// ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
Hessian2Input inputStream = new Hessian2Input(socket.getInputStream());
//客户端按顺序传了类名,方法名,参数类型,参数值。服务端这边顺序读取
String clazzName = inputStream.readString();
String methodName = inputStream.readString();
Class<?>[] parameterTypes = (Class<?>[]) inputStream.readObject();
Object[] args = (Object[]) inputStream.readObject();
//从服务注册表获取实例,例如spring的ContextLoader.getCurrentWebApplicationContext().getBean(
Object implObj = SpringSinglePool.singletonObjects.get(clazzName);
Method method = implObj.getClass().getMethod(methodName, parameterTypes);
Object returnObj = method.invoke(implObj, args);
// ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
Hessian2Output outputStream = new Hessian2Output(socket.getOutputStream());
outputStream.writeObject(returnObj);
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class Stub {
/**
* 生成代理类
*
* @param: clazz 服务端暴露出来的接口(服务端提供了实现)
* @return: 返回代理后的实例
* @author: wrx
* @date: 2022/5/13 23:15
*/
public static <T> T getStub(Class<T> clazz) {
InvocationHandler handler = new InvocationHandler() {
/*
* 生成的代理实例调用任意方法都会进入此逻辑
*
* @param: proxy 代理对象
* @param: method 调用的方法
* @param: args 方法参数
* @return: 调用代理方法后返回的对象
* @author: wrx
* @date: 2022/5/13 22:55
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8888);
// ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
Hessian2Output outputStream = new Hessian2Output(socket.getOutputStream());
//将类名,方法名,参数类型(方法可能重载,需要根据类型判断),参数值传给服务端
String clazzName = clazz.getSimpleName();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
// outputStream.writeUTF(clazzName);
// outputStream.writeUTF(methodName);
outputStream.writeString(clazzName);
outputStream.writeString(methodName);
outputStream.writeObject(parameterTypes);
outputStream.writeObject(args);
outputStream.flush();
//接收服务端的响应
// ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
Hessian2Input inputStream = new Hessian2Input(socket.getInputStream());
Object readObject = inputStream.readObject();
System.out.println("服务端响应的对象:" + JSONUtil.toJsonStr(readObject));
//关闭逻辑应在finally中,此处为代码简洁直接在方法抛异常
socket.close();
outputStream.close();
inputStream.close();
return readObject;
}
};
Object proxyInstance = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, handler);
//将代理实例转换为class对应的类
return clazz.cast(proxyInstance);
}
}
4.一些细节优化思路
1.注册中心注册的思路: 服务端
搞个Map<接口名,List<ip+端口+协议>>,启动时将需要的服务提供类及ip端口注册(list是因为服务端可能是集群)。 客户端
通过接口名从注册中心,随便搞个负载均衡算法取出ip+端口+协议,作为socket的参数发送请求。
要求:
共享数据:
可以利用redis这种单独的应用做Map共享数据,给予客户端及服务端共享访问(nacos,zk也行)。但是这样性能比较低,可以在本地搞个Map做本地缓存(查到redis则加入本地的Map)监听数据的变化:
如果服务提供者变多了等咋办?所以我们需要引入一些机制 - > 【redis的消费订阅】心跳检测
: 如果服务提供者挂了咋办?定时心跳检测,无心跳则过期掉。
2.容错降级思路:
直接把 Stub 类中的代理实现 invoke方法try catch起来,异常捕获中简单重试几次,重试后还是失败,则读取配置想要展示的降级信息。
3.假设服务端还在开发中:
客户端无法改动到服务端代码,所以改动Stub,响应mock数据,Stub 类中的代理实现 invoke方法直接取mock的内容响应。
String mock = System.getProperty("mock");
if (mock!=null && mock.startsWith("return:")){
return mock.replace("return:","");
}
4.支持不同协议
新增接口 Protocol,例socket协议的A类实现Protocol的 send方法,dubbo协议的B类实现Protocol的seng方法。(传参;读配置,工厂模式判断要调用A类还是B类的实现)