RPC:像调用本地函数一样调用远程服务
RPC像调用本地函数一样调用远程服务目录RPC是什么为什么需要RPC一次调用的完整过程Java 示例Stub 和 Skeleton框架做了什么和 REST 有什么区别小结RPC是什么在分布式系统里服务部署在不同的机器上。订单服务想查用户信息用户服务在另一台机器。RPC 解决的问题就是**让你调用远程机器上的方法像调用本地方法一样简单。**不需要自己拼 HTTP 请求、解析 JSON 响应、处理网络异常。只需要调一个接口方法RPC 框架帮你把剩下的事全干了。为什么需要RPC假设没有 RPC订单服务要调用户服务查用户信息代码大概长这样// 手动拼 HTTP 请求Stringurlhttp://user-service/api/user?id1001;HttpRequestrequestHttpRequest.newBuilder().uri(URI.create(url)).GET().build();HttpResponseStringresponseHttpClient.newHttpClient().send(request,HttpResponse.BodyHandlers.ofString());// 手动解析 JSONJsonNodejsonobjectMapper.readTree(response.body());Stringnamejson.get(name).asText();intagejson.get(age).asInt();UserusernewUser(name,age);这段代码有几个问题。调用方和协议强耦合。你得知道服务的地址、端口、路径、请求格式、返回格式。换一个服务就得重写一遍。错误处理散落各处。网络超时、服务不可用、返回格式异常每个远程调用都要单独处理代码里到处都是 try-catch。接口没有约束。你传了id还是userId返回的字段叫name还是userName全靠文档约定。文档过期了、字段名拼错了运行时才发现。RPC 的目标就是把这些重复的、容易出错的活封装掉。你定义一个接口写一个实现剩下的序列化、网络传输、错误处理、负载均衡框架全包了。一次调用的完整过程你在客户端写了一行代码User user userService.getUser(1001);。看起来就是一次普通的方法调用但背后发生了六件事。第一步客户端调用的是一个代理对象的方法不是真正的业务逻辑。这个代理就是我们接下来要说的 Stub。第二步代理把方法名、参数类型、参数值打包成一个请求对象序列化成字节流。通常用 JSON 或 Protobuf。第三步字节流通过网络发到服务端。底层走 TCP 还是 HTTP/2 取决于框架。第四步服务端收到字节流反序列化出方法名和参数。第五步服务端根据方法名找到对应的实现类执行真正的业务逻辑。第六步把返回值序列化通过网络传回客户端客户端反序列化后拿到结果。整个过程对调用方完全透明。你写的代码就是userService.getUser(1001)和调用本地方法一模一样。Java 示例用 Java 写一个最小的 RPC 调用示例感受一下像调本地方法一样是什么体验。先定义一个接口publicinterfaceUserService{UsergetUser(intid);}客户端代码// 拿到一个代理对象框架生成的不是你手写的UserServiceuserServiceRpcClient.create(UserService.class);// 就像调本地方法一样UseruseruserService.getUser(1001);System.out.println(user.getName());就这么多。没有 HttpClient没有 JSON 解析没有 try-catch 网络异常。两行代码拿代理、调方法。服务端代码publicclassUserServiceImplimplementsUserService{OverridepublicUsergetUser(intid){// 真正的业务逻辑查数据库returnuserMapper.selectById(id);}}和服务端的交互完全一样。实现接口写业务逻辑注册到框架完事。那中间那些序列化、网络传输谁干的框架干的。下面展开说。Stub 和 SkeletonRPC 框架的核心是两个代理层。客户端 │ ├─ UserService你定义的接口 │ └─ Stub客户端代理 │ ├─ 把方法调用转成请求对象 │ { method: getUser, args: [1001] } │ └─ 序列化 → 发送 → 等待响应 → 反序列化 │ ▼ 网络传输 │ ▼ Skeleton服务端代理 │ ├─ 反序列化请求 ├─ 根据方法名找到实现类 ├─ 调用 UserServiceImpl.getUser(1001) └─ 序列化返回值 → 发回客户端Stub 是客户端的代理。它实现了你定义的接口但方法体里没有业务逻辑只有一套编码和网络发送的代码。你调userService.getUser()它拦截这次调用把方法名和参数打包发出去等响应回来再拆包返回给你。从你的角度看就是在调一个普通方法。Skeleton 是服务端的代理。它监听网络请求收到后反序列化出方法名和参数找到对应的实现类去执行再把返回值打包发回去。它就像一个前台负责接单、派活、把结果送回去。Stub 和 Skeleton 的区别用一张表说清楚Stub客户端Skeleton服务端位置客户端进程内服务端进程内实现什么接口你定义的业务接口不需要干什么拦截方法调用编码发送接收请求解码执行对调用方透明吗是像调本地方法是像被本地调用有了这两层客户端和服务端都不需要关心网络细节。客户端只管调接口服务端只管写实现中间的活框架全包了。框架做了什么实际开发中你不会手写 Stub 和 Skeleton框架会帮你生成。来看几个主流的 RPC 框架框架出品方序列化协议特点Dubbo阿里Hessian2 / Protobuf / JSON国内微服务首选服务治理能力强gRPCGoogleProtobuf跨语言支持好HTTP/2性能强ThriftFacebook自定义二进制跨语言轻量适合高性能场景框架帮开发者做的事情还有以下这些服务注册与发现服务启动时自动注册到注册中心Nacos、ZooKeeper客户端不需要硬编码服务地址负载均衡多个实例时自动选择一个轮询、随机、加权可配置超时和重试网络抖动时自动重试超时自动熔断版本管理接口升级时灰度发布新旧版本并存链路追踪一次请求经过哪些服务每个环节耗时多少全链路可观测这些能力对业务开发是透明的。你只管写接口和实现框架在底层默默处理分布式系统的各种复杂问题。和 REST 有什么区别这是初学者最容易混淆的地方既然 REST 也能跨服务调用为什么还要 RPC维度RPCREST本质一次函数调用一次资源操作协议通常用二进制Protobuf、HessianJSON 文本接口定义IDL 文件可自动生成代码文档Swagger手动对接性能二进制序列化 HTTP/2更快JSON 解析 HTTP/1.1较慢适用场景内部服务间高频调用对外暴露的 HTTP API简单来说RPC 是面向函数调用的REST 是面向资源的。内部服务之间互相调用RPC 更合适性能好、接口约束强、代码自动生成。对外暴露 API 给前端或第三方REST 更通用浏览器原生支持、调试方便、格式可读。这两者不冲突很多系统同时用两种内部服务间走 Dubbo/gRPC对外暴露 REST 网关。小结RPC 的核心就是把网络调用封装成方法调用。Stub 代理拦截客户端的方法调用并编码发送Skeleton 代理接收请求并解码执行框架在中间处理序列化、传输、负载均衡、容错等所有分布式问题。所以开发者写的代码和调用本地方法在体感上基本没有区别体验感很不错。