说远程调用协议,大部分人先想到的是 REST 或 gRPC。那 JSON-RPC 是什么?跟它们比有什么优势?什么场景下该用它?
在之前写的 MCP 介绍 里提过,MCP 协议支持两种传输方式:stdio 和 Streamable HTTP。而 MCP 的消息格式底层用的正是 JSON-RPC(更准确地说,是 JSON-RPC 2.0 的一个超集)。要理解 MCP 的通信细节,得先搞清楚 JSON-RPC。
这篇文章就从头讲起:RPC 是什么、JSON-RPC 怎么用、它和 REST / gRPC 的区别、真实代码示例、以及容易踩的坑。
先说说 RPC 是什么
RPC(Remote Procedure Call,远程过程调用)的核心思想很简单:让调用远程函数像调用本地函数一样。
┌─────────────┐ ┌─────────────┐
│ │ ── 请求 ──▶ │ │
│ 客户端 │ │ 服务端 │
│ │ ◀── 响应 ── │ │
└─────────────┘ └─────────────┘比如客户端调了 add(1, 2),服务端收到请求、执行加法、返回结果 3。对客户端来说,它并不知道(也不关心)这个函数是在本机执行的还是在远端的另一台机器上执行的。
RPC 不是新概念,早在 1980 年代就有了(Sun RPC、DCE/RPC)。它要解决的根本问题是:跨进程调用怎么包装起来、让开发者感觉不到"跨"这件事。
传统 RPC 框架(CORBA、Java RMI、SOAP 等)各有各的序列化格式和协议约定,很重量级。JSON-RPC 的思路是:序列化用 JSON,传输层用 HTTP 或任何双向通道,能跑就行。把定义降到了最低,反而让它非常好用。
JSON-RPC 是什么
JSON-RPC 是一个无状态的、轻量级的远程过程调用协议,使用 JSON(RFC 4627)作为数据格式。
当前版本是 2.0(2008 年提出,2010 年正式定稿)。只有一个规范文档,非常短,核心内容一张 A4 纸就能写完。
JSON-RPC 的几个核心设计原则:
- 传输层无关:可以用 HTTP、TCP、Unix Socket、标准输入输出(stdio)、消息队列等任何双向通道。协议不绑定传输方式。
- 无状态:每个请求都是独立的,服务端不维护客户端会话。
- 轻量定义:规范中只定义请求对象、响应对象、错误对象三个结构,再加一个通知(Notification)概念。
- JSON 编码:所有数据都用 JSON,人类可读、语言无关、解析成本低。
JSON-RPC 2.0 的消息格式
规范定义了两种消息:请求(Request) 和 响应(Response)。
请求对象
{
"jsonrpc": "2.0",
"id": 1,
"method": "subtract",
"params": [42, 23]
}| 字段 | 必须 | 说明 |
|---|---|---|
jsonrpc |
是 | 固定 "2.0",用于版本协商 |
id |
条件 | 客户端建立的唯一标识。请求必须有 id,通知(Notification)没有 id |
method |
是 | 要调用的方法名(字符串) |
params |
否 | 参数结构。可以是数组(位置参数)或对象(命名参数) |
params 支持两种风格:
位置参数(数组):
{ "jsonrpc": "2.0", "id": 1, "method": "subtract", "params": [42, 23] }命名参数(对象):
{ "jsonrpc": "2.0", "id": 1, "method": "subtract", "params": { "subtrahend": 23, "minuend": 42 } }服务端可以选择支持一种或两种。规范建议命好名参数更清晰。
响应对象
成功响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": 19
}错误响应:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found",
"data": null
}
}| 字段 | 必须 | 说明 |
|---|---|---|
jsonrpc |
是 | 固定 "2.0" |
id |
是 | 必须和请求的 id 一致。请求的 id 为 null 或不存在时,返回 null |
result |
条件 | 调用成功时有,不能和 error 共存 |
error |
条件 | 调用失败时有,不能和 result 共存 |
关键约束: result 和 error 是互斥的,一个响应里只能有一个。
错误对象
| 成员 | 类型 | 必须 | 说明 |
|---|---|---|---|
code |
integer | 是 | 错误码 |
message |
string | 是 | 对人类可读的简短描述 |
data |
any | 否 | 附加调试信息(可选) |
JSON-RPC 2.0 预定义了几个标准错误码:
| 码 | 含义 | 说明 |
|---|---|---|
-32700 |
Parse error | 服务端收到无效 JSON |
-32600 |
Invalid Request | 发送的 JSON 不是合法的请求对象 |
-32601 |
Method not found | 方法不存在或不可用 |
-32602 |
Invalid params | 参数无效 |
-32603 |
Internal error | 服务端内部错误 |
-32000 到 -32099 |
Server error | 服务端自定义错误 |
错误码在 -32000 到 -32099 和 -32700 以下的,供实现者自定义使用。
通知(Notification):不需要响应的请求
通知是没有期望响应的请求。它和有响应的请求唯一区别就是 id 字段不存在(不能设成 null)。
{ "jsonrpc": "2.0", "method": "update", "params": [1, 2, 3] }服务端收到通知后 不得返回响应(不管成功还是失败)。如果服务端执意要响应,客户端因为没带 id 也认不出是哪个请求的响应。
通知适合「发完不管」的场景:日志、心跳、事件广播、进度更新等。
批量调用
JSON-RPC 支持一次发送多个请求,以数组形式发送:
[{ "jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1" }, { "jsonrpc": "2.0", "method": "notify_hello", "params": [7] }, { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2" }, { "foo": "boo" }]服务端按顺序处理(但规范不要求严格的顺序),然后返回数组形式的响应:
[
{ "jsonrpc": "2.0", "result": 7, "id": "1" },
{ "jsonrpc": "2.0", "result": 19, "id": "2" },
{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Request" }, "id": null }
]注意:通知不会出现在响应数组中(因为不应该有响应)。批量请求里的某个请求失败,不影响其他请求的响应。服务端总是返回和「有 id 的请求」一一对应的数组。
一个完整的例子(Node.js + HTTP)
用 Node.js 写一个最简单的 JSON-RPC 服务端和客户端。
server.js:
import http from 'node:http';
import { URL } from 'node:url';
const methods = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) throw { code: -32603, message: 'Division by zero' };
return a / b;
},
ping: () => 'pong',
};
function createError(id, code, message, data = null) {
return { jsonrpc: '2.0', id, error: { code, message, data } };
}
function createSuccess(id, result) {
return { jsonrpc: '2.0', id, result };
}
function handleRequest(body) {
// 批量请求
if (Array.isArray(body)) {
return body.map(handleSingle).filter((r) => r !== null);
}
// 单个请求
return handleSingle(body);
}
function handleSingle(req) {
// 校验必需字段
if (!req || typeof req !== 'object' || req.jsonrpc !== '2.0' || !req.method) {
return createError(req?.id ?? null, -32600, 'Invalid Request');
}
const { id, method, params } = req;
const isNotification = id === undefined || id === null;
if (typeof method !== 'string' || !method) {
return createError(id, -32600, 'Invalid Request');
}
const handler = methods[method];
if (!handler) {
return createError(id, -32601, `Method not found: ${method}`);
}
try {
const args = Array.isArray(params) ? params : [params];
const result = handler(...args);
// 通知不返回响应
if (isNotification) return null;
return createSuccess(id, result);
} catch (err) {
if (isNotification) return null;
return createError(id, err.code || -32603, err.message || 'Internal error');
}
}
const server = http.createServer(async (req, res) => {
if (req.method !== 'POST') {
res.writeHead(405);
return res.end('Method Not Allowed');
}
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => {
try {
const parsed = JSON.parse(body);
const response = handleRequest(parsed);
if (response !== null) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} else {
// 全是通知,不响应
res.writeHead(204);
res.end();
}
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(createError(null, -32700, 'Parse error')));
}
});
});
server.listen(8080, () => console.log('JSON-RPC server on :8080'));client.js:
const rpc = 'http://localhost:8080';
async function call(method, params) {
const res = await fetch(rpc, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
});
return res.json();
}
// 示例
console.log(await call('add', [1, 2])); // → { jsonrpc: '2.0', result: 3, id: 1 }
console.log(await call('divide', [10, 0])); // → error: Division by zero
console.log(await call('ping', [])); // → { jsonrpc: '2.0', result: 'pong', id: 1 }
console.log(await call('unknown', [])); // → error: Method not foundPython 版本
server.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
methods = {
'add': lambda a, b: a + b,
'subtract': lambda a, b: a - b,
'multiply': lambda a, b: a * b,
'divide': lambda a, b: b and a / b or exec('raise Exception("Division by zero")'),
'ping': lambda: 'pong',
}
def handle_request(body):
if isinstance(body, list):
return [handle_single(r) for r in body if r is not None]
return handle_single(body)
def handle_single(req):
if not isinstance(req, dict) or req.get('jsonrpc') != '2.0' or not req.get('method'):
return create_error(req.get('id'), -32600, 'Invalid Request')
method = req['method']
params = req.get('params', [])
is_notification = req.get('id') is None
handler = methods.get(method)
if not handler:
return create_error(req['id'], -32601, f'Method not found: {method}')
try:
args = params if isinstance(params, list) else [params]
result = handler(*args)
return None if is_notification else create_success(req['id'], result)
except Exception as e:
return None if is_notification else create_error(req['id'], -32603, str(e))
def create_success(id, result):
return {'jsonrpc': '2.0', 'id': id, 'result': result}
def create_error(id, code, message):
return {'jsonrpc': '2.0', 'id': id, 'error': {'code': code, 'message': message}}
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
try:
parsed = json.loads(body)
response = handle_request(parsed)
if response is not None:
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response).encode())
else:
self.send_response(204)
self.end_headers()
except json.JSONDecodeError:
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(create_error(None, -32700, 'Parse error')).encode())
def do_GET(self):
self.send_response(405)
self.end_headers()
HTTPServer(('', 8080), Handler).serve_forever()client.py:
import requests, json
def call(method, params):
res = requests.post('http://localhost:8080', json={
'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params
})
return res.json()
print(call('add', [1, 2])) # {'jsonrpc': '2.0', 'result': 3, 'id': 1}
print(call('ping', [])) # {'jsonrpc': '2.0', 'result': 'pong', 'id': 1}
print(call('unknown', [])) # error: Method not found作为进程间通信(IPC)协议
JSON-RPC 不依赖 HTTP,它可以跑在任何双向字节流上。最常见的非 HTTP 场景是子进程通信——父进程拉起一个子进程,通过子进程的 stdin/stdout 交换 JSON-RPC 消息。
这正是 MCP(Model Context Protocol)stdio 传输的底层机制(见 什么是 stdio 那篇文章):
父进程(Host) 子进程(MCP Server)
│ │
│──── JSON-RPC Request ──────▶ │
│ (写 stdin) │
│ │
│◀──── JSON-RPC Response ──── │
│ (读 stdout) │
│ │
│──── JSON-RPC Notification ──▶│
│ (写 stdin,不需要响应) │
│ │在这个模式下,每条消息以 \n 分隔(针对 HTTP 场景则用 Content-Length header)。MCP 的规范文档定义了更加严格的帧格式:Content-Length: <bytes>\r\n\r\n<body>。
这种 stdio + JSON-RPC 的组合是一种非常轻量的 IPC 选择:不需要绑端口、不需要网络配置、不需要额外服务发现,父进程拉起子进程就能通信。
JSON-RPC vs REST vs gRPC
很多人会问:已经有 REST 和 gRPC 了,为什么还需要 JSON-RPC?它们各自适合什么场景?
| 维度 | JSON-RPC | REST | gRPC |
|---|---|---|---|
| 数据格式 | JSON | JSON / XML / 任意 | Protocol Buffers(二进制) |
| 传输层 | 任意(HTTP、TCP、stdio...) | 几乎只有 HTTP | HTTP/2 |
| 接口定义 | 无固定格式,文档约定 | OpenAPI / Swagger 等 | .proto 文件(严格 schema) |
| 操作语义 | 方法调用(method + params) |
资源操作(GET/POST/PUT/DELETE) | 服务方法(Unary / Server Streaming / Client Streaming / Bidirectional Streaming) |
| 流式 | 不原生支持 | 不原生支持 | 原生支持(双向流) |
| 批量调用 | 原生支持(请求数组) | 不原生支持(需要自定义) | 不原生支持(但流式可以替代) |
| 缓存 | 不适用(POST 语义) | 原生 HTTP 缓存(GET 可缓存) | 不适用 |
| 浏览器兼容 | 需要手动处理 | 原生支持 | 需要 gRPC-Web 代理 |
| 人类可读 | 是 | 是 | 否(二进制) |
| 协议体积 | 小(纯文本) | 大(头信息、状态码等) | 小(二进制序列化) |
| 性能 | 中等 | 中等(JSON 序列化开销) | 高(protobuf + HTTP/2) |
什么时候用 JSON-RPC
- 进程内或子进程通信(IPC):JSON-RPC + stdio 是最便捷的选择。MCP、LSP(Language Server Protocol)都用了类似模式。
- 简单的方法调用:如果你的需求就是远程调几个函数,不需要复杂的 REST 资源模型、不需要流式传输,JSON-RPC 可以省掉大量样板代码。
- 服务端和客户端都由同一团队控制:不需要对外暴露 RESTful API 给第三方。
- 性能要求不高,更看重开发效率:JSON 序列化/反序列化对所有语言都是原生支持的。
- 轻量嵌入式场景:物联网、微服务内部轻量通信、浏览器扩展等,JSON-RPC 可以做到非常简洁。
什么时候用 REST
- 公开 API:需要被第三方集成,REST 有最成熟的工具链、缓存、HTTP 状态码约定。
- 资源导向的操作:CRUD(增删改查)天然符合
GET /users/123、POST /users这样的语义。 - 浏览器优先:前端直接
fetch就能用,不需要额外客户端库。 - 需要 HTTP 缓存:
GET请求可以走 CDN、浏览器缓存、反向代理缓存。
什么时候用 gRPC
- 高性能微服务间通信:protobuf 序列化 + HTTP/2 多路复用,延迟和吞吐都优于 JSON。
- 需要流式传输:实时数据推送、大文件分片处理、AI 推理流式输出等。
- 强类型接口要求:
.proto文件编译生成客户端和服务端代码,类型安全。 - 多语言互操作:gRPC 对大部分主流语言的代码生成支持很好。
- 内部服务网格:gRPC 在 Istio / Linkerd 等 Service Mesh 生态中有非常好的支持。
一个实用建议:内部 IPC 用 JSON-RPC,对外 API 用 REST,高性能内部服务间调用用 gRPC。
实践中容易踩的坑
1. id 字段必须唯一
同一个客户端不能同时发两个相同 id 的请求,除非你确保它们不会交叉响应。服务端匹配响应就是靠 id。如果 id 重复,客户端收到第二个响应时根本分不清对应的是哪个请求。
在批量请求中尤其要注意这一点——每个请求的 id 必须不同。
2. 不要用 null 当 id
规范里 id: null 在语义上等同于没有 id,会被当成通知。如果你想要响应,id 必须是非 null 的值(数字或字符串)。
这也是一个常见的混淆点:有人把 id 设成 null 来「省一个字段」,结果服务端不返回响应。id 为 null 就是通知,不返回响应,这是规范原文明确规定的。
3. JSON 数字精度问题
JSON-RPC 基于 JSON,JSON 的数字类型没有整型和浮点型的区分。对于大整数(超过 2^53),JavaScript 和大部分动态语言会丢失精度。
{ "id": 1, "result": 9007199254740993 }在 JavaScript 中解析后得到的是 9007199254740992(丢失了 1)。如果你的 RPC 返回大整数,应该考虑把它们序列化为字符串:
{ "id": 1, "result": "9007199254740993" }4. 通知的沉默是「单行道」
通知是没有响应的,意味着:
- 客户端永远不知道服务端是否处理成功
- 如果服务端处理通知时抛异常,客户端也收不到错误信息
- 不能把「需要知道结果」的操作设计成通知
一个常见错误是把通知用于「用户注册」之类的操作——用户点了注册按钮,前端发一个通知,然后一直等不到注册成功的响应。通知只适合那些「发出去就算完成」的场景。
5. 批量请求的顺序
规范不要求服务端严格按请求顺序返回响应。虽然大部分实现是顺序处理的,但客户端不能依赖这个顺序。唯一正确的匹配方式是通过 id 字段。
6. 传输层协议的差异
刚才说过 JSON-RPC 是传输层无关的,但不同传输层带来的差异需要留意:
- HTTP 传输:客户端通过 HTTP 状态码 200 判断请求已到达,再解 JSON 看是 result 还是 error。但如果网络断开,客户端可能收到一个不完整响应或链接重置。此外,如果所有请求都是通知,HTTP 场景下服务端可以返回 204 No Content。
- stdio 传输:没有 HTTP 状态码这个"外包装",所有错误都必须编码在 JSON-RPC 响应对象里。同时,stdio 模式下 stdout 是唯一的通信通道,任何非协议输出(比如日志打到 stdout)都会破坏消息帧。这块在 什么是 stdio 里讲得很详细了。
- WebSocket 传输:全双工,双方都可以主动发消息。需要注意的是,WebSocket 不会自动处理请求和响应的配对——依然是靠
id字段匹配。服务端也可以主动向客户端发请求(在这种模式下,客户端也扮演了服务端角色)。
7. 安全边界:谁来当服务端
在标准 HTTP JSON-RPC 中,服务端是固定的、客户端发起调用,方向明确。但在双向通信场景(比如 WebSocket 或 MCP),双方都可能发起请求。这时候「服务端」和「客户端」的边界会模糊。
一个实际例子:在 MCP 协议中,MCP Host(比如 Cursor、Claude Desktop)和 MCP Server 之间的通信虽然是基于 JSON-RPC,但 MCP 定义了额外的消息类型(如 notifications/initialized、resources/list),以及双向请求的能力。Host 可以向 Server 发起 tools/call,Server 也可以通过 sampling/createMessage 向 Host 请求模型生成结果。
这种双向能力是在 JSON-RPC 基础之上扩展的,但「请求必须有 id」和「通知没有 id」的规则依然适用。
JSON-RPC 的生态
JSON-RPC 虽然协议很简单,但各种语言的实现库相当成熟。以下是一些常用库:
- JavaScript/TypeScript:
jsonrpc-lite、jayson(支持 HTTP/TCP/WebSocket/TLS/Unix Socket) - Python:
jsonrpcserver(服务端)、jsonrpcclient(客户端) - Go:
gorilla/rpc、gojsonrpc - Java:
jsonrpc4j - Rust:
jsonrpc-core、jsonrpsee - PHP:
json-rpc/json-rpc-php
不过说实话,对于简单的场景,自己手写一个 handler 也就几十行代码 —— 协议本身太简单了,引入一个库有时候比手写还麻烦。
总结
JSON-RPC 是一个小而美的协议。它没有 REST 那样丰富的语义和工具链,也没有 gRPC 那样的高性能和强类型约束,但它的简洁和传输层无关性让它在很多场景下是恰到好处的选择。
一句话总结:
JSON-RPC 用最少的约定,达到了「远程调用像本地调用一样」的目标。
如果你需要的是:
- 子进程间传递命令和结果
- 内部微服务之间的轻量通信
- 可以用 JSON 描述的一切请求-响应模式
JSON-RPC 值得一试。尤其是最近几年 MCP 等协议的兴起,JSON-RPC 作为底层通信协议正在被越来越多的人接触到。
下一篇预告:如果你想更深入了解 JSON-RPC 在 AI 协议生态中的应用,可以读一下之前写的 什么是 MCP——它的通信层正是基于 JSON-RPC 2.0 构建的。
参考资料
- JSON-RPC 2.0 Specification(官方规范,非常短,推荐直接读)
- JSON-RPC 2.0 淘宝 NPM 镜像(MDN 上的简要介绍)
- JSON-RPC in 100 lines of Python(jsonrpcserver 库)
- MCP Specification: Messages(MCP 对 JSON-RPC 的扩展用法)