首先,要知道rpc(Remote Procedure Call)只是一种定义,表示的是远程调用。
啥是远程调用?
两台不在一起的电脑A和B,A电脑上存有一些用户数据,b想要查询a上存的用户信息,这个时候,就需要远程调用A暴露出来的接口进行查询。或者说调用A提供的服务进行查询。
两个电脑想要通过网络通信,最重要的:二进制!一切信息在网络中都可以看成bit0、bit1,或者说高电平、低电平。
接下来,看看rpc是怎么一步一步演进的。
先定义一下基本的User结构
import java.io.Serializable;
public class User implements Serializable {
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
A中还定义了一个接口,其中暴露了一个可以通过id查询User的方法
public interface IUserService {
public User findUserById(Integer id);
}
v1-最原始的二进制传输
既然网络传输使用的是二进制,那最直接的方法:把需要传输的user信息转换成二进制。
使用的是:ByteArrayOutputStream、ByteArrayInputStream
package rpc_v1;
public class Client {
public static void main(String[] args) throws IOException {
// 写id给server
Socket socket = new Socket("127.0.0.1",8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(123);
socket.getOutputStream().write(baos.toByteArray());
socket.getOutputStream().flush();
// 从server接收User信息
DataInputStream dis = new DataInputStream(socket.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = new User(id, name);
System.out.println(user);
dos.close();
socket.close();
}
}
客户端client需要把查询id传递给server,使用ByteArrayOutputStream ,再套一层DataOutputStream,为了方便我们写入各种数据类型。再通过socket.getOutputStream().write(baos.toByteArray()); 把我们的数据通过二进制传递出去啦~
package rpc_v1;
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();
}
ss.close();
}
private static void process(Socket s) throws Exception{
InputStream inputStream = s.getInputStream();
OutputStream outputStream = s.getOutputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
//读client发过来的id
int id = dataInputStream.readInt();
// 查找服务
IUserService service = new UserServiceImpl();
User user = service.findUserById(id);
// 写会对象
dataOutputStream.writeInt(user.getId());
dataOutputStream.writeUTF(user.getName());
dataOutputStream.flush();
}
}
对于server端,
- 1 读取client发送的id,nt id = dataInputStream.readInt();
- 2 查找User
- 3 把这个User对象写给client
这里是最最最基本的方法,把这个对象的每个数据类型写回去!
这种方式的缺点很明显,比如User对象改变一个属性,那么server端的代码都需要改。这种写死的方法,要求client、server都必须了解对象,且每一个方法都需要再写方法。且传输过程代码、业务逻辑代码全部混淆在一起,太糟糕了!
v2-简化客户端,引入stub
v1太糟糕了,那么v2想要的改进点:把client端传输代码和业务代码拆分开。
使用一个代理stub代理client中网络通信的操作。
public class Client {
public static void main(String[] args) throws IOException {
Stub stub = new Stub();
System.out.println(stub.findUserById(123));
}
}
public class Stub {
public User findUserById(int i) throws IOException {
Socket socket = new Socket("127.0.0.1",8888);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(123);
socket.getOutputStream().write(baos.toByteArray());
socket.getOutputStream().flush();
DataInputStream dis = new DataInputStream(socket.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = new User(id, name);
System.out.println(user);
dos.close();
socket.close();
return user;
}
}
但这个stub缺点同样很明显,只代理了一个findUserById方法就需要写这么多,有更多的方法,就需要写更多更多的代码,差评!
v3-动态代理生成service
v2虽然把client端网络通信和业务代码分开了,但目前的代理只能代理一个方法。
希望:代理直接给客户端提供封装好的service,这个service有getUserById方法,且代理给client屏蔽掉了网络细节,那我就可以远程访问了!
public class Client {
public static void main(String[] args) throws Exception {
IUserService service = Stub.getStub();
System.out.println(service.findUserById(123));
}
}
如上的代码,client直接调用service.findUserById(123)就可以通信了,原本的网络通信呢,被service隐藏了,但它执行findUserById时会给我们加上网络通信的逻辑,无需我们再操心。
(这里其实涉及了代理模式)
当我们在client调用service时,这个类是动态产生的。使用Stub.getStub()实际会调用stub中getStub中定义的invoke方法。
public class Stub {
public static IUserService getStub(){
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
Socket socket = new Socket("127.0.0.1",8888);
//传给server
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(123);
socket.getOutputStream().write(baos.toByteArray());
socket.getOutputStream().flush();
//接收server的信息
DataInputStream dis = new DataInputStream(socket.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = new User(id, name);
dos.close();
socket.close();
return user;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );
return (IUserService)o;
}
}
InvocationHandler是调用方法时的处理
invoke方法有三个参数
- proxy :哪个代理
- method :哪个方法
- args:哪些输入参数
这个stub的写法好处很明显,当给IUserService添加新的方法时,我们都可以在invoke可统一处理
v4-动态代理,支持多个方法
v3的stub其实存在缺陷,代码:dos.writeInt(123);是写死的。不论你调用的啥方法,传给server的都是123。想用别的方法怎么办呢?继续完善。
stub采用动态代理的思想来设计,
public class Stub {
public static IUserService getStub(){
InvocationHandler h = new InvocationHandler(){
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
Socket socket = new Socket("127.0.0.1",8888);
// 改动部分
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
String methodName = method.getName(); //获取方法名
Class[] parameterTypes = method.getParameterTypes(); //获取方法参数类型,避免在server端的重载
out.writeUTF(methodName); // 代替client把方法名写出去
out.writeObject(parameterTypes); // 把参数类型写出去
out.writeObject(args); // 把参数写出去
out.flush();
DataInputStream in = new DataInputStream(socket.getInputStream());
int id = in.readInt();
String name = in.readUTF();
User user = new User(id, name);
out.close();
socket.close();
return user;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );
return (IUserService)o;
}
}
对我们的服务器端,同样改进。
当我们的client如下时:
public class Client {
public static void main(String[] args) {
IUserService service = Stub.getStub();
System.out.println(service.findUserById(123));
}
}
通过service调用findUserById时,会调用到stub的invoke,传入的参数method=findUserById。最终通过:方法名》参数类型》参数,这种顺序传递给了server端。
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();
}
ss.close();
}
private static void process(Socket s) throws Exception{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream oin = new ObjectInputStream(in);
DataOutputStream dout = new DataOutputStream(out);
//改进部分
String methodName = oin.readUTF(); // 读入方法名
Class[] parameterTypes = (Class[]) oin.readObject(); //读参数类型
Object[] args = (Object[])oin.readObject(); //读参数
IUserService service = new UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes); //找到这个方法
User user = (User)method.invoke(service, args); // 调用
dout.writeInt(user.getId());
dout.writeUTF(user.getName());
dout.flush();
}
}
在server端,接收到方法名、参数类型、参数后,通过 IUserService service = new UserServiceImpl()提供一个服务。
这样的改进,可以提供对同一个接口的很多方法支持,但是只能支持一个接口( IUserService service = new UserServiceImpl();),这里写死了。
并且在stub这,回传的对象还是经过了拆解的,如下。
DataInputStream in = new DataInputStream(socket.getInputStream());
int id = in.readInt();
String name = in.readUTF();
User user = new User(id, name);
v5 返回值用object封装,支持任意类型
针对v4中的缺点,改进措施:读写都以obejct为单位
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();
}
ss.close();
}
private static void process(Socket s) throws Exception{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream oin = new ObjectInputStream(in);
DataOutputStream dout = new DataOutputStream(out);
String methodName = oin.readUTF();
Class[] parameterTypes = (Class[]) oin.readObject();
Object[] args = (Object[])oin.readObject();
IUserService service = new UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes);
User user = (User)method.invoke(service, args);
/**
dout.writeInt(user.getId());
dout.writeUTF(user.getName());
**/
//改进部分
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(user);
dout.flush();
}
}
public class Stub {
public static IUserService getStub(){
InvocationHandler h = new InvocationHandler(){
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
Socket socket = new Socket("127.0.0.1",8888);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
String methodName = method.getName();
Class[] parameterTypes = method.getParameterTypes();
out.writeUTF(methodName);
out.writeObject(parameterTypes);
out.writeObject(args);
out.flush();
/**
DataInputStream in = new DataInputStream(socket.getInputStream());
int id = in.readInt();
String name = in.readUTF();
User user = new User(id, name);
**/
// 改动后
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User user = (User)ois.readObject(); // 把返回值封装成了对象
out.close();
socket.close();
return user;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );
return (IUserService)o;
}
}
到这为止,接口可以任意增减方法,user类可任意增减属性,代码都适用。
v6 支持任意service
v5的局限性还是有,那就是在client端IUserService service = Stub.getStub(),我们只能拿到IUserService,写stub只能把我们生成这个。
//v5-stub中写死了
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );
我们希望,可以通过该stub拿到任意接口。
其实很简单,只要把希望的service类型传给stub就行了
public class Stub {
//public static IUserService getStub(){
public static Object getStub(Class clazz){
InvocationHandler h = new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = new Socket("127.0.0.1",8888);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
String clazzName = clazz.getName(); // 先获取一波类名
String methodName = method.getName();
Class[] parameterTypes = method.getParameterTypes();
out.writeUTF(methodName);
out.writeObject(parameterTypes);
out.writeObject(args);
out.flush();
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
//User user = (User)ois.readObject();
Object o = ois.readObject();
out.close();
socket.close();
return o;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );
//return (IUserService)user;
return o;
}
}
在server端,小修改即可
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();
}
ss.close();
}
private static void process(Socket s) throws Exception{
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(in);
String clazzName = ois.readUTF(); //先读进来类名即可
String methodName = ois.readUTF();
Class[] parameterTypes = (Class[]) ois.readObject();
Object[] args = (Object[])ois.readObject();
/**
IUserService service = new UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes);
User user = (User)method.invoke(service, args);
**/
// 这里其实用spring注入即可
Class clazz = null;
clazz = UserServiceImpl.class; //从服务注册表找到具体的类
Method method = service.getClass().getMethod(methodName,parameterTypes);
Object o = method.invoke(clazz.newInstance(), args);
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(o);
oos.flush();
}
}
v7
到v6,rpc的主要逻辑已经很ok了。
但是,远程过程调用最底层的逻辑,就是把数据变成二进制在网络中传输。在java中,变成二进制(或者说序列化)的主要方法是实现Serializable接口的类,就可以被序列化。内部逻辑是java端的,这个序列化方法是只支持java的、性能差、系列化后长度又很长。
总而言之,就是不太好用。因此,现在有很多rpc序列化框架。下图,都主要是用来序列化的!!!