什么是RPC?
RPC的全称是远程过程调用(Remote Procedure Call),由于数据不在同一个内存空间,而是在其他的服务器上,所以需要通过网络来表达语义和传达数据,通过它可以调用其他服务器上提供的服务。运用最多的地方就是在微服务里面,它能够让调用方像调用本地方法一样来调用远程服务,从而提高开发效率。
一款RPC产品应该具有哪些功能:
一、调用代理类
代理类就是一个程序代码里的一个class,这个类提供远程调用的功能,调用方只需要在代码里new一个代理对象,通过这个代理对象提供的方法来调用远程服务获取数据,使用起来非常的方便。如下所示,userServiceProxy是用户服务的代理类,调用类的getUserInfo方法获得用户数据。
//调用用户服务获得用户信息,通过代理类
UserServiceProxy userServiceProxy = new UserServiceProxy();
User user = userServiceProxy.getUserInfo(1L);
二、组包和解包
调用远程服务必然要走网络,走网络就要涉及到数据在网络上的传输,对于网络传输我们都是基于socket编程的,那么在socket的接收缓冲区里数据是一个一个的字节组成的字节流,服务接受方收到多个客户端传来的数据时,多个客户端的数据都是连在一起的字节流,如果不按照一定的规范组织就无法区分一个个的请求数据。为了区分一个个的请求数据需要定义一个协议来组织字节流数据,协议的数据由一个一个的数据包组成,一个数据包就代表一次客户端的请求数据,在这里列举了一个协议包的示例如下图所示:
数据包分为包头(Header)和包体(Body)2大部分,包头的长度是固定的,包体的长度是边长的。包头用来识别整个包,而包体用来存放数据,由于包的数据有多有少,所以包体的长度是边长的。
包头(Header)里包含有版本号(Version)、命令(Command)、魔法号(MagicNumber)、包体长度(BoydLength)。
版本号:当协议有升级的时候,可以通过版本号来做兼容逻辑。
命令:表示调用服务提供方的哪个服务,比如Command等于1表示获取用户服务的用户信息。
魔法号:用户判断接受的包是合法的包,而不是其他伪造的包。
包体长度:表示body的长度。
以上介绍了RPC的数据包结构,我们的RPC数据就得按照协议来组织字节流,所以就要把RPC对象组装成一个字节流的数据包(组包)和把字节流的数据包解包成RPC对象(解包),如下代码所示:
package rpc.protocol;
public class RpcProtocol
{
private int version;
private int command;
private int magicNumber;
private int bodyLength;
private byte[] body;
final public static int HEAD_LENGTH = 16;
public RpcProtocol setVersion(int version) {
this.version = version;
return this;
}
public int getVersion() {
return version;
}
public RpcProtocol setCommand(int command) {
this.command = command;
return this;
}
public int getCommand() {
return command;
}
public RpcProtocol setMagicNumber(int magicNumber) {
this.magicNumber = magicNumber;
return this;
}
public int getMagicNumber() {
return magicNumber;
}
public RpcProtocol setBodyLength(int bodyLength) {
this.bodyLength = bodyLength;
return this;
}
public int getBodyLength() {
return bodyLength;
}
public RpcProtocol setBody(byte[] body) {
this.body = body;
return this;
}
public byte[] getBody() {
return body;
}
//把RPC对象组装成一个字节流的数据包
public byte[] generateByteArray() {
byte[] data = new byte[bodyLength+HEAD_LENGTH];
int index = 0;
System.arraycopy(ByteConverter.intToBytes(version), 0, data, index, Integer.BYTES);
index += Integer.BYTES;
System.arraycopy(ByteConverter.intToBytes(command), 0, data, index, Integer.BYTES);
index += Integer.BYTES;
System.arraycopy(ByteConverter.intToBytes(magicNumber), 0, data, index, Integer.BYTES);
index += Integer.BYTES;
System.arraycopy(ByteConverter.intToBytes(bodyLength), 0, data, index, Integer.BYTES);
index += Integer.BYTES;
System.arraycopy(body, 0, data, index, body.length);
return data;
}
//把字节流的数据包解包成RPC对象
public RpcProtocol byteArrayToRpcHeader(byte[] data) {
if (data == null || data.length==0) {
return null;
}
int index = 0;
this.setVersion(ByteConverter.bytesToInt(data, index));
index += Integer.BYTES;
this.setCommand(ByteConverter.bytesToInt(data,index));
index += Integer.BYTES;
this.setMagicNumber(ByteConverter.bytesToInt(data,index));
index += Integer.BYTES;
this.setBodyLength(ByteConverter.bytesToInt(data,index));
index += Integer.BYTES;
this.body = new byte[bodyLength];
System.arraycopy(data, index, this.body, 0, bodyLength);
return this;
}
}
三、序列化和反序列化
除了要把数据按照数据包的格式组织外,包里面的每一个字段都要转化成字节流的形式,因为我们传送和接收数据都是按照字节的方式来处理的。比如数据包里的版本号(Version)字段,就得由int类型转化成为4字节的byte数组,Command、MagicNumber、BodyLength和Body都要转化成byte数组,这样才能在网络上传输,这就需要一个序列化的功能。有了序列化就得有反序列化把序列化的数据解析出来。
序列化:将int类型的数据序列化为字节数组,如下所示:
public static byte[] intToBytes(int n) {
byte[] buf = new byte[4];
buf[0] = (byte) n;
buf[1] = (byte) (n>>8);
buf[2] = (byte) (n>>16);
buf[3] = (byte) (n>>24);
return buf;
}
反序列化:将字节数组反序列化为int数,如下所示:
public static int bytesToInt(byte[] buf, int offset) {
return buf[offset] & 0xff
| (buf[offset+1] << 8) & 0xff00
| (buf[offset+2] << 16) & 0xff0000
| (buf[offset+3] << 24) & 0xff000000;
}