RPC(Remote Procedure Call)

从单机走向分布式,产生了很多分布式的通信方式

  • 最古老也是最有效,并且永不过时的,TCP/UDP的二进制传输,事实上所有的通信方式归根结底都是TCP/UDP
  • CORBA Common Object Request Broker Architecture。古老而复杂的,支持面向对象的通信协议。
  • Web Service(SOA SOAP RDDI WSDL ...)基于http+xml的标准化Web API
  • RestFul 回归简单化本源的Web API的事实标准,http+json
  • RMI Remote Method Invocation Java内部的分布式通信协议
  • JMS Java Message Service JavaEE中的消息框架标准,为很多MQ所支持
  • RPC Remote Procudure Call 远程过程方法调用,这只是一个统称概念,远程通信的方式,重点在于方法调用(不支持对象的概念),具体实现甚至可以用RMI RestFul等去实现,但一般不用,因为RMI不能跨语言,而RestFul效率太低。多用于服务器集群间的通信,因此常使用更加高效,短小精悍的传输模式以提高效率。

    从单机到分布式->分布式通信(一台机器解决不了的问题,需要多台机器解决,多台机器内部需要进行通信)->最基本:二进制,两台机器之间通过网络通信一定是二进制数据传输TCP/IP(socket)

RPC01

首先构造一个用于传输的类User,该类实现了序列化可以进行序列化从而通过网络传输二进制数据,他会跑在一台机器上去访问数据库,有人想拿到这个User的话我会对外提供一些服务



然后通过AUserService暴露FindById接口来进行数据查询,你给我一个ID,我给你一个User

User.java

package com.airsky.demo.rpc.Common;

import java.io.Serializable;

public class User implements Serializable {
private static final long serialVersionUID = 1L;
int id;
String name; public User(int id, String name) {
this.id = id;
this.name = name;
} public int getId() {
return id;
} public void setId(int 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 + '\'' +
'}';
}
}

AUserService.java

package com.airsky.demo.rpc.Common;

public interface AUserService {
User findById(int id);
}

最终在AUserServiceImpl进行了实现,对传入的id进行了模拟数据库查询,返回一个User对象。这里只进行一个模拟,具体实现的话也很简单。

package com.airsky.demo.rpc.Common;

public class AUserServiceImpl implements AUserService{

    @Override
public User findById(int id) {
//模拟数据库查询
return new User(id,"AirSky");
}
}

如果我们想通过最原始的方式来完成上述操作该怎么做呢?这个也是很多游戏和软件类似功能最底层的实现。

首先我们这台机器对外提供服务,就需要提供一个Server打开一个Socket端口进行监听,下面的实现是在Server类中新建了一个ServerSocket对象,并监听8888端口,accept接收一个客户端连接,然后通过send方法对连接进行处理。

public class Server {
//加上条件变量,不然s.close()不可达
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket s=new ServerSocket(8888);
while (running){
System.out.println("接收连接中...");
Socket client=s.accept();
System.out.println("连接主机:" + client.getInetAddress());
send(client);
client.close();
}
s.close();
}

下面来实现send方法对连接进行进一步处理。大概就是你给我一个id,我给你一个id和id对应的名字,就是这么简单。send方法传入了一个socket对象,我们可以通过socket对象的getInputStream方法来打开一个输入流,getOutputStream方法打开一个输出流。使用DataOutputStream的readInt对输入流中的数据进行指定方式读取,使用该包装流来简化读写操作,如果客户端以正确的方式传送了ID的即可读取到,然后调用AUserService的findById方法来查询id对应的User对象,最后通过DataOutputStream的writeInt和writeUTF以指定的方式分别向输出流中写入获取到的id和name。

    private static void send(Socket client) throws Exception {
//获取Socket的输入流,用Data流包装读取二进制数据
DataInputStream dis = new DataInputStream(client.getInputStream());
//获取Socket的输出流,用Data流包装写出二进制数据
DataOutputStream dos = new DataOutputStream(client.getOutputStream()); //读取Id
int id = dis.readInt();
System.out.println("接收到ID为:"+id);
AUserService service= new AUserServiceImpl();
User user = service.findById(id);
//写出数据
System.out.println("返回的对象为:"+user);
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush();
dis.close();
dos.close();
}

服务端实现了我们再来实现以下客户端。服务端监听了8888端口我们是不是就需要新建一个Socket来连接8888端口,同样使用getOutputStream来获取一个输出流,再用DataOutputStream向流中写指定格式数据(二进制),最后来使用getInputStream获取输入流得到数据n。得到数据之后new一个新对象拿来用。

public class Client {
public static void main(String[] args) throws Exception {
Socket s = new Socket("127.0.0.1",8888);
//使用DataOutputStream包装以二进制写入
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(8888);
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();
String name = dis.readUTF();
User user = new User(id,name);
System.out.println(user);
}
}

现在我们启动服务端,然后启动客户端后,服务端和客户端就会收到以下数据





以上这种是最简单最原始的方式,这种方式非常的不灵活,这种方式只能传输单一的对象,对于传输的对象必须了解才能传的过去拿的过来,如果在代码量大的情况下需要对某个或者多个对象加属性,那么我们的就需要大量整 改,尤其是传输过程和业务逻辑代码全部混合在一起的时候,所以我们就需要一种写起来更加爽的方式。RPC02应运而生。

RPC02

假如我现在只是个业务开发,对网络这块儿不太熟,现在我想告诉自己,能不能给个简单的方法,让我直接访问接口编写逻辑代码就行了。所以这里我们就需要对网络这块儿进行一个简单的封装。这就是RPC的演进过程。

最简单的方法是将网络操作封装为代理类Stub,Stub屏蔽了关于网络的细节

package com.airsky.demo.rpc.rpc02;

import com.airsky.demo.rpc.Common.User;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket; public class Stub {
public User findUserById(Integer id) throws Exception {
Socket s = new Socket("127.0.0.1",8888);
//使用DataOutputStream包装以二进制写入
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(id);
DataInputStream dis = new DataInputStream(s.getInputStream());
int id1 = dis.readInt();
String name = dis.readUTF();
User user = new User(id1,name);
return user;
}
}

现在只要在new一个Stub出来,用Stub的findUserById方法就能拿到对象

Client中调用代理类

package com.airsky.demo.rpc.rpc02;

public class Client {
public static void main(String[] args) throws Exception {
Stub stub = new Stub();
System.out.println(stub.findUserById(33));
}
}

这就是第一步的演进,开发是一个不断螺旋递增的迭代过程,瀑布式的模型在很多场景下,尤其是互联网下早就被敏捷开发所替代了,敏捷一定要迭代。我们很多时候了解到的都是结果,直接接触到了最终的版本,所以我们根本理解不了中间的演进过程,也理解不了前人们为什么做出来这么多的改进,这就是我们不能理解RPC的原因。从历史演进学习技术,会理解得更透彻,知道前因后果,怎么问题的步骤与思路。

RPC03

到了这里大家可能会说,作为一个Stub,这非常的不完善,你只能代理一个方法,返回一个类,这也太弱了。那我们就一点点来演进。

如果说Stub能给我提供这样的一个接口,我们想使用Service的findUserById,而Stub的接口提供给我这个方法。通过Stub代理过的findUserById方法我们就能远程访问了。当我们调用findUserById的时候,他帮我们加进去了一些代码,这些代码就是网络服务,所以Stub这里实现了代理模式里面的动态代理,这里最难的。

Stub

package com.airsky.demo.rpc.rpc03;

import com.airsky.demo.rpc.Common.IUserService;
import com.airsky.demo.rpc.Common.User; 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 {
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在内存中声明一个数组流
// ByteArrayOutputStream bos = new ByteArrayOutputStream();
//使用DataOutputStream包装以二进制写入
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(456);//这里写死了需要改进
// s.getOutputStream().write(bos.toByteArray());
//
// System.out.println(s.getInputStream().read());
DataInputStream dis = new DataInputStream(s.getInputStream());
int id1 = dis.readInt();
String name = dis.readUTF();
User user = new User(id1,name); dos.close();
dis.close();
s.close();
return user;
}
};
Object o= Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h);
System.out.println(o.getClass().getName());
System.out.println(o.getClass().getInterfaces()[0]);
return (IUserService)o; }
}

Client

package com.airsky.demo.rpc.rpc03;

import com.airsky.demo.rpc.Common.IUserService;
import com.airsky.demo.rpc.Common.User; public class Client {
public static void main(String[] args) {
IUserService service=Stub.getStub();//返回一个动态生成的对象
User user=service.findUserById(111);
System.out.println(user);
}
}

当我们在Client端调用findUserById的时候,首页一点要明白,service是一个具体的类,是我们动态产生的,当调用这个方法的时候,他本质上会调用动态代理,那么既然调用了动态代理,那么就要使用InvocationHandler调用处理器进行处理,从而调用我们在invoke方法中实现的内容。invoke的第一个参数声明了调用该方法的代理实例,第二参数指向了它里边的findUserById方法,第三个参数为传入的参数。这样我们就可以代理实现修改类方法。这样写的好处在于以后在这个接口里添加任何方法的时候都可以在这里进行处理。此时通过动态代理产生的是一个实现了UserService接口的新的类,通过打印o类名即可看出。



通过反射获取className看出这是动态产生的新类$Proxy0



我们通过invoke第二个参数告诉他这个是实现UserService接口。





通过反射获取interface看出这是实现了UserService接口



我们对这个类进行任何方法调用的时候,都是要经过InvocationHandler的处理,然后我在里面可以判断你调用的是哪个方法,我做相应的处理,调用findUserById我要联网,调用别的我要检查你的权限等等。

RPC04

上一个版本里的代码是有缺陷的,为什么说呢,可以看到在Stub的此处



不管我们调用哪个方法,都只能传456,如果我们想实现一个Save功能那不就完蛋了吗。所以我们需要改进这里为可控参数。那么怎么改进呢,无论什么方法,我都用一个通用的版本实现即可。既然我们要远程调用其他方法,那么就可以通过invoke方法的三个参数来获取被代理的的方法信息,然后将方法信息传输过去。

Stub

package com.airsky.demo.rpc.rpc04;

import com.airsky.demo.rpc.Common.IUserService;
import com.airsky.demo.rpc.Common.User; import java.io.DataInputStream;
import java.io.DataOutputStream;
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将对象数据序列化传输过去。
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String methodName = method.getName();
Class[] parametersTypes = method.getParameterTypes();
oos.writeUTF(methodName);
oos.writeObject(parametersTypes);
oos.writeObject(args);
oos.flush(); DataInputStream dis = new DataInputStream(s.getInputStream());
int id1 = dis.readInt();
String name = dis.readUTF();
User user = new User(id1,name); oos.close();
dis.close();
s.close();
return user;
}
};
Object o= Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h);
System.out.println(o.getClass().getName());
System.out.println(o.getClass().getInterfaces()[0]);
return (IUserService)o; }
}

下面来假设一下,假如我们要调用saveUser方法,我们通过getName获取了方法名,通过getParamterTypes获取了参数类型,args获取到了参数,那么我们把这些数据通过ObjectOutputStream流告诉服务端,马上给我调这个方法,参数类型数据都给你。服务端马不停蹄的给我找到了这个方法,并且传入参数得到了返回值。

既然客户端做了这么大的改变,那么服务端是不是也要修改呀?是滴,服务端同样使用ObjectInputStream读入对象信息,首先按照传输的顺序使用readUTF读取方法名,然后使用readObject读取参数类型,返回类型为Class数组,接着使用readObject读取方法参数args,返回类型为Object,接下来先实例化需要调用方法的类,然后通过反射找到对应方法名和参数类型的方法,最后通过invoke进行反射调用,args为参数值,调用完成之后即可得到该方法的返回值得到一个User对象。当然如果我们想要实现一个saveUser方法也同样可以使用这种方式进行调用,只需要改变一下Client的方法名,其它的不需要进行改动。非常方便。

package com.airsky.demo.rpc.rpc04;

import com.airsky.demo.rpc.Common.UserService;
import com.airsky.demo.rpc.Common.UserServiceImpl;
import com.airsky.demo.rpc.Common.User; import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket; public class Server {
//加上条件变量,不然s.close()不可达
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket s=new ServerSocket(8888);
while (running){
System.out.println("接收连接中...");
Socket client=s.accept();
System.out.println("连接主机:" + client.getInetAddress());
send(client);
client.close();
}
s.close();
} private static void send(Socket client) throws Exception { ObjectInputStream ois= new ObjectInputStream(client.getInputStream());
//获取Socket的输出流,用Data流包装写出二进制数据
DataOutputStream dos = new DataOutputStream(client.getOutputStream()); String methodName = ois.readUTF();
Class[] parameterTypes= (Class[]) ois.readObject();//读取类型
Object[] args= (Object[]) ois.readObject();//读取Id
System.out.println("接收到ID为:"+args[0]);
UserService service=new com.airsky.demo.rpc.rpc04.UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes);
User user = (User) method.invoke(service,args);
//写出数据
System.out.println("返回的对象为:"+user);
// client.getOutputStream().write(user.getName().getBytes());
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush();
ois.close();
dos.close();
//
//
// System.out.println(client.getInputStream().read());
}
}

所以在这个版本里面我可以提供很多方法的支持,对于同一个接口里面很多方法支持。

RPC05

但是如果我们想要对随意接口的任意方法进行调用,那么上面的写法就无法满足我们了。还有我们的Client端对于返回值的处理还不完善,现在是将user对象拆解来进行传输的。如果我们的User实现改变了,那么所有的代码也需要进行改变,这肯定不行。所以在Server发送和Client接收对象时我们均使用ObjectOutputStream来直接传输user对象的序列化数据。这样的话我们的User就可以随意改变了。

Server

package com.airsky.demo.rpc.rpc05;

import com.airsky.demo.rpc.Common.User;
import com.airsky.demo.rpc.Common.UserServiceImpl; 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 {
//加上条件变量,不然s.close()不可达
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket s=new ServerSocket(8888);
while (running){
System.out.println("接收连接中...");
Socket client=s.accept();
System.out.println("连接主机:" + client.getInetAddress());
send(client);
client.close();
}
s.close();
} private static void send(Socket client) throws Exception { ObjectInputStream ois= new ObjectInputStream(client.getInputStream()); ObjectOutputStream oos = new ObjectOutputStream(client.getOutputStream()); String methodName = ois.readUTF();
Class[] parameterTypes= (Class[]) ois.readObject();//读取类型
Object[] args= (Object[]) ois.readObject();//读取Id
System.out.println("接收到ID为:"+args[0]);
UserServiceImpl service=new UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes);
User user = (User) method.invoke(service,args);
//使用ObjectOutputStream直接序列化写出对象
System.out.println("返回的对象为:"+user);
oos.writeObject(user);
oos.flush();
ois.close();
oos.close();
}
}

Stub

package com.airsky.demo.rpc.rpc05;

import com.airsky.demo.rpc.Common.IUserService;
import com.airsky.demo.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将对象数据序列化传输过去。
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String methodName = method.getName();
Class[] parametersTypes = method.getParameterTypes();
oos.writeUTF(methodName);
oos.writeObject(parametersTypes);
oos.writeObject(args);
oos.flush(); //使用ObjectOutputStream将序列化数据转换为对象。
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
User user = (User) ois.readObject();
oos.close();
ois.close();
s.close();
return user;
}
};
Object o= Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h);
System.out.println(o.getClass().getName());
System.out.println(o.getClass().getInterfaces()[0]);
return (IUserService)o; }
}

到了这里大家发现没有,我们的UserService可以随意添加方法,与此同时User还可以自由改变实现,我们的程序会变得更灵活,尤其是把Stub的内容对特别弱鸡级(我)的程序员屏蔽掉,告诉鸡你就调我的Stub方法就能进行远程调用了,里边怎么处理的你不用管。

RPC06

当然上个版本也不是很完美,我们还可以做进一步的加工。在上个版本中我们只能拿到一个UserService,那我们你不能通过getStub拿到任意类型的公开接口,可不可以?非常可以。我们都知道在Proxy的newProxyInstance方法中传入了接口的class对象,那么我们是不是可以在这个地方进行动态的传入,传入我们想要拿到的任意接口。

Stub

package com.airsky.demo.rpc.rpc06;

import com.airsky.demo.rpc.Common.IUserService;
import com.airsky.demo.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 {
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",8888); //使用ObjectOutputStream将对象数据序列化传输过去。
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String clazzName = clz.getName();
String methodName = method.getName();
Class[] parametersTypes = method.getParameterTypes();
oos.writeUTF(clazzName);
oos.writeUTF(methodName);
oos.writeObject(parametersTypes);
oos.writeObject(args);
oos.flush(); //使用ObjectOutputStream将序列化数据转换为对象。
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object o = ois.readObject();
oos.close();
ois.close();
s.close();
return o;
}
};
Object o= Proxy.newProxyInstance(clz.getClassLoader(),new Class[]{clz},h);
System.out.println(o.getClass().getName());
System.out.println(o.getClass().getInterfaces()[0]);
return o; }
}

可以看到我们给getStub新添加了一个class参数clz,而将这个参数传入了newProxyInstance,并且我们的getStub返回的是一个Object,这样你给我一个class类型我就可以返回给你一个实现了这个class类型的动态代理的那个类对象。在invoke中我们首先通过clz.getName()获取class的名字,因为我们需要调用的是任意接口,所以我们得先把接口名字写出去让服务端去进行调用。然后往外写的时候先写需要调用的接口名称,在写需要调用的接口方法,最后写参数类型和参数。我们再来看看Server端需要怎么改动。

Server

package com.airsky.demo.rpc.rpc06;

import com.airsky.demo.rpc.Common.ProductServiceImpl;
import com.airsky.demo.rpc.Common.User;
import com.airsky.demo.rpc.Common.UserService;
import com.airsky.demo.rpc.Common.UserServiceImpl; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket; public class Server {
//加上条件变量,不然s.close()不可达
private static boolean running = true;
public static void main(String[] args) throws Exception {
ServerSocket s=new ServerSocket(8888);
while (running){
System.out.println("接收连接中...");
Socket client=s.accept();
System.out.println("连接主机:" + client.getInetAddress());
send(client);
client.close();
}
s.close();
} private static void send(Socket client) throws Exception { ObjectInputStream ois= new ObjectInputStream(client.getInputStream()); ObjectOutputStream oos = new ObjectOutputStream(client.getOutputStream()); String clazzName = ois.readUTF();
String methodName = ois.readUTF();
Class[] parameterTypes= (Class[]) ois.readObject();//读取类型
Object[] args= (Object[]) ois.readObject();//读取Id
System.out.println("接收到ID为:"+args[0]);
//去服务注册表找到具体的类,如果使用spring甚至还可以直接根据配置注入bean然后根据bean查找。
Class clazz = ProductServiceImpl.class;
Method method = clazz.getMethod(methodName,parameterTypes);
Object o = method.invoke(clazz.newInstance(),args);
//使用ObjectOutputStream直接序列化写出对象
System.out.println("返回的对象为:"+o);
oos.writeObject(o);
oos.flush();
ois.close();
oos.close();
}
}

Server端无非就是在读的时候先读到我们的className,然后就去服务注册表找到具体的实现类,最后找对应的方法,最后传入参数进行调用就行了。当然这个过程完全可以使用Spring来注入来查找具体的实现类。我们通过新增一个ProductService接口来测试代码是否完善。

Product

package com.airsky.demo.rpc.Common;

import java.io.Serializable;

public class Product implements Serializable {
private static final long serialVersionUID = 1L;
int id;
String name;
int count; public Product(int id, String name, int count) {
this.id = id;
this.name = name;
this.count = count;
} public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getCount() {
return count;
} public void setCount(int count) {
this.count = count;
} @Override
public String toString() {
return "Product{" +
"id=" + id +
", name=" + name +
", count=" + count +
'}';
}
}

ProductService

package com.airsky.demo.rpc.Common;

public interface ProductService {
Product findProductByName(String name);
}

ProductServiceImpl

package com.airsky.demo.rpc.Common;

public class ProductServiceImpl implements ProductService {
@Override
public Product findProductByName(String name) {
return new Product(4,name,4);
}
}

Client

package com.airsky.demo.rpc.rpc06;

import com.airsky.demo.rpc.Common.Product;
import com.airsky.demo.rpc.Common.ProductService;
public class Client {
public static void main(String[] args) {
ProductService service= (ProductService) Stub.getStub(ProductService.class);//返回一个动态生成的对象
Product product=service.findProductByName("SuperMan");
System.out.println(product); }
}



可以看到成功的进行了远程方法调用。

现在我们来梳理回顾一下全过程,我在Client端要对Server端的服务做调用,Server端服务提供了很多个,简单点叫服务1,服务2...,调用服务的时候我非常不爽,因为每次都要写网络细节。这时我就生成了一个代理类,这个代理类用动态生成的方式来生成网络细节,你只要告诉我这个工具类,我要服务1的方法1,我传了一个参数1,工具类就会生成一个代理类,将我们的方法调用信息传给服务器,服务器找到对应的实现类,对应的方法。将执行完成的结果返回。 所谓的RPC:Remote Procudure Call,远程的、方法的、调用。帮你屏蔽了底层。这里面的实现有好多种,所以这只是一种通信的方式,我可以通过屏蔽底层的方式(Stub),就跟我调用本机的方法似的。现在只是一个最基础的RPC实现,这里面有大量的可以改进的地方,一步一步的改进你就可以自己写一个RPC框架了。

RPC08

此时我们在底层的实现是序列化,转换为二进制,因为所有网络传输的都是二进制。我们现在用的序列化是jdk自带的Serializable,这个方式是最土的,因为它只支持java语言,而且效率低,长度长。所以对于RPC序列化上的实现就有好多好多可以替代的内容。

了解一下RPC的序列化框架。

  • java.io.Serializable
  • Hessian
  • google protobuf
  • fackbook Thrift
  • kyro
  • fst
  • json序列化框架
  • jackson
  • google Gson
  • FastJson
  • xmlrpc(xstream)

    Json框架就是先转换为JSON字符串格式,通过JSON框架再转换为二进制,XML就是转换为XML格式。

    以上只是RPC框架的序列化实现部分,完整的RPC框架还要提供服务注册、服务发现、服务治理、服务监控、服务的负载均衡各种各样的东西。

    这里我们就来了解一下Hessian序列化框架,序列化就是把一个对象转换成字节数组,反序列化就是反之。我们在HelloHessian中实现了一个方法serialize,作用是将一个对象转换成字节数组。怎么转换的呢?还是使用ByteArrayOutputStream创建字节数组输出流对象然后捕获内存缓冲区的数据(Object),转换成字节数组,在外面我们将原来嵌套的ObjectOutputStream换成了Hessian2Output,然后将对象数据写入了字节数组输出流中转换成了字节数组。

    HelloHessian
package com.airsky.demo.rpc.rpc08_Hessian01;

import com.airsky.demo.rpc.Common.User;
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 HelloHessian {
public static void main(String[] args) throws IOException {
User a = new User(4,"AirSky");
byte[] bytes = serialize(a);
System.out.println(bytes.length);
User a1 = (User)deserialize(bytes);
System.out.println(a1);
} private static byte[] serialize(User a) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.writeObject(a);
output.flush();
byte[] bytes = baos.toByteArray();
baos.close();
output.close();
return bytes; }
private 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;
}
}

这时你可能会说,你在搞笑吗?我直接用JDK自带的序列化不就行了,干嘛搞的这么麻烦。别急,跟着我来看看Hessian和JDK的对比。

package com.airsky.demo.rpc.rpc08_Hessian01;

import com.airsky.demo.rpc.Common.User;
import com.caucho.hessian.io.Hessian2Output; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream; public class HessianVSJDK {
public static void main(String[] args) throws IOException {
User a = new User(1,"AirSky"); long b =System.currentTimeMillis();
byte[] HessianBytes = HessianSerialize(a);
long c =System.currentTimeMillis();
System.out.println("HessianLength:"+HessianBytes.length);
System.out.println("HessianString:"+new String(HessianBytes));
System.out.println("HessianTime:"+(c-b)); System.out.println("------------------------------------------------------------------"); long b1 =System.currentTimeMillis();
byte[] JdkBytes = JdkSerialize(a);
long c1 =System.currentTimeMillis();
System.out.println("JdkLength:"+JdkBytes.length);
System.out.println("JdkString:"+new String(JdkBytes));
System.out.println("JdkTime:"+(c1-b1));
}
private static byte[] HessianSerialize(Object a) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.writeObject(a);
output.flush();
byte[] bytes = baos.toByteArray();
baos.close();
output.close();
return bytes; }
private static byte[] JdkSerialize(Object a) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.flush();
byte[] bytes = baos.toByteArray();
baos.close();
oos.close();
return bytes;
}
}

额外

我们在HessianVSJDK中实现了一个使用Hessian和ObjectOutputStream序列化的方法,通过比较这两个方法序列化User对象a之后的长度和字符串内容可以看出Hessian去掉了原生序列化中没有必要传输的大量字符。但通过序列化时间也可以看出java自身所带的方法明显比hessian自带的序列化效率更高。这里也有实验数据https://my.oschina.net/caomuquan/blog/378416。RPC中必须有的就是序列化,所以在RPC的序列化中产生了一系列的序列化框架,这些框架有什么区别呢?区别就是有的快、有的慢、有的大、有的小、有的效率高、有的效率低、有的用纯文本、有的用xml。

说到这里大家是不是明白了几个概念了,第一、RPC的基本概念,第二、RPC的序列化框架。

RPC10

RPC除了有序列化的一面还有什么呢?作为RPC中的Server来说,现在用的传输数据的方式是最基础的TCP/IP,但是我们传输数据的协议有好多种是不是?我也可以用HTTP来传输我的字符串或者二进制,所以RPC的概念除了序列化之外还有RPC的网络协议,这个你可以自己选。可以选HTTP,也可以选底层的TCP/IP,可以选WebService,甚至可以选mail协议。协议无所谓,无非就是通过这些协议把我们序列化好的数据发出去。不同的框架可能就使用的不同的协议,需要自己去选,去定义才行。

整理自马士兵老师:《36行代码透彻解析RPC》

RPC框架从0到10的更多相关文章

  1. Spark2.1.0——内置RPC框架详解

    Spark2.1.0——内置RPC框架详解 在Spark中很多地方都涉及网络通信,比如Spark各个组件间的消息互通.用户文件与Jar包的上传.节点间的Shuffle过程.Block数据的复制与备份等 ...

  2. .NET 跨平台RPC框架DotNettyRPC Web后台快速开发框架(.NET Core) EasyWcf------无需配置,无需引用,动态绑定,轻松使用 C# .NET 0配置使用Wcf(半成品) C# .NET Socket 简单实用框架 C# .NET 0命令行安装Windows服务程序

    .NET 跨平台RPC框架DotNettyRPC   DotNettyRPC 1.简介 DotNettyRPC是一个基于DotNetty的跨平台RPC框架,支持.NET45以及.NET Standar ...

  3. 高并发架构系列:如何从0到1设计一个类Dubbo的RPC框架

    在过去持续分享的几十期阿里Java面试题中,几乎每次都会问到Dubbo相关问题,比如:“如何从0到1设计一个Dubbo的RPC框架”,这个问题主要考察以下几个方面: 你对RPC框架的底层原理掌握程度. ...

  4. 如何从0到1设计一个类Dubbo的RPC框架

    之前分享了如何从0到1设计一个MQ消息队列,今天谈谈"如何从0到1设计一个Dubbo的RPC框架",重点考验: 你对RPC框架的底层原理掌握程度. 以及考验你的整体RPC框架系统设 ...

  5. Google 高性能 RPC 框架 gRPC 1.0.0 发布(附精彩评论)

    gRPC是一个高性能.开源.通用的RPC框架,面向移动和HTTP/2设计,是由谷歌发布的首款基于Protocol Buffers的RPC框架. gRPC基于HTTP/2标准设计,带来诸如双向流.流控. ...

  6. c#---Socean.RPC框架实测[并发量13w][响应时间0.04ms]

    目录1.高性能RPC框架:Socean.RPC 前言 经过一段时间的优化,Socean.RPC的性能又提高了一些,不过这差不多是socketAPM模型的极限了.本框架仅仅2000多行代码,无第三方框架 ...

  7. 从0 开始手写一个 RPC 框架,轻松搞定!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 来源:juejin.im/post/5c4481a4f265da613438aec3 之前在 RPC框架底层到底什么原理得知 ...

  8. RPC框架实现 - 通信协议篇

    RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面.其对业务隐藏了底层通信过程(TCP/UDP.打包/解包.序列化/反序列化),使 ...

  9. RPC框架性能基本比较测试

    RPC框架:gRPC.Thrift.Wildfly.Dubbo 原文链接:http://www.open-open.com/lib/view/open1426302068107.html gRPC是G ...

随机推荐

  1. JAVA环境安装及其配置

    一.JAVA版本的选择 我使用的是JAVA8,所以这次方法是JAVA8的安装过程. 这里我给出其下载地址,可以自行下载. 链接: https://pan.baidu.com/s/1k2Xydi6FJ2 ...

  2. cglib、orika、spring等bean copy工具性能测试和原理分析

    简介 在实际项目中,考虑到不同的数据使用者,我们经常要处理 VO.DTO.Entity.DO 等对象的转换,如果手动编写 setter/getter 方法一个个赋值,将非常繁琐且难维护.通常情况下,这 ...

  3. SPFA算法优化

    前言 \(SPFA\) 通常在稀疏图中运行效率高于 \(Dijkstra\) ,但是也容易被卡. 普通的 \(SPFA\) 时间复杂度为 \(O(km)\) ,其中 \(k\) 是一条边松弛其端点点的 ...

  4. 【复习笔记】重习 AC 自动机

    发现已经忘了许多....于是复习一下 基础要点概况 AC 自动机基于 Trie 树 的结构,即构建 AC 自动机前需要先建 Trie. 一个状态中除了转移 \(\delta\) 之外还有失配指针 \( ...

  5. 新挖个坑,准备学习一下databricks的spark博客

    挖坑 https://databricks.com/blog 一.spark3.0特性(Introducing Apache Spark 3.0) 1.通过通过自适应查询执行,动态分区修剪和其他优化使 ...

  6. window+nginx+php

    今天在Windows上配置了下nginx,看了不少其他大牛们记录的博客,自己也操作了一番,记录一下备忘. nginx download: http://nginx.org/en/download.ht ...

  7. python(iJmeter-master)接口测试程序部署实践

    记录学习性能测试过程遇到的问题 环境 安装环境如下: Windows 10 1803 VMWare Workstation 15 Pro Centos Linux release 7.9.2009(c ...

  8. AOP 有几种实现方式?

    1. 回顾 AOP 是什么? 维基百科解释如下: 面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计.剖面导向程序设计)是计算机科学中的一种程 ...

  9. mysql单机多实例配置

    Windows上配置多个mysql实例,主要改下配置文件即可,mysql目录如下: my2中主要改两个配置内容 datadir = D:/Program Files/Mysql/mysql-5.7.2 ...

  10. babel 与 ast

    什么是 babel Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中. 什 ...