无尘阁日记

无尘阁日记

RPC 不写网址,它到底怎么找到对方的
2026-03-20

你真正困惑的点,其实非常关键:

如果 RPC 不是像 HTTP 那样,直接写一个完整地址,那它到底怎么知道该调用谁?

答案是:

RPC 不是不需要地址,而是“你平时不用自己手写地址”,地址这件事被框架和基础设施接管了。

你可以先记住这句话。下面我用特别小白的话,一层一层讲开。

一、先说结论:RPC 不是不要地址,而是把地址藏起来了

很多人第一次看 RPC,会以为它很神奇,像这样:

$userService->getUserInfo(1001);

感觉根本没写:

  • IP

  • 端口

  • URL

  • 路由

那它怎么找到对方?

其实答案很简单:

底层仍然一定要知道“对方在哪”,否则网络请求根本发不出去。
只是这个“对方在哪”,不是你业务代码手写,而是交给别的东西来处理了。

所以你要把它理解成:

HTTP 是你自己手填导航地址,RPC 是你告诉司机“去国贸三期”,司机自己导航过去。

你没看见路线,不代表没有路线。

二、为什么 HTTP 里你更容易看到“地址”

比如你调用一个普通接口,经常会写成这样:

http://user-service:8080/api/user/detail?id=1001

这里你能很直观看到:

1. 主机

user-service

或者 192.168.1.10

2. 端口

8080

3. 路径

/api/user/detail

所以你会感觉:

哦,我就是靠这个地址找到它的。

这个很直白。

三、RPC 为什么表面上看不到这些

因为 RPC 框架做了一层“代理”或者“中间翻译”。

你业务里写的是:

$userService->getUserInfo(1001);

但实际上背后不是直接执行本地方法,而是:

1. 先识别你要调哪个服务

比如 UserService

2. 再识别你要调哪个方法

比如 getUserInfo

3. 再去某个地方查

“UserService 现在部署在哪几台机器上?”

4. 选一台可用机器

比如:

  • 10.2.1.15:9001

  • 10.2.1.16:9001

  • 10.2.1.17:9001

5. 把请求发过去

发给某一个实例

所以:

你虽然没写地址,但 RPC 框架在背后替你查地址、选地址、连地址。

四、你可以把 RPC 理解成“按名字找人”,不是“按门牌找人”

这个比喻非常适合你记忆。

HTTP 更像什么

HTTP 更像你自己拿着详细地址去找人:

“北京市朝阳区 xx 路 xx 号 3 楼 302”

你自己知道详细门牌号。

RPC 更像什么

RPC 更像你跟前台说:

“我找财务部的小王。”

然后前台帮你查:

  • 小王今天在哪层

  • 在哪个工位

  • 现在在不在

  • 如果小王不在,财务部还有谁能处理

然后再帮你转过去。

所以 RPC 更像:

按“服务名”找人,不是按“固定门牌号”找人。

五、那 RPC 到底靠什么识别对方

核心靠两样东西:

1. 服务名

比如:

  • UserService

  • OrderService

  • VerifyService

  • TokenService

这相当于“我要找谁”。

2. 服务注册信息

也就是一张“服务通讯录”

这张通讯录里会写:

  • UserService 在哪几台机器上

  • 每台机器的 IP 是什么

  • 端口是什么

  • 哪些实例还活着

  • 哪些已经挂了

这就相当于“那个人现在具体坐在哪”。

六、最关键的角色:注册中心

这个东西你一定要理解,因为它就是你这个疑问的核心答案。

注册中心,你可以把它理解成:

一本动态通讯录 + 实时在线名单。

比如系统里有三个用户服务实例:

  • 10.0.0.11:9001

  • 10.0.0.12:9001

  • 10.0.0.13:9001

它们启动时,会主动去注册中心报到:

“我是 UserService,我的地址是 10.0.0.11:9001,我现在活着。”

“我是 UserService,我的地址是 10.0.0.12:9001,我现在活着。”

“我是 UserService,我的地址是 10.0.0.13:9001,我现在活着。”

于是注册中心就记住了:

UserService
├── 10.0.0.11:9001
├── 10.0.0.12:9001
└── 10.0.0.13:9001

当你这边调用:

$userService->getUserInfo(1001);

RPC 框架就会先去问注册中心:

“请问 UserService 现在哪些机器可用?”

注册中心返回:

  • 10.0.0.11:9001

  • 10.0.0.12:9001

  • 10.0.0.13:9001

然后框架自己挑一台发请求。

这就完成寻址了。

七、所以不是没有 IP 和端口,而是你不亲自写

这个地方你一定要彻底想通。

很多人误会 RPC 是:

完全不用 IP、端口。

其实不可能。

因为只要是网络通信,最终一定得落到某个具体地址:

  • IP

  • 域名

  • 端口

否则包都不知道往哪发。

真正的区别是:

HTTP 常常是你业务代码里直接写

比如:

http://10.0.0.11:8080/api/user/detail

RPC 常常是框架运行时帮你查出来

比如:

“UserService” → 查到 10.0.0.11:9001

所以可以说:

RPC 只是把“硬编码地址”换成了“服务名 + 动态解析地址”。

八、那“方法名”和“服务名”又是怎么识别的

这个要分两层看。

第一层:先找到哪台机器

靠服务名,比如:

  • UserService

  • VerifyService

这是“找到哪一家”。

第二层:到了那台机器后,调哪个方法

靠方法名,比如:

  • getUserInfo

  • verifyUser

  • createToken

这是“找到这家里的哪个窗口”。

你可以理解成:

服务名

像公司名字
比如“天健兴业用户中心”

方法名

像公司里的具体业务窗口
比如“一号窗口查用户”“二号窗口校验身份”

所以一次 RPC 调用,背后通常包含:

  • 服务名

  • 方法名

  • 参数

类似这样:

{
  "service": "UserService",
  "method": "getUserInfo",
  "params": [1001]
}

然后先靠 service 找机器,再靠 method 找机器里的处理逻辑。

九、为什么 RPC 不愿意让你手写固定 IP

因为固定 IP 很笨,不灵活。

比如你手写:

10.0.0.11:9001

如果这台机器挂了怎么办?

那你代码就废了。

再比如,用户服务为了抗并发,扩容成 5 台机器:

  • 10.0.0.11:9001

  • 10.0.0.12:9001

  • 10.0.0.13:9001

  • 10.0.0.14:9001

  • 10.0.0.15:9001

这时候如果你还手写某一台 IP,就会有几个问题:

1. 单点问题

只打一台,很容易打挂

2. 扩容无感知

新机器加了,你也用不上

3. 容灾差

某台挂了你就报错

所以 RPC 更喜欢:

只认服务名,不认死地址。

这样底层可以动态切换。

十、RPC 底层常见的完整寻址流程

我给你用最白的话走一遍。

假设你写:

$userService->getUserInfo(1001);

背后大概是这样。

第一步:本地代理对象接管调用

你以为你在调 userService,其实这个 userService 往往不是一个真正的本地业务类,而是一个“代理对象”。

这个代理对象专门负责:

  • 拦截你的方法调用

  • 组装请求

  • 发给远程服务

也就是说,它像个秘书。

你一说“帮我查用户”,秘书就开始办事了。

第二步:确定服务名

代理对象知道自己代表的是 UserService

第三步:去注册中心查地址

查到当前可用实例:

  • 10.0.0.11:9001

  • 10.0.0.12:9001

第四步:负载均衡挑一台

比如轮询选中:

  • 10.0.0.12:9001

第五步:把方法和参数打包

打包成请求消息:

{
  "service": "UserService",
  "method": "getUserInfo",
  "params": [1001]
}

第六步:通过网络发到 10.0.0.12:9001

这个时候真正还是发给了一个 IP + 端口

第七步:服务端收到后解析

发现你要调:

  • 服务:UserService

  • 方法:getUserInfo

  • 参数:[1001]

第八步:执行并返回结果

所以你看,整个链路里:

IP 和端口一直都存在,只是它们在幕后。

十一、那如果没有注册中心呢

也可以做 RPC,但会比较笨。

比如你在配置文件里写死:

UserService: 10.0.0.11:9001
OrderService: 10.0.0.21:9001

那框架照样能调。

这种本质上也是 RPC,只不过寻址方式不是“动态查注册中心”,而是“读固定配置”。

所以你可以这样理解:

有注册中心

像通讯录实时更新,谁在线、谁下线都知道

没注册中心

像你自己拿着一张纸条,纸条上写死了电话号

能用,但不灵活。

十二、再说“地址路由”这个词,你可能混了两层东西

你说“服务那边的地址路由”,这里其实容易把两件事混到一起。

第一层:找到哪台机器

这是网络层寻址

比如:

  • 10.0.0.12:9001

这是“去哪个楼”。

第二层:找到哪个方法

这是服务内部路由

比如:

  • UserService.getUserInfo

  • UserService.verifyUser

这是“进楼以后去哪个办公室”。

HTTP 里这两层通常写在一个 URL 里,所以你更容易看到:

http://10.0.0.12:9001/api/user/detail

里面:

  • 10.0.0.12:9001 是机器地址

  • /api/user/detail 是业务路由

但 RPC 往往拆开了:

  • 机器地址:运行时动态解析

  • 业务方法:在请求包里标记

所以你会感觉“没有路由”,其实不是没有,而是换了表达方式。

十三、一个特别形象的比喻:打总机

RPC 特别像以前公司打总机。

你不是每次都直接拨某个人的分机号。
你可能只说:

“帮我转财务部,找小王。”

总机做了这些事:

1. 确认你找财务部

这就是服务名

2. 查财务部现在有哪些人在岗

这就是注册中心的可用实例列表

3. 选一个可接电话的人

这就是负载均衡

4. 接通后告诉对方你要办什么业务

这就是方法名 + 参数

所以 RPC 不是不寻址,而是:

从“自己手拨号码”变成了“总机代为转接”。

十四、为什么这对微服务特别重要

因为微服务里,服务实例经常变化。

今天用户服务可能在:

  • A 机器

  • B 机器

明天可能扩容到:

  • C 机器

  • D 机器

后天 A 机器挂了,E 机器又补上来。

如果你业务代码里全写死 IP,那维护会疯掉。

RPC 采用服务发现机制之后,你只管写:

$userService->getUserInfo(1001);

底层会自己感知:

  • 哪些实例还活着

  • 哪些挂了

  • 该打给谁

这就是它的价值。

十五、你可以这样建立最终认知

你现在最应该建立的,不是“RPC 很神秘”,而是下面这个认知:

第一层

RPC 调用表面上像本地方法调用

第二层

但底层一定还是网络通信

第三层

既然是网络通信,就一定还是要知道对方的实际地址

第四层

只是这个地址不是你手写,而是由服务发现、注册中心、代理对象、负载均衡这些东西帮你处理了

这个逻辑一旦通了,你后面看 RPC 就不会迷糊了。

十六、最适合你记忆的一句话

RPC 不是没有地址,而是“按服务名找地址”,不是“你自己手写死地址”。

十七、我再给你一个超简版

你看到的

$userService->getUserInfo(1001);

你没看到的

1. 识别服务名:UserService
2. 去注册中心查:UserService 在哪几台机器上
3. 选一台:10.0.0.12:9001
4. 组装请求:method=getUserInfo, params=[1001]
5. 发过去
6. 对方执行后返回

十八、最后用你的语言收一下

所以你问的这个点,答案其实就是:

他不是“不用 ip、端口、路由”,而是这些东西被藏到框架底层了。

你业务层只写“我要调用哪个服务、哪个方法”。
至于:

  • 去哪台机器

  • 用哪个端口

  • 当前哪台活着

  • 该选哪个实例

都由 RPC 框架自动完成。

这就是为什么你表面看不到地址,但它仍然能精准找到对方。

如果你愿意,我下一条可以继续给你画一张:

“HTTP 接口寻址 vs RPC 寻址”的对照图

我会直接用你熟悉的:

controller → service → 对方系统

这个链路给你讲,一下就彻底通了。