使用Java实现简单的RPC框架

本文详细介绍了RPC的概念、工作原理,以Go语言的gRPC和protobuf为例,展示了如何定义和使用RPC框架,并通过Java实现了一个简单的RPC框架,包括接口定义、协议设计、客户端和服务端的交互过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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框架,按照上面的说法,应该要实现的是:

框架:

  1. 接口文件,定义方法名、参数、返回值,供客户端和服务端使用
  2. RPC框架,让客户端和服务端可以使用框架,达到本地调用,远端执行的目的

测试:

  1. 写一个TestServer可执行程序,注册方法的实现后,使用rpcServer进行监听并处理rpc调用
  2. 写一个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,结果如下图所示:

image-20231230222732247 image-20231230221511683

总结

使用多层模型设计和实现简单的RPC框架,使我对RPC(远程过程调用)的原理有了更深刻的理解,感谢孟宁老师提供的实践机会。在网络程序设计课程中,非科班的我对网络技术如websocket、epoll、RPC有了更加深刻的了解,也掌握了GDB调试的基本知识。

本次实现的RPC框架较为简单,完整的RPC框架还应提供更多功能,比如客户端如何找到能够调用的服务端的ip和端口,在本框架中我们直接在代码中协调好了,然而实际工程中会有更多的问题,我也会在今后的学习中不断完善该框架的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值