目录
第一版:简单实现
第二版:静态代理
第三版:动态代理
第四版:动态代理封装请求参数以及返回值,实现该接口所有方法调用
第五版:动态代理所有接口所有方法调用
序列化框架介绍
最终版:优化序列化框架
总结
简单介绍一下然后主要看代码。
RPC(Remote Procedure Call) 远程过程调用,也可以称作远程方法调用。RPC只是一个概念,具体的实现有很多种。可以调用restful风格的接口,也可以使用Socket长连接来调用。
这里主要讲解使用Socket来调用接口,对于服务器内部通信来说通过Socket会比http的方式效率更高。
第一版
在公告服务中定义公共的类,客户端和服务端都要需要使用
package com.zxh.rpc.common;
import java.io.Serializable;
public interface IUserService extends Serializable {
public User findUserById(Integer id);
}
package com.zxh.rpc.common;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@ToString
public class User implements Serializable {
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
}
服务端实现公共接口
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
public class UserServiceImpl implements IUserService {
@Override
public User findUserById(Integer id) {
return new User(id,"Lucy");
}
}
服务端要做的事很简单,监听8888端口等待客户端连接。连接上后,通过InputStream
获取传入的id.通过OutputStream
返回User
对象
import com.zxh.rpc.common.User;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by zxh on 2022/1/25
*/
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8888);
while (running){
Socket s = ss.accept(); // 接收一个客户端连接
process(s);
s.close();
}
}
// 处理该客户端链接的输入输出流
private static void process(Socket s) throws Exception{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
DataInputStream dis = new DataInputStream(in);
DataOutputStream dos = new DataOutputStream(out);
int id = dis.readInt(); // 通过输入流读取数据
System.out.println("客户端发送的id:"+id);
UserServiceImpl service = new UserServiceImpl();
User user = service.findUserById(id);
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush();
}
}
然后看一下客户端如何调用
import com.zxh.rpc.common.User;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
public class Client {
// 客户端:将id=123 传给服务器并获取返回值
public static void main(String[] args) throws Exception{
Socket s = new Socket("127.0.0.1", 8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 字节数组输出流,就是一块内存空间
DataOutputStream dos = new DataOutputStream(baos); // 转换成数据输出流, 在外面包装了一层 DataOutputStream 专门用来写各种各样基础数据类型的
dos.writeInt(123);
// 通过socket 输出流 输出
s.getOutputStream().write(baos.toByteArray()); // toByteArray 最终将数据转成真正的字节数组
s.getOutputStream().flush();
// 通过socket 输入流 获取返回数据
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = new User(id,name);
System.out.println(user);
dos.close();
s.close();
}
}
第二版
对于客户端来说每次调用接口都要写网络连接的方法过于麻烦。关注业务的同时还要关注网络,方法的调用过于复杂不利于开发。于是变想到使用代理的方式帮我远程方法调用的问题。
新增一个Stub代理类实现了IUserService
接口
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
// 使用一个代理
public class Stub implements IUserService{
public User findUserById(Integer id){
User user = null;
try {
Socket s = new Socket("127.0.0.1", 8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 字节数组输出流 , 就是一块内存空间
DataOutputStream dos = new DataOutputStream(baos); // 转换成数据输出流, 在外面包装了一层 DataOutputStream 专门用来写各种各样基础数据类型的
dos.writeInt(11);
// 通过socket 输出流 输出
s.getOutputStream().write(baos.toByteArray()); // toByteArray 最终将数据转成真正的字节数组
s.getOutputStream().flush();
// 通过socket 输入流 获取返回数据
DataInputStream dis = new DataInputStream(s.getInputStream());
int receivedId = dis.readInt();
String name = dis.readUTF();
user = User.builder().id(receivedId).name(name).build();
dos.close();
s.close();
}catch (Exception e){
e.printStackTrace();
}
return user;
}
}
客户端修改,只要调用代理类方法即可
public class Client {
public static void main(String[] args) throws Exception {
Stub stub = new Stub();
System.out.println(stub.findUserById(123));
}
}
第三版
可以发现第二版其实就是将原来写在Client
的内容封装到了一个类中。好处就是IUserService
下的所有接口都可以在代理类Stub
中直接调用。但这样还是有缺陷,就是如果IUserService新增接口,那么Stub
就需要再写新的实现方法依然是十分麻烦。有没有什么办法可以代理所有接口呢?有!那就是动态代理。
对动态代理不太了解的可以看我这篇文章【Java】代理模式(Proxy模式)详解
同样的服务端代码无需修改
客户端调用
package com.zxh.rpc03;
import com.zxh.rpc.common.IUserService;
public class Client {
public static void main(String[] args) throws Exception {
IUserService stub = Stub.getStub();
System.out.println(stub.findUserById(123));
}
}
Stub代理类
package com.zxh.rpc03;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
// 使用动态代理
public class Stub {
// 通过该方法可以让客户端获取到 一个IUserService的实现类!!!
public static IUserService getStub(){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(123);
s.getOutputStream().write(baos.toByteArray());
s.getOutputStream().flush();
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = User.builder().id(id).name(name).build();
dos.close();
s.close();
return user;
}
};
// 代理IUserService,实现IUserService接口
IUserService service = (IUserService)Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
return service;
}
}
第四版
虽然第三版帮我们生成了IUserService的实现类,但还是无法代理所有的方法。因为请求参数,返回值,调用的方法都没有动态。这一版将对这些进行动态生成。
如何确定一个方法?
方法名,方法参数类型(以防有重载方法)即可确定一个方法,然后传入参数即可调用。
package com.zxh.rpc05;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
import java.io.DataInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
// 使用动态代理
public class Stub {
public static IUserService getStub(){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String methodName = method.getName(); // 方法名
Class<?>[] parameterTypes = method.getParameterTypes(); // 方法参数类型
oos.writeUTF(methodName);
oos.writeObject(parameterTypes);
oos.writeObject(args);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object o = ois.readObject(); // 返回值类型不做转换,直接返回Object
oos.close();
s.close();
return o;
}
};
// 代理IUserService,实现IUserService接口
IUserService service = (IUserService)Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
return service;
}
}
当然服务端也需要进行一定的修改
package com.zxh.rpc05;
import com.zxh.rpc.common.User;
import java.io.DataOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8888);
while (running){
Socket s = ss.accept(); // 接收一个客户端连接
process(s);
s.close();
}
}
// 处理该客户端链接的输入输出流
private static void process(Socket s) throws Exception{
ObjectInputStream ois = new ObjectInputStream(s.getInputStream()); // 输入流
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream()); // 输出流
String methodName = ois.readUTF();
Class[] parameterType = (Class[]) ois.readObject();
Object[] args = (Object[]) ois.readObject();
UserServiceImpl service = new UserServiceImpl();
Method method = service.getClass().getMethod(methodName, parameterType); //通过反射获取到方法
Object o = method.invoke(service, args); // 调用方法返回Object
System.out.println("调用的方法:"+methodName+"("+parameterType+")"+"参数:"+args );
oos.writeObject(o); // 通过输出流写出
oos.flush();
}
}
如上就实现了IUserService
的所有方法调用
第五版
当然还可以进行优化上面只实现了IUserService
定义的方法调用,那是否可以实现拿到任意类型的接口,能够帮我生成更多的代理?同样可以!那就是告诉服务端那个类型的接口。
先看一下客户端的调用
package com.zxh.rpc06;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
public class Client {
public static void main(String[] args) throws Exception {
IUserService stub = Stub.getStub(IUserService.class); // 传入需要代理的接口类型
User user = stub.findUserById(123);
System.out.println(user);
}
}
package com.zxh.rpc06;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
// 使用动态代理
public class Stub{
// v05: 传入什么类型就返回该类型的动态代理实现类
// 通过泛型来定义 返回值类型
public static <T> T getStub(Class<T> clazz){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String clazzName = clazz.getName(); // 获取到接口类型
String methodName = method.getName(); // 方法名
Class<?>[] parameterTypes = method.getParameterTypes();
oos.writeUTF(clazzName);
oos.writeUTF(methodName);
oos.writeObject(parameterTypes);
oos.writeObject(args);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object o = ois.readObject();
oos.close();
s.close();
return o;
}
};
// 生成代理,强转成T
T service = (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, h);
return service;
}
}
服务端同样要做修改
package com.zxh.rpc06;
import com.zxh.rpc.common.User;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8888);
while (running){
Socket s = ss.accept(); // 接收一个客户端连接
process(s);
s.close();
}
}
// 处理该客户端链接的输入输出流
private static void process(Socket s) throws Exception{
ObjectInputStream ois = new ObjectInputStream(s.getInputStream()); // 输入流
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream()); // 输出流
String clazzName = ois.readUTF(); // 管道是队列 先入先出。客户端先写的clazzName 这边就先读clazzName
String methodName = ois.readUTF();
Class<?>[] parameterType = (Class<?>[]) ois.readObject();
Object[] args = (Object[]) ois.readObject();
Class clazz = null;
// 这里需要注意,我们只能拿到接口的类型,而具体的实现类对象需要自己定义用哪个
if("com.zxh.rpc.common.IProductService".equals(clazzName)){
clazz = ProductServiceImpl.class; // 这里可以使用springIOC注入
}else if ("com.zxh.rpc.common.IUserService".equals(clazzName)){
clazz = UserServiceImpl.class;
}
Method method = clazz.getMethod(methodName, parameterType);
Object o = method.invoke(clazz.newInstance(), args); // 第一个参数是调用该方法的对象,第二个参数是调用该方法要传入的属性
System.out.println("调用的方法:"+methodName+"("+parameterType+")"+"参数:"+args );
oos.writeObject(o);
oos.flush();
oos.close();
ois.close();
}
}
这样就实现了所有暴露出来的接口都可以进行动态代理。
序列化框架
还有什么可以优化的呢?那就是序列化框架!
jdk自带的序列化框架java.io.Serializable
有一些缺点:
- 序列化后的大小较大
- 序列化的时间较长
市面上的一些序列化框架
- java.io.Serializable
- Hessian
- google protobuf
- facebook Thrift
- kyro
- fst
- xmlrpc(xstream)
- json序列化框架
- Jackson
- google Gson
- Ali FastJson
下面是JDK和Hessian序列化对比的程序
引入依赖
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.65</version>
</dependency>
package com.zxh.rpc09_Hessian02;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.zxh.rpc.common.User;
import java.io.*;
public class HessianVSJDK {
// Hessian 序列化的长度比JDK端,序列化的时间也比JDK短
public static void main(String[] args) throws Exception {
User user = new User("zhangsan", 1);
System.out.println("Hessian:"+hessianSerialize(user).length);
System.out.println("JDK:"+jdkSerialize(user).length);
}
public static byte[] hessianSerialize(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.writeObject(o);
output.flush();
byte[] bytes = baos.toByteArray();
baos.close();
output.close();
return bytes;
}
public static <T> T deserialize(byte[] bytes,Class<T> clazz) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bais);
Object o = input.readObject();
bais.close();
input.close();
return (T)o;
}
public static byte[] jdkSerialize(Object o) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(baos);
output.writeObject(o);
output.flush();
byte[] bytes = baos.toByteArray();
baos.close();
output.close();
return bytes;
}
public static <T> T jdkDeserialize(byte[] bytes,Class<T> clazz) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream input = new ObjectInputStream(bais);
Object o = input.readObject();
bais.close();
input.close();
return (T)o;
}
}
最终输出结果,可见JDK
序列化后的字节码长度要远大于Hessian
的序列化。且时间也比Hessian
长。这里就不做继续比较了
Hessian:45
JDK:187
最终版
改进序列化框架
首先封装一个请求对象
package com.zxh.rpc10;
import lombok.Data;
import java.io.Serializable;
@Data
public class RPCReq implements Serializable {
private String clazzName; // 接口类型名
private String methodName; // 方法名
private Class[] parameterTypes; // 方法参数类型
private Object[] args; // 方法实参
public RPCReq() {
}
public RPCReq(String clazzName, String methodName, Class[] parameterTypes, Object[] args) {
this.clazzName = clazzName;
this.methodName = methodName;
this.parameterTypes = parameterTypes;
this.args = args;
}
}
封装一个HessianUitl
package com.zxh.rpc10;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class HessianUtil {
public static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.writeObject(o);
output.flush();
byte[] bytes = baos.toByteArray();
baos.close();
output.close();
return bytes;
}
public static <T> T deserialize(byte[] bytes,Class<T> clazz) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bais);
Object o = input.readObject();
bais.close();
input.close();
return (T)o;
}
public static Object deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bais);
Object o = input.readObject();
bais.close();
input.close();
return o;
}
}
Stub
生成代理方法修改
package com.zxh.rpc10;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
public class Stub{
public static <T> T getStub(Class<T> clazz){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s = new Socket("127.0.0.1", 8888);
OutputStream output = s.getOutputStream();
InputStream input = s.getInputStream();
String clazzName = clazz.getName(); // 接口类型
String methodName = method.getName(); // 方法名
Class[] parameterTypes = method.getParameterTypes(); // 参数类型
// 封装请求对象
RPCReq req = new RPCReq(clazzName,methodName,parameterTypes,args);
byte[] serialize = HessianUtil.serialize(req); // 序列化请求对象
output.write(serialize);
output.flush();
byte[] bytes = new byte[1024];
input.read(bytes);
Object o = HessianUtil.deserialize(bytes); // 反序列化返回值
s.close();
output.close();
input.close();
return o;
}
};
// 代理IUserService,实现IUserService接口
T service = (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, h);
return service;
}
}
服务端代码修改
package com.zxh.rpc10;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket ss = new ServerSocket(8888);
while (running){
Socket s = ss.accept(); // 接收一个客户端连接
process(s);
s.close();
}
}
// 处理该客户端链接的输入输出流
private static void process(Socket s) throws Exception{
InputStream input = s.getInputStream();
OutputStream output = s.getOutputStream();
byte[] bytes = new byte[1024];
input.read(bytes); // 将字节流数据读入字节数组
RPCReq req = HessianUtil.deserialize(bytes, RPCReq.class); // 反序列化成RPCReq对象
String clazzName = req.getClazzName();
String methodName = req.getMethodName();
Class[] parameterTypes = req.getParameterTypes();
Object[] args = req.getArgs();
Class clazz = null;
// 根据接口类型调用对应的接口实现
if("com.zxh.rpc.common.IProductService".equals(clazzName)){
clazz = ProductServiceImpl.class;
}else if ("com.zxh.rpc.common.IUserService".equals(clazzName)){
clazz = UserServiceImpl.class;
}
Method method = clazz.getMethod(methodName, parameterTypes);
Object o = method.invoke(clazz.newInstance(), args); // 第一个参数是调用该方法的对象,第二个参数是调用该方法要传入的属性
byte[] serialize = HessianUtil.serialize(o); // 序列化返回值
output.write(serialize);
output.flush();
output.close();
input.close();
System.out.println("调用的方法:"+methodName+"("+parameterTypes+")"+"参数:"+args );
}
}
总结
RPC远程过程调用。其实就是客户端通过网络传递数据告诉服务端要调用哪个方法。而对于Java来说如何唯一确定一个方法呢?那就是接口类型,方法名,方法参数类型就可以唯一确定一个方法!再加上实参就可以调用方法了。
通过公共模块定义好一个接口以及其他参数类型和返回值类型。客户端和服务端都依赖于这个公共模块。
客户端通过JDK动态代理生成接口实现类。主要就是负责传递参数以及处理返回值。
服务端获取客户端的参数,根据接口类型找到对应的实现类,调用方法并返结果。
而其中在网络传输部分可以有Hessian
等序列化框架进行优化。