RPC演进
本文介绍RPC的演进过程,从最原始的RPC到现在的RPC框架
预备知识
通信实现(序列化和反序列化):
在Java中,我们要在客户端和服务端(两台机器间)间进行通信,我们需要将我们需要传输的对象进行序列化,也就是将对象数据或其他类型的数据转化成能在通信管道中传输的二进制数据。同样的,在服务端接受到数据后,我们需要将得到的二进制数据进行反序列化,也就是将二进制数据还原成对象或者其他类型的数据.
在Java中我们就是通过实现**Serializable
接口来实现序列化和反序列化的,只有实现了这个接口的数据才可以进行序列化和反序列化**
v1.0(原始版)
例如:
提供对象:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private Integer age;
}
提供服务和实现类:
//接口(服务)
public interface Userservice {
User findById(Integer id);
}
//接口实现
public class Userserviceimpl implements Userservice {
@Override
public User findById(Integer id) {
//模拟服务端从数据库中根据id查出数据并返回
return new User(id,"小明",18);
}
}
最初,我们就是通过TCP/IP
来实现最基本的二进制传输,也就是通过实现**Serializable
接口以及socket编程**来实现服务之间的通信
代码:
/**
* 服务端
*/
public class Server {
private static Boolean running=true;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8080);
while(running){
Socket ans=serverSocket.accept();
process(ans);
ans.close();
}
serverSocket.close();
}
private static void process(Socket ans) {
//模拟处理接受数据...
//一般都是通过InputStream和Outputstream来实现
}
}
/**
* 客户端
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket s=new Socket("127.0.0.1",8080);
//通过socket读取数据...
Integer id=new DataInputStream(s.getInputStream()).readInt();
String name=new DataInputStream(s.getInputStream()).readUTF();
Integer age=new DataInputStream(s.getInputStream()).readInt();
//获得数据
User user=new User(id,name,age);
//写数据到socket里再传输出去...
}
}
这种方法对于不熟悉网络编程的程序员来说简直就是场灾难,而且这样写的话,服务代码与实现通信的代码杂糅在一起,耦合度也是十分的高,开发效率极低.
所以我们需要对它进行一点改造
v2.0
代码如下:
/**
* 服务端
*/
public class Client {
public static void main(String[] args) throws IOException {
Stu stu=new Stu();
//直接调用方法即可
stu.findById(100);
}
}
/**
* 客户端
*/
public class Stu {
public User findById(Integer Id) throws IOException {
Socket s = new Socket("127.0.0.1", 8080);
//写数据到socket里...
//通过socket读取数据...
Integer id = new DataInputStream(s.getInputStream()).readInt();
String name = new DataInputStream(s.getInputStream()).readUTF();
Integer age = new DataInputStream(s.getInputStream()).readInt();
//获得数据
User user = new User(id, name, age);
return user;
}
}
你会发现,其实我们就是将Client网络端的代码封装到一个类中取别名Stu,然后我们通过Stu对外暴露的一个接口来实现服务。其实就是,我们通过引入了一个代理对象来实现我们需要做的事。我们将这件事全权交给了这个对象,而不需知道过多的细节
但是我们现在的服务还是不完善,我们依旧需要改进
v3.0
这是改动最大的一个版本,我们现在需要运用到设计模式中的代理模式中的动态代理
这里,我们
先上代码:
public class Stub {
public static Userservice getStub(){
InvocationHandler h=new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//处理方法
User user=new User(18,"小明",18);
return user;
}
};
Object o= Proxy.newProxyInstance(Userservice.class.getClassLoader(),Userservice.class.getInterfaces(),h);
return (Userservice) o;
}
}
public class Server {
public static void main(String[] args) throws IOException {
//通过动态代理获得一个实现了Userservice接口的代理对象
Userservice userservice=Stub.getStub();
//调用代理对象的方法来实现服务
userservice.findById(18);
}
}
这里我们把原本的Stub改进成一个动态代理模式,其中就输入我们想让他代理的接口,这样我们通过这个动态代理就能获得对应的实现了Userservice
的接口方法的代理对象。然后我们通过代理对象调用服务即可。
关于动态代理,可以点击这里:代理模式
这里就体现了代理模式的优点:
- 代理模式可以隐藏真实对象的实现细节,使客户端无需知晓真实对象的工作方式和结构。
- 通过代理类来间接访问真实类,可以在不修改真实类的情况下,对其进行扩展、优化或添加安全措施。
但是我们当前的服务还是有很大的缺陷,如果我们当前的接口又要新增方法,那么我们该怎么办呢?
v4.0
我们就想了,如果我们可以将客户端要调用的方法告诉服务端,由服务端来实现我们需要做的方法,再返回给我们可以吗?
答案显然是可以的,我们只需要在invoke方法中将我们需要调用的方法发送给服务端,服务端知道了我们要调用的方法,不就能通过反射等机制找到方法然后执行了吗?
public class Stub {
public static Userservice 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",8080);
//将方法名,参数类型和参数告诉服务端
ObjectOutputStream oss=new ObjectOutputStream(s.getOutputStream());
oss.writeObject(method.getName());
oss.writeObject(method.getGenericParameterTypes());
oss.writeObject(args);
//接受数据
User user=new User(18,"小明",18);
return user;
}
};
Object o= Proxy.newProxyInstance(Userservice.class.getClassLoader(),Userservice.class.getInterfaces(),h);
return (Userservice) o;
}
}
可以看到,我们将方法名,方法参数类型和参数给服务端,而服务端在根据这些数据找到指定的方法,最后再调用方法将数据返回给客户端
但是,我们现在还是有缺点,我们现在只能得到实现了Userservice接口的代理对象,我们希望能得到实现任意接口的代理对象该怎么做呢?
v5.0
其实很简单,我们只需要将我们需要的接口也告诉服务端不就可以了吗,于是我们就可以将返回值的类型改为:Object
,来返回任意接口类型的代理对象
代码:
public class Stub {
public static Object getStub(Class clz){
InvocationHandler h=new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket s=new Socket("127.0.0.1",8080);
//将方法名和参数类型告诉服务端
ObjectOutputStream oss=new ObjectOutputStream(s.getOutputStream());
oss.writeObject(method.getName());
oss.writeObject(method.getGenericParameterTypes());
//将接口名称也告诉服务端
oss.writeObject(clz.getName());
//接受数据
User user=new User(18,"小明",18);
return user;
}
};
Object o= Proxy.newProxyInstance(clz.getClassLoader(),clz.getInterfaces(),h);
return o;
}
}
所以再调用代理服务时,我们只需要告诉我们需要什么类型的接口,就可以得到对应的实现该接口的代理对象,进而调用对应的方法:
public class Server {
public static void main(String[] args) {
Userservice userservice = (Userservice) Stub.getStub(Userservice.class);
userservice.findById(18);
}
}
总结
从单机走向分布式,产生了很多分布式的通信方式
-
最古老也是最有效,并且永不过时的,TCP/UDP的二进制传输。事实上所有的通信方式归根结底都是TCP/UDP
-
CORBA Common Object Request Broker Architecture。古老而复杂的,支持面向对象的通信协议. Web Service (SoA SOAP RDDI WSDL…)基于http + xml的标准化Web APl
-
RestFul(Representational State Transfer)一种编码标规范,在请求网址时经常使用
-
RMI Remote Method InvocationJava内部的分布式通信协议
-
JMS Java Message Service
JavaEE中的消息框架标准,为很多MQ所支持
-
RPC (Remote Procedure Call)远程方法调用,这只一个统称,重点在于方法调用(不支持对象的概念),具体实现甚至可以用RMl RestFull等去实现,但一般不用,因为RMI不能跨语言,而RestFul效率太低。多用于服务器集群间的通信,因此常使用更加高效短小精悍的传输模式以提高效率。
什么是RPC:RPC(Remote Procedure Call)全称为远程过程调用,这其实是一个统称,它主要是会为了告诉远程的服务我需要什么调用什么接口的什么方法。它屏蔽了具体实现的细节,使得程序员只需要关注业务层面的事情,不需要再关注网络传输层面的事情。
RPC通讯协议:
-
http
-
http2.0(gRPC)
-
TCP
同步/异步阻塞/非阻塞
-
WebService