
纲要RPC的核心概念远程过程调用定义跨机器的方法/函数调用服务拆分后面临的通信挑战本地调用与远程调用的差异Stub存根在序列化与反序列化中的角色RPC通信的关键问题服务寻址IP 端口或域名目标方法标识服务名.方法名数据传输格式请求与响应结构体设计方法名、参数、请求ID序列化协议JSON、XML、二进制传输协议TCP、UDP、HTTPRPC与RESTful的对比定义差异RPC 是一种通信思想RESTful 是一种架构风格目标差异RPC 面向函数/方法RESTful 面向资源调用方式RPC 追求本地式调用体验系统耦合度RPC 通常用于同一系统内部RESTful 适用于独立系统间协议承载RPC 可实现 RESTful 规范但核心关注点不同net/rpc实践项目目录结构服务端核心步骤定义服务结构体与方法方法签名要求定义请求、响应结构体注册服务到rpc server使用 TCP 监听并接受连接并发处理客户端请求客户端核心步骤拷贝服务端定义的请求/响应结构体通过net.Dial建立 TCP 连接创建rpc client调用远程方法指定服务名.方法名错误处理与连接关闭方法签名规则与错误处理RPC 的核心概念在单体服务架构中调用功能只需实例化对象并直接调用方法即可完成业务。然而当系统按照领域边界拆分为多个独立服务后订单服务与用户服务可能部署在不同的物理机或容器中传统的内存级调用就变得不可行。我们需要一种机制让跨网络的服务调用像调用本地函数一样简洁自然这就是RPCRemote Procedure Call远程过程调用。RPC 允许一个程序请求网络上另一个程序中的某个功能或方法而无需了解底层网络细节。换句话说它把“远程调用”封装成类似本地函数调用的形式。例如用户服务在自己的进程中创建一个RPC Server对外暴露诸如GetUser之类的方法订单服务则通过RPC Client连接到该 Server像调用本地函数一样调用GetUser底层网络传输、序列化等细节全部被屏蔽。为了实现这种透明的远程调用RPC 框架需要在客户端和服务端两侧都引入Stub存根角色。客户端的 Stub 负责将调用请求方法名、参数等序列化为网络传输数据并发送给服务端服务端的 Stub 则负责反序列化请求找到对应的服务方法并执行最后将结果序列化后返回给客户端。这个“序列化→发送→接收→反序列化→执行→返回”的闭环构成了 RPC 通信的基本流程。构建 RPC 通信需要解决哪些问题要设计一个完整的 RPC 通信机制必须逐层回答四个核心问题1. 如何寻找服务目标服务拆分后订单服务需要知道用户服务的网络地址。最直接的方式依然是通过IP 端口来定位一台机器上的具体进程。在生产环境中IP 可能被替换为域名或者通过服务注册中心如 Consul、Etcd动态发现但“地址端口”的基本逻辑不变。2. 如何确定调用的方法找到服务进程之后还需要指定具体要调用的函数。例如用户服务提供了Register、Login、GetUser等多个方法每个方法的功能和参数都不同。因此客户端必须携带一个方法标识通常采用服务对象名.方法名的格式比如UserService.GetUser。3. 如何传递参数与结果不同方法的参数和返回值千差万别必须设计通用的数据结构来封装请求和响应。在 Go 的net/rpc中典型的思路是请求结构体包含要调用的方法名、一个interface{}类型的参数可容纳任意具体类型以及一个唯一的请求 ID用于关联异步或并发场景下的请求与响应。响应结构体包含匹配的请求 ID、一个interface{}类型的结果载体以及一个错误字段用于传递执行过程中的异常。通过这样的泛化设计RPC 框架就能“认识”各种方法及其参数而无需为每个方法单独建立一套协议。4. 选择何种序列化协议与传输协议RPC 并未强制规定使用特定协议常见的序列化格式包括序列化协议优点缺点JSON可读性强跨语言支持好体积大解析慢XML自描述性强非常臃肿解析慢二进制如 Protobuf体积小解析极快可读性差需预定义 IDL传输层同样灵活TCP 协议因其可靠的字节流传输应用最为广泛HTTP 也因穿透防火墙方便而被大量采用。Go 的net/rpc默认使用 Go 特有的gob编码但可以通过额外封装支持 JSON 等其他编码。下面的 Mermaid 时序图直观展示了 RPC 通信的完整过程其中 Stub 层负责处理序列化与反序列化ServerServer StubClient StubClientServerServer StubClient StubClient调用方法(参数)序列化请求(方法名参数ID)通过网络(TCP/HTTP)发送反序列化请求定位方法执行实际方法返回结果或错误序列化响应(结果ID错误)通过网络返回反序列化响应返回结果(或错误)RPC 与 RESTful为什么微服务间更倾向于 RPC在服务间通信的选择上不少开发者会产生疑问既然都可以通过 HTTP 做远程调用为什么不采用RESTful风格而要使用 RPC二者确实都能实现服务 A 调用服务 B 的需求但出发点和本质有显著差别。RPC 是一种通信思想其目标是让远程函数调用具备与本地函数调用一样的体验。它强调“动作”——我要调用你的哪个函数我要执行什么操作。因此RPC 天然面向方法客户端和服务端通常属于同一系统内部的不同模块耦合度相对较高但调用方式更自然。RESTful 是一种架构风格它面向“资源”将一切视为可以通过 URI 唯一标识的实体并基于 HTTP 动词GET/POST/PUT/DELETE进行操作。RESTful 要求无状态客户端与服务端相互独立接口严格统一。它更适合跨系统、跨组织的松耦合场景。从定义上就可以看出RPC 并不排斥 RESTful 的实现——我们可以基于 HTTP 和 JSON 实现一套 RPC同时符合 RESTful 的部分规范但核心区别仍然存在调用目标RPC 调用的是“方法/函数”RESTful 操作的是“资源”。调用方式RPC 通过服务名.方法名定位RESTful 通过 URL 和 HTTP 动词定位。设计重心RPC 强调本地调用体验减少网络细节RESTful 强调资源的表述和状态转移。在微服务内部服务之间往往需要复杂的业务逻辑交互如果采用 RESTful 的方式不仅要精心设计 URL 和动词还会被迫暴露过多的资源细节。而使用 RPC开发人员可以像调用本地函数一样定义接口极大降低心智负担。因此微服务内部通信更倾向于使用 RPC 框架而对外暴露的 API 网关则有可能转化为 RESTful 供第三方调用。Gonet/rpc实践Go 标准库自带的net/rpc包提供了一套轻量级的 RPC 实现无需引入第三方依赖即可快速搭建服务端与客户端。下面通过一个获取用户信息的案例展示从目录结构到完整代码的构建过程。项目目录结构项目根目录下按职责划分文件夹结构清晰project-root/ ├── data/ │ └── user.go # 用户数据与存储 ├── server/ │ ├── server.go # 服务端定义与启动 │ └── types.go # 请求/响应结构体定义 └── client/ ├── main.go # 客户端调用示例 └── types.go # 复制自服务端的请求/响应结构体数据层data/user.go维护一个简单的用户结构体切片模拟数据库查询packagedatatypeUserstruct{IDintNamestringPhonestringAgeint}// 模拟用户数据表varUsersmap[int]User{1:{ID:1,Name:Alice,Phone:123456,Age:30},2:{ID:2,Name:Bob,Phone:987654,Age:25},}服务端实现服务端需要做四件事定义方法、注册服务、监听端口、处理连接。其中方法签名必须符合net/rpc的规定方法必须由两个参数第一个是请求结构体第二个是响应结构体的指针并返回一个error。方法还必须是被导出的首字母大写。定义请求/响应结构体惯例上请求和响应结构体以“方法名”作为前缀便于阅读和维护packageserver// 请求参数GetUser 的入参typeGetUserRequeststruct{IDint}// 响应结果GetUser 的出参typeGetUserResponsestruct{IDintNamestringPhonestringAgeint}定义服务结构体和方法packageserverimport(errorsyour_project/data// 导入数据包)typeUserServicestruct{}// GetUser 根据 ID 获取用户信息。// 方法签名须符合 func(t *T, args *ArgsType, reply *ReplyType) errorfunc(u*UserService)GetUser(req*GetUserRequest,resp*GetUserResponse)error{id:req.ID user,ok:data.Users[id]if!ok{returnerrors.New(user not found)}resp.IDuser.ID resp.Nameuser.Name resp.Phoneuser.Phone resp.Ageuser.Agereturnnil}注册服务并启动 TCP 监听packagemainimport(lognetnet/rpcyour_project/server)funcmain(){// 1. 实例化服务对象userService:new(server.UserService)// 2. 注册到 RPC Server注意传递指针err:rpc.Register(userService)iferr!nil{log.Fatal(register error:,err)}// 3. 监听 TCP 端口listener,err:net.Listen(tcp,:1234)iferr!nil{log.Fatal(listen error:,err)}log.Println(RPC server started on :1234)// 4. 循环接受连接并启动协程处理for{conn,err:listener.Accept()iferr!nil{log.Println(accept error:,err)continue}gorpc.ServeConn(conn)// 并发处理客户端请求}}服务端启动后会持续等待客户端连接。当有新连接到来时rpc.ServeConn会负责读取请求、调用对应方法并将响应编码后写回连接。整个过程对开发者透明。客户端实现客户端同样需要知道远程方法的签名以及请求/响应的结构体。通常这些定义可以从服务端复制或通过共享包引用。packagemainimport(fmtlognet/rpc// 假设从公共包导入请求/响应定义your_project/server)funcmain(){// 1. 建立 TCP 连接conn,err:net.Dial(tcp,localhost:1234)iferr!nil{log.Fatal(dial error:,err)}deferconn.Close()// 2. 创建 RPC 客户端client:rpc.NewClient(conn)deferclient.Close()// 3. 构造请求与响应req:server.GetUserRequest{ID:1}resp:server.GetUserResponse{}// 4. 远程调用服务名.方法名errclient.Call(UserService.GetUser,req,resp)iferr!nil{log.Fatal(call error:,err)}fmt.Printf(User: %v\n,resp)}在上面的代码中client.Call(UserService.GetUser, req, resp)中的服务名就是注册时的类型名称UserService方法名为GetUser。该方法是同步调用的Go 标准库同时提供了client.Go方法用于异步调用。常见错误与排查“没有找到服务”当客户端指定的服务名如UserService与注册的类型名不匹配时服务端会返回rpc: cant find service ...错误。检查注册时使用的类型名与客户端调用时是否一致。“没有找到方法”服务存在但方法不存在时会返回rpc: cant find method ...需确认方法是否满足net/rpc的签名规范以及方法名是否已导出。响应未修改客户端传入的resp必须是指针类型否则结果无法写回。同时方法内部对resp的赋值要确保字段被正确填充。序列化错误默认使用gob编码要求传递的结构体字段必须是导出的首字母大写。若跨语言通信可切换到net/rpc/jsonrpc包。小结RPC 的出现源于分布式系统中对透明远程调用的强烈需求。它将复杂的网络通信、序列化、Stub 代理等细节封装起来让开发者可以集中精力处理业务逻辑。通过对比 RPC 与 RESTful 的设计哲学可以看出二者并不矛盾而是适用于不同层次的通信场景RPC 更适合系统内的高效、面向过程的交互RESTful 更适合系统间松耦合的 API 暴露。借助 Go 标准库的net/rpc我们可以用极少的代码搭建出可用的 RPC 服务深入理解方法的注册、寻址规则、数据编解码等底层细节。这一基础实践为后续使用更高性能的 RPC 框架如 gRPC、Thrift或自行设计私有 RPC 协议打下了坚实基础。希望本文能帮助你将 RPC 从抽象概念转化为扎实的工程实践。