{"id":26151364,"url":"https://github.com/tomstillcoding/simple-rpc","last_synced_at":"2025-10-12T17:44:54.289Z","repository":{"id":40608555,"uuid":"498049766","full_name":"tomstillcoding/Simple-RPC","owner":"tomstillcoding","description":"☕️ 用Java手撸一个简单的RPC框架｜一个写C++的在看完gRPC后用Java手撸了个简单RPC框架","archived":false,"fork":false,"pushed_at":"2022-05-31T04:33:16.000Z","size":1951,"stargazers_count":57,"open_issues_count":0,"forks_count":25,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-14T05:13:07.021Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tomstillcoding.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-30T18:08:10.000Z","updated_at":"2025-03-29T13:01:14.000Z","dependencies_parsed_at":"2022-07-14T04:00:33.715Z","dependency_job_id":null,"html_url":"https://github.com/tomstillcoding/Simple-RPC","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tomstillcoding/Simple-RPC","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomstillcoding%2FSimple-RPC","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomstillcoding%2FSimple-RPC/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomstillcoding%2FSimple-RPC/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomstillcoding%2FSimple-RPC/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tomstillcoding","download_url":"https://codeload.github.com/tomstillcoding/Simple-RPC/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tomstillcoding%2FSimple-RPC/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264545157,"owners_count":23625403,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-11T06:33:43.545Z","updated_at":"2025-10-12T17:44:49.258Z","avatar_url":"https://github.com/tomstillcoding.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple-RPC\n用Java手撸一个简单的RPC框架｜一个写C++的在看完gRPC后用Java手撸了个简单RPC框架\n# 使用方式\n用IDEA打开后配置Maven，右键点击pom.xml，重新加载项目（Reload Project）\n然后直接运行服务器程序和客户端程序\n\n带你手撸简单RPC框架｜一个写C++的在看完gRPC后用Java快速手撸了个简单RPC框架\n\n# 目录\n\n今天的视频分三部分：\n\n1、介绍RPC是什么\n\n2、介绍谷歌gRPC的Go语言使用\n\n3、用Java手撸一个最简单的RPC框架\n\n今天聊到的所有东西都会开源，在看完视频后，你可以照着文档，以及看看源代码，就能明白RPC的核心内容，也可以在这个简单RPC框架代码上补充各种组件，最后写到自己的简历上，最重要的目的是，撸一套RPC框架，能让你掌握RPC的核心功能实现。\n\n（等录完视频，我就去剪头发 做核酸 看MSI了）\n\n# 介绍RPC\n\n\u003cimg src=\"./assets/Screen Shot 2022-05-29 at 14.24.21-3816076.png\" alt=\"Screen Shot 2022-05-29 at 14.24.21\" style=\"zoom:50%;\" /\u003e\n\n# gRPC使用\n\ngrpc，就像是Dubbo一样，都是RPC框架，可能你听得云里雾里的，先看看具体怎么用吧\n\n定义hello.proto文件：\n\nproto是啥，IDL，IDL是什么，接口描述语言（Interface description language，缩写IDL），什么意思？不重要，重要的是，客户端在调用服务器的方法的时候，用IDL确定了两者的方法名、参数、返回值.\n\n下面我们编写hello.proto文件\n\n```protobuf\nsyntax = \"proto3\"; // 版本声明，使用Protocol Buffers v3版本\npackage pb; // 包名\n\n// 定义服务\nservice Greeter {\n    // SayHello 方法\n    rpc SayHello (HelloRequest) returns (HelloResponse) {}\n}\n\n// 请求消息\nmessage HelloRequest {\n    string name = 1;\n}\n\n// 响应消息\nmessage HelloResponse {\n    string reply = 1;\n}\n```\n\n```bash\nprotoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto\n```\n\n通过.proto文件，用grpc的工具，生成我们需要的库代码，然后用Go编写客户端和服务端程序。\n\n不会Go语言？无所谓，都是面向对象语言，调用方法=方法名+参数+返回值，看得懂就够了，看不懂，花一分钟查下语法就看懂了\n\n\n\n服务端代码：收到客户端请求，拿到需要调用的函数（SayHello）+参数（HelloRequest），调用本地的函数实现，将返回值（HelloResponse）返回给客户端\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"hello_server/pb\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n)\n\n// ----------------------- 实现pb中的接口 Start ------------------------\ntype server struct {\n\tpb.UnimplementedGreeterServer\n}\nfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {\n\treturn \u0026pb.HelloResponse{Reply: \"Hello \" + in.Name}, nil\n}\n// ----------------------- 实现pb中的接口 End ------------------------\n\nfunc main() {\n\t// 1. 监听本地的8972端口\n\tlis, err := net.Listen(\"tcp\", \":8972\")\n\tif err != nil {\n\t\tfmt.Printf(\"failed to listen: %v\", err)\n\t\treturn\n\t}\n  \n  // 2. 创建grpc服务器并注册服务（注册服务就是说，将pb接口的方法实现并register）\n\trpcServer := grpc.NewServer()\n\tpb.RegisterGreeterServer(rpcServer, \u0026server{})\n\t\n  // 3. 启动grpc服务器\n\terr = rpcServer.Serve(lis)\n\tif err != nil {\n\t\tfmt.Printf(\"failed to serve: %v\", err)\n\t\treturn\n\t}\n}\n```\n\n客户端代码：调用函数（SayHello），传入参数（HelloRequest），获得返回值（HelloResponse）\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"log\"\n\t\"time\"\n\n\t\"hello_client/pb\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\n// ----------------------- 一会儿需要用到的变量 Start ------------------------\nconst (\n\tdefaultName = \"world\"\n)\n\nvar (\n\taddr = flag.String(\"addr\", \"127.0.0.1:8972\", \"the address to connect to\")\n\tname = flag.String(\"name\", defaultName, \"Name to greet\")\n)\n// ----------------------- 一会儿需要用到的变量 End ------------------------\n\nfunc main() {\n\tflag.Parse()\n\t// 连接到server端，此处禁用安全传输\n\tconn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\tlog.Fatalf(\"did not connect: %v\", err)\n\t}\n\tdefer conn.Close()\n  \n  // ------------------------- rpc调用 start -----------------------------------\n  // 创建grpcClient\n\trpcClient := pb.NewGreeterClient(conn)\n\n\t// 设置上下文（没事不用管）\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n  \n  // 执行rpc调用\n\tr, err := rpcClient.SayHello(ctx, \u0026pb.HelloRequest{Name: *name})\n  \n  // 错误处理\n\tif err != nil {\n\t\tlog.Fatalf(\"could not greet: %v\", err)\n\t}\n\tlog.Printf(\"Greeting: %s\", r.GetReply())\n  // ------------------------- rpc调用 end -----------------------------------\n}\n```\n\n\n\n总结：\n\nproto文件（IDL）定义方法、参数、返回值，使用工具直接生成go可以用的库。\n\nServer：创建**grpcServer对象**，注册服务（也就是自己定义的方法，针对proto文件方法的实现），**启动服务器**。\n\nClient：创建**grpcClient对象（给出服务ip+port地址）**，用grpcClient对象调用服务（也就是调用proto文件里面声明的方法），然后打印返回的结果。\n\n\n\n# 手撸Java RPC框架\n\n因此，我们要用Java手撸一个最简单的RPC，按照上面的说法，应该要实现的是：\n\n框架：\n\n1. 接口文件，定义了方法名、参数、返回值，供客户端和服务端使用\n2. RPC框架，让客户端和服务端可以使用框架，达到**本地调用，远端执行**的目的\n\n测试：\n\n1. 写一个TestServer可执行程序，注册方法的实现后，使用rpcServer进行监听并处理rpc调用\n2. 写一个TestClient可执行程序，使用rpcClient进行发起rpc调用，并打印rpc调用结果\n\n\n\n\u003cimg src=\"./assets/Screen Shot 2022-05-29 at 14.24.39.png\" alt=\"Screen Shot 2022-05-29 at 14.24.21\" style=\"zoom:50%;\" /\u003e\n\n### 第一步：写IDL文件\n\nIDL.Hello的内容直接编写完毕。\n\n按照grpc的方式，编写接口HelloService，以及里面的消息体HelloRequest和HelloResponse，客户端和服务器都使用这同一套接口\n\n### 第二步：编写RPC协议\n\nRpcRequest和RpcResponse都是RPC协议，RPC协议包括header和body两部分，header我们用String表示，body我们用序列化后的byte[]流表示，这里的字节流的序列化的方式可以是Java的序列化方式，可以换成JSON序列化方式，也可以用Thrift、PB序列化方式，为了简单，我们这次直接用Java的序列化方式。\n\n然后body中被序列化的内容，因为是codec层的工作，放在了codec包中，RPCReuqest要调用一个方法，需要知道接口名、方法名、参数、参数类型，因此把这些东西放进RpcRequestBody中即可，后面把它序列化后房价RpcRequest的body字节流中；同理，RPCResponse 的body中，只需要一个被序列化后的Java Object即可。\n\n### 第三步：分析\n\n我们的rpcClient要执行一个函数hello，传入参数HelloRequest，然后返回HelloResponse。\n\n整个流程就是：客户端获得rpcService对象，使用rpcService对象执行hello方法，那么rpcService底层实现就发送一条RpcRequest协议（对比HTTP协议）把：要执行的接口名+方法名+参数类型+具体参数序列化后，放进RpcRequest协议的body字节流中，然后给RpcRequest加上header，发给服务端，服务端解析出Rpc协议的body（对比HTTP协议解析body）中的接口名、方法名等，直接调用本地的接口的实现，然后将返回值包装成一条RpcResponse消息，发送给客户端即可，rpcService底层将该response消息解析，从body中拿到（也是对比HTTP解析body）返回值，然后返回给客户端。\n\n### 第四步：客户端实现（动态代理）\n\n客户端方面，客户端本地只有IDL.Hello中的内容，没有方法的具体实现，也就是说要调用一个没有实现的接口，显然，我们使用Java反射的动态代理特性，实例化一个接口，将调用接口方法“代理”给InvocationHandler中的invoke来执行，在Invoke中获取到接口名、方法名等包装成Rpc协议，发送给服务端，然后等待服务端返回。\n\n### 第五步：服务端实现（反射调用）\n\n服务端方面，本地需要实现接口的方法，然后在启动监听网络之前注册所有的接口，当消息到来的时候，根据RpcRequestBody中的接口名拿到接口对象，然后用反射的方式调用即可，将调用结果包装成RpcResponse，发送给客户端。\n\n### 第六步：测试\n\n编写测试用例\n\n一共两个接口，HelloService和PingService，HelloService有两个方法：hello和hi，PingService有一个方法：ping\n\n### 总结\n\n事实上一个完整的RPC框架不仅包含上面的内容，还要提供很多功能，比如说\n\n服务发现：客户端怎么找到能够调用rpc的服务器的ip和端口？\n\n服务治理：整个rpc的运行怎么可靠，比如说客户端请求太多，一台服务器不顶用，IO打满了，再加一台服务器？\n\n服务注册：服务器怎么才能把自己能够handle的接口告诉客户端，不然自己都不能处理，客户端调用接口，调了也是失败\n\n编解码：比如上面body中我们是直接用的Java序列化，那要是跨平台怎么办，客户端用Java写，服务端用Go写，Go又不能处理Java序列化，对吧，而且编解码性能、编码后的字节数量也是很重要的一些东西\n\nRPC协议：协议的字段有很多的，协议版本、传输方式、序列化方式、连接个数等等\n\n\n\n所以，今天讲的这个东西不过是一个RPC的最简化Demo，帮大家理解rpc的原理，按照多层模型去设计与实现，后面往这个架子上面搭东西，或者去学习真正的RPC框架是比较有帮助的，祝大家周末快乐！\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomstillcoding%2Fsimple-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftomstillcoding%2Fsimple-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftomstillcoding%2Fsimple-rpc/lists"}