RPC简介
RPC,全称为Remote Procedure Call,即远程过程调用,它是一个计算机通信协议。它允许像调用本地服务一样调用远程服务。它可以有不同的实现方式。如RMI(远程方法调用)、Hessian、Http invoker等。另外,RPC是与语言无关的。

如上图所示,假设Computer1在调用sayHi()方法,对于Computer1而言调用sayHi()方法就像调用本地方法一样,调用 –>返回。但从后续调用可以看出Computer1调用的是Computer2中的sayHi()方法,RPC屏蔽了底层的实现细节,让调用者无需关注网络通信,数据传输等细节。
RPC框架的使用
在实现简单的PRC框架前,首先我们要了解如何使用RPC。这里我们以go语言为例,使用gRPC框架,protocol buffer作为IDL(接口描述语言)为例:
定义proto文件
proto是当今使用最广泛的IDL,本例中客户端在调用服务器的方法的时候,用proto文件确定两者的方法名、参数、返回值等。
下面我们编写hello.proto文件
syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本
package pb; // 包名
// 定义服务
service Greeter {
// SayHello 方法
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 请求消息
message HelloRequest {
string name = 1;
}
// 响应消息
message HelloResponse {
string reply = 1;
}
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto
通过.proto文件,使用gRPC提供的工具生成我们需要的库代码,然后用Go编写客户端和服务端程序。
编写服务端代码
服务端收到客户端请求,拿到需要调用的函数(SayHello)+参数(HelloRequest),调用本地的函数实现,将返回值(HelloResponse)返回给客户端
package main
import (
"context"
"fmt"
"hello_server/pb"
"net"
"google.golang.org/grpc"
)
// ----------------------- 实现pb中的接口 Start ------------------------
type server struct {
pb.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}
// ----------------------- 实现pb中的接口 End ------------------------
func main() {
// 1. 监听本地的8972端口
lis, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
// 2. 创建grpc服务器并注册服务(注册服务即将pb接口的方法实现并register)
rpcServer := grpc.NewServer()
pb.RegisterGreeterServer(rpcServer, &server{})
// 3. 启动grpc服务器
err = rpcServer.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
编写客户端代码
客户端调用函数(SayHello),传入参数(HelloRequest),获得返回值(HelloResponse)
package main
import (
"context"
"flag"
"log"
"time"
"hello_client/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// ----------------------- 一会儿需要用到的变量 Start ------------------------
const (
defaultName = "world"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
name = flag.String("name", defaultName, "Name to greet")
)
// ----------------------- 一会儿需要用到的变量 End ------------------------
func main() {
flag.Parse()
// 连接到server端,此处禁用安全传输
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// ------------------------- rpc调用 start -----------------------------------
// 创建grpcClient
rpcClient := pb.NewGreeterClient(conn)
// 设置上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 执行rpc调用
r, err := rpcClient.SayHello(ctx, &pb.HelloRequest{Name: *name})
// 错误处理
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetReply())
// ------------------------- rpc调用 end -----------------------------------
}
总结:
proto文件(IDL)定义方法、参数、返回值,使用工具直接生成可以使用的库。
Server:创建grpcServer对象,注册服务(即自己定义的方法,针对proto文件里声明的方法的实现),启动服务器。
Client:创建grpcClient对象(给出服务ip+port地址),用grpcClient对象调用服务(也就是调用proto文件里面声明的方法),然后打印返回的结果。
简单的RPC框架实现
因此,我们要用Java实现一个最简单的RPC框架,按照上面的说法,应该要实现的是:
框架:
- 接口文件,定义方法名、参数、返回值,供客户端和服务端使用
- RPC框架,让客户端和服务端可以使用框架,达到本地调用,远端执行的目的
测试:
- 写一个TestServer可执行程序,注册方法的实现后,使用rpcServer进行监听并处理rpc调用
- 写一个TestClient可执行程序,使用rpcClient进行发起rpc调用,并打印rpc调用结果

总体设计思路如下:
客户端获得rpcService对象,使用rpcService对象执行hello方法,那么rpcService底层实现就发送一条RpcRequest协议(对比HTTP协议)把:要执行的接口名+方法名+参数类型+具体参数序列化后,放进RpcRequest协议的body字节流中,然后给RpcRequest加上header,发给服务端,服务端解析出Rpc协议的body(对比HTTP协议解析body)中的接口名、方法名等,直接调用本地的接口的实现,然后将返回值包装成一条RpcResponse消息,发送给客户端即可,rpcService底层将该response消息解析,从body中拿到(也是对比HTTP解析body)返回值,然后返回给客户端。
第一步:编写IDL文件
按照gRPC的方式,编写接口HelloService,以及里面的消息体HelloRequest和HelloResponse,客户端和服务器都将使用这同一套接口。
public interface HelloService {
HelloResponse hello(HelloRequest request);
HelloResponse hi(HelloRequest request);
}
public class HelloRequest implements Serializable {
private String name;
}
public class HelloResponse implements Serializable {
private String msg;
}
第二步:编写RPC协议
RpcRequest和RpcResponse都是RPC协议,RPC协议包括header和body两部分,header我们用String表示,body我们用序列化后的byte[]流表示,这里的字节流的序列化的方式直接用Java的序列化方式。
public class RpcRequest implements Serializable {
// 协议头部分
private String header;
// 协议体部分
private byte[] body;
}
public class RpcResponse implements Serializable {
// 协议头部分
private String header;
// 协议体部分
private byte[] body;
}
body中被序列化的内容,是codec编解码层的工作,在源代码中我们放在了codec包中,RPCReuqest要调用一个方法,需要知道接口名、方法名、参数、参数类型,因此把这些东西放进RpcRequestBody中即可,把它序列化后反正该RpcRequest的body字节流中;同理,RPCResponse 的body中,只需要一个被序列化后的Java Object即可。
public class RpcRequestBody implements Serializable {
private String interfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
}
public class RpcRequestBody implements Serializable {
private String interfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
}
第四步:客户端实现(动态代理)
客户端方面,客户端本地只有IDL.Hello中的内容,没有方法的具体实现,也就是说要调用一个没有实现的接口,显然,我们使用Java反射的动态代理特性,实例化一个接口,将调用接口方法“代理”给InvocationHandler中的invoke来执行,在Invoke中获取到接口名、方法名等包装成Rpc协议,发送给服务端,然后等待服务端返回。
public class RpcClientProxy implements InvocationHandler {
@SuppressWarnings("unchecked")
public <T> T getService(Class<T> clazz) {
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(),
new Class<?>[]{clazz},
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1、将调用所需信息编码成bytes[],即有了调用编码【codec层】
RpcRequestBody rpcRequestBody = RpcRequestBody.builder()
.interfaceName(method.getDeclaringClass().getName())
.methodName(method.getName())
.paramTypes(method.getParameterTypes())
.parameters(args)
.build();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(rpcRequestBody);
byte[] bytes = baos.toByteArray();
// 2、创建RPC协议,将Header、Body的内容设置好(Body中存放调用编码)【protocol层】
RpcRequest rpcRequest = RpcRequest.builder()
.header("version=1")
.body(bytes)
.build();
// 3、发送RpcRequest,获得RpcResponse
RpcClientTransfer rpcClient = new RpcClientTransfer();
RpcResponse rpcResponse = rpcClient.sendRequest(rpcRequest);
// 4、解析RpcResponse,也就是在解析rpc协议【protocol层】
String header = rpcResponse.getHeader();
byte[] body = rpcResponse.getBody();
if (header.equals("version=1")) {
// 将RpcResponse的body中的返回编码,解码成我们需要的对象Object并返回【codec层】
ByteArrayInputStream bais = new ByteArrayInputStream(body);
ObjectInputStream ois = new ObjectInputStream(bais);
RpcResponseBody rpcResponseBody = (RpcResponseBody) ois.readObject();
Object retObject = rpcResponseBody.getRetObject();
return retObject;
}
return null;
}
}
// 传入protocol层的RpcRequest,输出protocol层的RpcResponse
public class RpcClientTransfer {
public RpcResponse sendRequest(RpcRequest rpcRequest) {
try (Socket socket = new Socket("localhost", 9000)) { //这里我们直接协调好端口
// 发送【transfer层】
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
objectOutputStream.writeObject(rpcRequest);
objectOutputStream.flush();
RpcResponse rpcResponse = (RpcResponse) objectInputStream.readObject();
return rpcResponse;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
第五步:服务端实现(反射调用)
服务端方面,本地需要实现接口的方法,然后在启动监听网络之前注册所有的接口,当消息到来的时候,根据RpcRequestBody中的接口名拿到接口对象,然后用反射的方式调用即可,将调用结果包装成RpcResponse,发送给客户端。
public class RpcServer {
private final ExecutorService threadPool;
// interfaceName -> interfaceImplementation object
private final HashMap<String, Object> registeredService;
public RpcServer() {
int corePoolSize = 5;
int maximumPoolSize = 50;
long keepAliveTime = 60;
BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
this.threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workingQueue, threadFactory);
this.registeredService = new HashMap<String, Object>();
}
// 参数service就是interface的implementation object
public void register(Object service) {
registeredService.put(service.getClass().getInterfaces()[0].getName(), service);
}
public void serve(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)){
System.out.println("server starting...");
Socket handleSocket;
while ((handleSocket = serverSocket.accept()) != null) {
System.out.println("client connected, ip:" + handleSocket.getInetAddress());
threadPool.execute(new RpcServerWorker(handleSocket, registeredService));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class RpcServerWorker implements Runnable{
private Socket socket;
private HashMap<String, Object> registeredService;
public RpcServerWorker(Socket socket, HashMap<String, Object> registeredService) {
this.socket = socket;
this.registeredService = registeredService;
}
@Override
public void run() {
try {
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
// 1、Transfer层获取到RpcRequest消息【transfer层】
RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
// 2、解析版本号,并判断【protocol层】
if (rpcRequest.getHeader().equals("version=1")) {
// 3、将rpcRequest中的body部分解码出来变成RpcRequestBody【codec层】
byte[] body = rpcRequest.getBody();
ByteArrayInputStream bais = new ByteArrayInputStream(body);
ObjectInputStream ois = new ObjectInputStream(bais);
RpcRequestBody rpcRequestBody = (RpcRequestBody) ois.readObject();
// 调用服务
Object service = registeredService.get(rpcRequestBody.getInterfaceName());
Method method = service.getClass().getMethod(rpcRequestBody.getMethodName(), rpcRequestBody.getParamTypes());
Object returnObject = method.invoke(service, rpcRequestBody.getParameters());
// 1、将returnObject编码成bytes[]即变成了返回编码【codec层】
RpcResponseBody rpcResponseBody = RpcResponseBody.builder()
.retObject(returnObject)
.build();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(rpcResponseBody);
byte[] bytes = baos.toByteArray();
// 2、将返回编码作为body,加上header,生成RpcResponse协议【protocol层】
RpcResponse rpcResponse = RpcResponse.builder()
.header("version=1")
.body(bytes)
.build();
// 3、发送【transfer层】
objectOutputStream.writeObject(rpcResponse);
objectOutputStream.flush();
}
} catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
测试
首先我们在本地实现HelloService方法:
public class HelloServiceImpl implements HelloService {
// 实现接口中的hello方法
@Override
public HelloResponse hello(HelloRequest request) {
String name = request.getName();
String retMsg = "hello: " + name;
HelloResponse response = new HelloResponse(retMsg);
// 返回响应对象
return response;
}
// 实现接口中的hi方法
@Override
public HelloResponse hi(HelloRequest request) {
String name = request.getName();
String retMsg = "hi: " + name;
HelloResponse response = new HelloResponse(retMsg);
// 返回响应对象
return response;
}
}
接下来服务端创建RPC服务器,注册helloService,并开始监听端口:
public class TestServer {
public static void main(String[] args) {
RpcServer rpcServer = new RpcServer(); // 真正的rpc server
HelloService helloService = new HelloServiceImpl(); // 包含需要处理的方法的对象
rpcServer.register(helloService); // 向rpc server注册对象里面的所有方法
rpcServer.serve(9000);
}
}
下面是客户端的工作,客户端通过动态代理的方式拿到helloService,并调用helloService方法得到返回的helloResponse,注意本地并未实现该方法,采用的是远程过程调用。
public class TestClient {
public static void main(String[] args) {
// 获取RpcService
RpcClientProxy proxy = new RpcClientProxy();
HelloService helloService = proxy.getService(HelloService.class);
// 构造出请求对象HelloRequest
HelloRequest helloRequest = new HelloRequest("Young");
// rpc调用并返回结果对象HelloResponse
HelloResponse helloResponse = helloService.hello(helloRequest);
// 从HelloResponse中获取msg
String helloMsg = helloResponse.getMsg();
// 打印msg
System.out.println(helloMsg);
// 调用hi方法
HelloResponse hiResponse = helloService.hi(helloRequest);
String hiMsg = hiResponse.getMsg();
System.out.println(hiMsg);
}
}
依次启动TestServer、TestClient,结果如下图所示:


总结
使用多层模型设计和实现简单的RPC框架,使我对RPC(远程过程调用)的原理有了更深刻的理解,感谢孟宁老师提供的实践机会。在网络程序设计课程中,非科班的我对网络技术如websocket、epoll、RPC有了更加深刻的了解,也掌握了GDB调试的基本知识。
本次实现的RPC框架较为简单,完整的RPC框架还应提供更多功能,比如客户端如何找到能够调用的服务端的ip和端口,在本框架中我们直接在代码中协调好了,然而实际工程中会有更多的问题,我也会在今后的学习中不断完善该框架的功能。