返回首页

什么是 JSON-RPC:轻量级远程调用协议

JSON-RPC 是一个轻量级的远程过程调用(RPC)协议,使用 JSON 作为数据格式。本文从 RPC 的基本概念讲起,介绍 JSON-RPC 的请求/响应格式、通知(Notification)、批量调用、传输层无关性等核心特性,并对比 REST 和 gRPC 说明各自的适用场景。文章还包含 Node.js 和 Python 的完整代码示例,以及实践中容易踩的坑(id 字段、数字精度、通知无响应等)。

30 约 13 分钟 · 18056 字 AI

说远程调用协议,大部分人先想到的是 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 共存

关键约束: resulterror 是互斥的,一个响应里只能有一个。

错误对象

成员 类型 必须 说明
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 found

Python 版本

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/123POST /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/initializedresources/list),以及双向请求的能力。Host 可以向 Server 发起 tools/call,Server 也可以通过 sampling/createMessage 向 Host 请求模型生成结果。

这种双向能力是在 JSON-RPC 基础之上扩展的,但「请求必须有 id」和「通知没有 id」的规则依然适用。

JSON-RPC 的生态

JSON-RPC 虽然协议很简单,但各种语言的实现库相当成熟。以下是一些常用库:

  • JavaScript/TypeScriptjsonrpc-litejayson(支持 HTTP/TCP/WebSocket/TLS/Unix Socket)
  • Pythonjsonrpcserver(服务端)、jsonrpcclient(客户端)
  • Gogorilla/rpcgojsonrpc
  • Javajsonrpc4j
  • Rustjsonrpc-corejsonrpsee
  • PHPjson-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 构建的。

参考资料