个人主页代码不加冰欢迎来访作者简介java后端学习者❄️个人专栏LeetCode刷题日记 苍穹外卖日记SSM框架深入JavaWeb✨命运的结局尽可永在不屈的挑战却不可须臾或缺前言大家好我是代码不加冰这里给大家分享一下最近在一个项目中学的新知识也算是自我的一个总结方便以后复习。摘要本文结合实际项目经验对 RPC远程过程调用的核心原理进行了系统梳理。从为什么微服务之间不直接使用 HTTP 调用入手分析了 RPC 在性能、开发体验以及服务治理方面的优势。随后深入讲解了 RPC 的底层调用流程包括序列化、网络传输、服务执行和结果返回等关键环节并重点介绍了动态代理在 RPC 框架中的作用帮助读者理解 Dubbo 如何实现“像调用本地方法一样调用远程服务”。此外文章还详细对比了 JDK 动态代理与 CGLIB 动态代理的实现方式并进一步延伸到 Nginx 反向代理和负载均衡机制分析其与 Dubbo 客户端负载均衡的区别与协作关系。通过本文读者可以建立从动态代理、RPC 到微服务通信架构的完整知识体系。什么是RPC框架RPCRemote Procedure Call即远程过程调用。本地调用你的代码在同一个 JVM同一个进程里。你写了一个userService.getUserById(1)直接在内存里点过去就执行了。远程调用随着业务变大你把用户模块拆分成了User 服务把订单模块拆分成了Order 服务它们分别部署在两台不同的服务器上。此时Order 服务想要调用User 服务的方法由于内存不共享普通的本地调用直接失效了。RPC 的核心目标就是让调用远程服务的方法时像调用本地方法一样简单、透明。1.为什么不用 HTTP / RestTemplate非要用 RPC很多刚学完 Spring Boot 的同学会问“我用 HTTP 请求比如 RestTemplate 或者是 HttpClient一样可以跨服务器调用啊为什么要用 RPC 框架”我们可以用一个简单的表格来对比特性HTTP (RESTful 常用)RPC (以 Dubbo 为例)传输协议通常基于 HTTP/1.1文本传输JSON常用底层 TCPDubbo协议或 HTTP/2Triple协议二进制传输性能消息体相对较大序列化/反序列化慢开销大消息体小序列化速度极快吞吐量极高开发体验需要手动拼 URL、处理状态码、解析 JSON就像调用本地接口一样强类型约束支持代码补全服务治理需要额外搭配各种组件如 Ribbon 负载均衡自带服务发现、负载均衡、容错、流量控制等功能大白话总结HTTP 就像邮寄挂号信格式通用但包装重、速度慢RPC 就像内部专用传送带专门为了高并发、高性能的分布式系统“量身定制”。2.RPC 的底层工作流程一个完整的 RPC 框架在底层到底做了什么其实它的核心流程其实就 4 步Stub桩/代理消费端Consumer通过动态代理生成一个接口的代理对象。你以为你调用的是方法其实调用的是代理。Serialize序列化代理对象把你的请求方法名、参数类型、参数值打包转成二进制字节流。Transport网络传输通过网络比如 Netty/TCP将字节流发送到服务提供端Provider。Deserialize反序列化提供端收到字节流还原成 Java 对象通过反射调用真正的业务方法然后再把结果按原路返回。动态代理正如前面所说RPC 的目标是让你像调用本地方法一样调用远程服务。但问题是消费端Consumer手里只有接口比如UserService.java并没有实现类实现类在远端的服务器上。在 Java 中接口是不能直接new出来对象的。那userService.getUserById(1)这行代码为什么能跑通而且还能把请求发送到网络另一端呢这就是动态代理施展的地方。1. 什么是动态代理静态代理比如你想买海外的商品你找了一个专业的代购。这个代购代理类和你想买的东西接口是一一对应的代购在代码运行前就已经存在了。动态代理你去了一个巨大的万能办事处。你走过去说“我想调用UserService的getUserById方法。” 办事处当场给你临时变出一个经纪人代理对象。这个经纪人根本不知道getUserById怎么具体实现但他知道把你的请求打包、通过网络发给远端的真实服务器、再把结果拿回来还给你。动态代理的核心能力在程序运行期间根据你传入的接口动态地在内存中生成一个代理对象。你对这个接口的所有方法调用都会被拦截并重定向到一个统一的处理器中。2. 在 RPC 项目中动态代理用来干什么如果没有动态代理每次想调用远程服务// 极其痛苦每次调用都要手动写网络请求 byte[] requestData serialize(getUserById, 1); byte[] responseData HttpClient.send(http://192.168.1.10:8080/userService, requestData); User user deserialize(responseData);有了动态代理后框架在底层帮你把这些脏活累活全封装了// 1. 动态代理在内存中生成一个 UserService 的实现类对象 UserService userService (UserService) Proxy.newProxyInstance(...); // 2. 你像调用本地代码一样调用它 User user userService.getUserById(1);当你调用userService.getUserById(1)时JVM 会自动把这个调用转发给动态代理的InvocationHandler调用处理器。在这个处理器里面框架帮你做了 4 件事组装报文把方法名getUserById、参数类型Long、参数值1封装成一个请求对象如RpcRequest。寻找地址去注册中心问一下UserService部署在哪台服务器上服务发现/负载均衡。网络传输把请求对象序列化成二进制流通过 Netty 或 Socket 发送给提供者Provider。获取结果等待提供者返回结果反序列化成User对象并return给你的业务代码。对于编写业务代码的你来说你根本不知道中间发生了一次网络跨越这就是所谓的“透明化调用”。3. Java 中常见的动态代理实现① JDK 动态代理Dubbo 2.x 及大部分 RPC 默认首选原理利用 JDK 自带的java.lang.reflect.Proxy类基于接口生成代理类。要求目标类必须实现接口。因为 RPC 本身就是面向接口编程的消费端只有接口所以 JDK 动态代理完美契合 RPC 场景。核心代码结构public class RpcProxyHandler implements InvocationHandler { Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 在这里拦截到方法调用 System.out.println(准备调用远程方法: method.getName()); // 2. 在这里写你的网络发送逻辑 (Socket/Netty) // ... 发送并等待接收结果 ... return rpcResponse.getResult(); // 返回远程执行的结果 } }② CGLIB / Javassist 动态代理原理通过修改字节码生成目标类的子类来做代理。要求不需要实现接口只要类和方法不是final的就行。在 Dubbo 中的应用Dubbo 为了追求极致的性能并没有单纯使用 JDK 自带的动态代理而是默认使用了Javassist或者在新版本中支持 ByteBuddy来动态生成字节码。因为 Javassist 生成代理类的速度和执行效率比原生 JDK 更快。Nginx的反向代理我们在苍穹外卖中学过一个词叫反向代理可能大家都快忘了包括我也是只知道有这个词但是具体的原理早就忘记了这里顺便提一下。对比一下正向代理它们最核心的区别在于它们究竟是在给客户端当代理还是在给服务端当代理。正向代理代理的是客户端传话筒。服务端不知道真正发起请求的客户端是谁。反向代理代理的是服务端挡箭牌。客户端不知道真正提供服务的是哪台后端服务器。 正向代理正向代理位于客户端和目标服务器之间。客户端非常清楚自己要访问谁但由于某些原因比如网络限制、隐藏身份客户端无法直接访问或者不想直接访问于是找了一个代理服务器。经典场景科学上网国内上不了某些国外网站你找了一台能上该网站的国外代理服务器。你把请求发给代理代理帮你去该网站拿数据再传回给你。公司内网行为管理公司为了防止员工上班摸鱼让所有人的电脑都通过一个正向代理服务器上网。这个代理服务器可以限制你不能访问游戏网站、购物网站。特点客户端必须进行配置比如在浏览器里设置代理 IP 和端口。目标服务器只知道代理服务器的 IP不知道真正的客户端是谁保护了客户端隐私。 反向代理反向代理同样位于客户端和目标服务器之间但它是服务端的入口。客户端只知道反向代理服务器的地址比如nginx.com直接把请求发过去。反向代理收到后在后台悄悄把请求分发给真正干活的业务服务器如 Tomcat。经典场景Nginx 负载均衡你的网站访问量巨大一台服务器扛不住。你用 Nginx 作为反向代理后面挂了 10 台业务服务器。用户统一访问 NginxNginx 自动把流量分发给这 10 台服务器。安全防护保护后端服务器不被黑客直接攻击。黑客只能看到反向代理服务器的 IP根本碰不到内网真正的数据库和核心业务服务器。特点客户端不需要做任何配置用户完全感知不到反向代理的存在觉得这就是普通的网站。客户端只知道反向代理的 IP不知道后端真正提供服务的是哪台服务器保护了服务端隐私。为了方便记忆我们可以通过下面这张表来做对比对比维度正向代理 (Forward Proxy)反向代理 (Reverse Proxy)代理的对象客户端替客户端发送请求服务端替服务端接收请求谁知道真相客户端知道它自己配置的代理服务端知道它自己架构的 Nginx谁被隐瞒了目标服务端不知道真正的客户端是谁客户端不知道真正的业务服务器是谁部署位置靠近客户端通常在局域网内靠近服务端通常在服务集群前端核心作用突破访问限制翻墙、隐藏客户端、上网行为审计负载均衡、保障内网安全、缓存加速类比一下正向代理找中介你想租房但你不想让房东知道你是谁可能你是个明星。于是你找了一个房屋中介正向代理替你去和房东谈。房东只看到了中介合同也是和中介签的房东不知道真正的租客是谁。反向代理找前台/物业你去租某个大集团的公寓你来到公寓的接待前台反向代理。你跟前台说“我要租房”前台帮你办理了手续并分给你 302 房间。你自始至终不知道这个公寓背后的老板/管家具体是谁你只和前台打交道。负载均衡这里想到了负载均衡也顺便提一下吧我看的项目也涉及到了Nginx 的反向代理负载均衡就像一个商场的大门保安兼导购。外面的顾客浏览器/App想进来买东西统一先找 NginxNginx 看看哪个收银台Web 服务器人少就把顾客带到哪个收银台。它面对的是外部世界。Dubbo 的负载均衡就像商场内部的对讲机调度系统。收银台Web 服务在结账时需要呼叫仓库Order 服务或者财务User 服务。收银员自己用对讲机问“仓库 A 和仓库 B 哪个现在有空”然后直接把请求发过去。它面对的是企业内部服务之间的沟通。深入细节对比维度Nginx 反向代理负载均衡Dubbo 负载均衡工作位置系统最前端网关/边缘。介于客户端浏览器和微服务群之间。系统内部RPC 层。介于内部服务 A 和服务 B 之间。代理对象代理服务器。客户端只知道 Nginx 的 IP不知道后端真实服务器的 IP。代理接口/方法。通过动态代理让开发人员感觉在调用本地方法。传输协议通常是HTTP / HTTPS。通常是高性能的TCP 协议如 Dubbo 协议、Triple 协议。负载均衡机制集中式服务端负载均衡。请求必须先经过 Nginx 这个“中间商”由 Nginx 决定转发给谁。客户端负载均衡。消费者Consumer本地有服务列表自己决定调用哪台 Provider不经过任何中间商直连过去。服务发现需要手动在nginx.conf里配置upstream的 IP 列表或者结合外部组件静态配置。动态自动发现。通过注册中心如 Nacos/ZooKeeper服务上线下线自动感知无需人工干预。客户端负载均衡 vs 服务端负载均衡Nginx服务端负载均衡 (Server-side LB)浏览器发送请求给 Nginx。Nginx 作为一个实体服务器架在中间由它来选择一台后端的 Tomcat并把请求转发Forward过去。缺点Nginx 成了系统的单点瓶颈。如果流量巨大Nginx 可能会扛不住而且多了一次网络转发会有微小的延迟。Dubbo客户端负载均衡 (Client-side LB)Dubbo 的消费端Consumer在启动时会去注册中心Nacos把服务提供者Provider的 IP 地址列表下载到本地缓存起来。当代码调用userService.getUserById()时Dubbo 在本地运行负载均衡算法比如随机、轮询直接选出一个 IP比如192.168.1.5。消费端直接发起 Socket 连接调用该 IP中间没有任何性能损耗和多余的服务器中转。它们在真实架构中如何协同工作在一套标准的微服务架构中它们是各司其职的。流量的完整生命周期通常是这样的[ 用户的浏览器/手机App ] │ (HTTP 协议) ▼ ┌───────────────┐ │ Nginx 大门 │ --- 服务端负载均衡把外网 HTTP 流量分发给前端 Web 服务器 └───────┬───────┘ │ (HTTP 协议) ▼ ┌──────────────────────────────────────┐ │ Spring Boot 业务网关 / 前端应用集群 │ └───────┬──────────────────────────────┘ │ │ (Dubbo 动态代理拦截在本地做负载均衡转为 TCP 协议) ▼ ┌──────────────────────────────────────┐ │ 内部微服务 A (Consumer) │ └───────┬──────────────────────────────┘ │ (TCP 直连不经过中间商) ▼ ┌──────────────────────────────────────┐ │ 内部微服务 B (Provider) │ └──────────────────────────────────────┘第一步用户在国内发起一个 HTTP 请求首先到达Nginx。Nginx 看眼前有 3 台网关服务器通过轮询把请求丢给其中一台Nginx 实现了外网到内网的分流。第二步网关处理完基础验证后需要调用内部的“商品服务”。此时网关作为 Dubbo 的Consumer利用动态代理拦截了调用并在本地计算出“商品服务-实例B”目前最空闲。第三步Consumer 直接通过 TCP 协议把请求发给“商品服务-实例B”Dubbo 实现了内网微服务之间的高性能沟通。结语如果对你有帮助请点赞关注收藏你的支持就是我最大的鼓励