1. 先说透一件事:JSON 是数据格式,JSON-RPC 是通信协议
做了十几年接口开发,我见过太多人在同一个问题上栽跟头:看到"JSON-RPC"这个名字,就下意识以为它是 JSON 的某种增强版,或者干脆把它当成"远程版的 JSON"。等你真去排查一个 JSON-RPC 接口报错时,才发现自己连"这个报错到底该怪 JSON 还是怪协议"都说不清楚。
这篇文章就把这两兄弟彻底掰开揉碎讲。JSON(JavaScript Object Notation)是一种数据交换格式,管的是"数据长什么样";JSON-RPC 是一种远程过程调用协议,管的是"程序之间怎么互相叫对方干活"。前者是语法,后者是约定,两者根本不在同一个层面。搞懂这一点,后面所有细枝末节的知识点都能像串珠子一样串起来。
1.1 "格式"和"协议"的差别,是一切混乱的根源
先说格式。格式解决的是"一段数据怎么写出来、怎么读进去"的问题。JSON 规定了一整套语法:大括号表示对象、方括号表示数组、冒号分隔键和值、缩进不参与语义、字符串必须用双引号。只要你的数据长成这个样,任何语言、任何平台上符合规范的解析器都能把它读出来。它本质上就是一份"文字的排版规则"。
再说协议。协议解决的是"通信双方怎么对话"的问题。它要管的事情远比语法复杂:谁来发起对话?消息分几种类型?对方不回复怎么办?出错了怎么报告?这些语义层面的约定,语法一概不管。
举一个最直观的例子:{"name": "张三", "age": 18}这段 JSON,它可以是配置文件里的一行、日志系统输出的一条记录、HTTP 接口返回的一个用户对象,也可以是 JSON-RPC 协议里某个请求的参数部分。JSON 本身完全不知道也不关心这段数据拿去干什么。而 JSON-RPC 不一样,它规定了jsonrpc、method、params、id这些字段必须出现、各自代表什么含义、服务端收到后必须怎么响应。这才叫协议。
1.2 为什么命名让人犯迷糊
坦白说,"JSON-RPC"这个名字起得确实容易误导人。它看起来像是"JSON 的 RPC",让人误以为它是 JSON 自带的一个功能模块。实际恰恰相反:JSON-RPC 只是"用 JSON 字符串来做消息编码"的众多种 RPC 协议之一。
打个类似的比方,市面上有 XML-RPC、gRPC、Thrift。gRPC 的默认消息编码是 Protobuf,但你不能说 gRPC 是 Protobuf 的升级版。同样,JSON-RPC 只是把 JSON 当成"信封里的纸",真正的协议内容是写在纸上的那些约定。甚至你在 REST 接口的请求体里塞一段 JSON,这段 JSON 跟 JSON-RPC 半毛钱协议关系都没有——它只是被 REST 借用的一种数据载体。
所以记住这个分层关系就够了:JSON 是承载数据的语法,JSON-RPC 是把这种语法用作远程调用约定的协议。下文所有对比,全部围绕这个核心展开。
2. JSON 的本来面目:一套极简但坑不少的数据语法
要把 JSON 和 JSON-RPC 区别开,前提是先把 JSON 本身吃透。很多人天天在用 JSON,但对它的边界、缺陷和约定俗成的处理方式其实一知半解。下面从语法、用途、实操三个角度把这层说完。
2.1 六种数据类型和语法规则速览
JSON 的数据类型只有六种:对象(object)、数组(array)、字符串(string)、数字(number)、布尔(boolean)和 null。没有日期、没有二进制、没有 undefined,也没有注释。
规范的硬性要求就几条:
- 对象的键必须用双引号包裹,单引号、裸键名一律不合法(虽然很多解析器容忍,但规范不支持)。
- 字符串值必须用双引号,不能出现未经转义的控制字符。
- 数字不支持前导零、不支持
NaN和Infinity、不支持十六进制写法。 - 顶层可以是任意类型的值,实践中绝大多数是对象。
这些规则看起来简单,但恰恰是这份"极简"让 JSON 拥有了跨语言、跨平台的生命力。Python 的字典、JavaScript 的对象、Java 的 HashMap、Go 的 struct 通过 JSON 序列化后,格式完全统一。前端拿到的数据可以原样传给后端,后端处理完再原样返回,中间不需要任何语言绑定的转换逻辑。
2.2 JSON 解决的核心问题
JSON 在 2001 年由 Douglas Crockford 提出,当时 XML 正如日中天,但 XML 的标签冗余、解析繁琐让数据交换变得很重。JSON 用更小的体积、更接近编程语言原生结构的表达方式,迅速成为 Web 世界的通用语。
在实际项目里,JSON 承担的角色大致有这么几类:
- 配置文件:package.json、tsconfig.json、各种工具的配置都是 JSON。这里有个小知识,JSON 不支持注释,导致很多人写配置文件时非常痛苦,于是衍生出 JSONC(允许注释的 JSON)、JSON5 等变体,或者干脆投奔 YAML。你要是维护一个内部工具,我建议直接选 JSONC 或者 YAML,别跟注释较劲。
- 日志输出:结构化日志几乎默认用 JSON。每条日志是一行 JSON,字段如
time、level、msg、trace_id,查询时用 jq 这类命令行工具按字段过滤,比 grep 正则匹配舒服一个量级。这也就是大家常搜的"json查询函数"——本质上不是函数,是 jq 之类的查询工具。 - 数据存储:MongoDB 的 BSON、PostgreSQL 的 jsonb 字段、Elasticsearch 的文档模型,本质都是 JSON 家族的存储形态。Kettle 这类 ETL 工具解析 JSON、转 JSON,也是家常便饭。
- 接口传输:REST API 的请求体和响应体,消息队列里的消息体,WebSocket 推送的帧,绝大多数都直接使用 JSON。
2.3 用 JSON 时绕不开的几个现实问题
正因为 JSON 只解决了"怎么写"的问题,很多"语义"就得靠约定补齐,这就埋下了各种坑。
注释问题。前面说了,规范不支持注释。我见过强行用//写在 JSON 文件里的配置,结果被严格解析器拒之门外。处理方式要么用 JSONC,要么在字段命名上做文章,比如_comment。
日期问题。JSON 没有 Date 类型,所以日期要么转成 ISO 8601 字符串(2024-06-01T12:00:00Z),要么转成 Unix 时间戳。这两种方案各有拥趸,最怕的是双方没有约定好。热搜里那个经典的报错JSON parse error: cannot deserialize value of type java.util.Date from String,十有八九就是客户端传了个"2024-06-01"这种不含时间部分的字符串,而服务端 Jackson 默认期望的格式是"2024-06-01T00:00:00.000+0000"。这不是 JSON 语法的错,是数据契约的错。
大数字精度问题。JavaScript 的 Number 安全整数上限是 2 的 53 次方。你要是用 JavaScript 解析一个超过这个上限的整数 ID——比如雪花算法生成的 19 位 ID——精度直接丢失。解决办法是约定:超过安全范围的整数一律用字符串传输。
键的顺序问题。规范明确对象键无序,但实际绝大多数解析器(包括 V8、Python 3.7+ 的 dict)保留插入顺序。有些人依赖这个特性做"有序 JSON",一旦换了解析器就可能出问题。正经做法是:需要顺序就放到数组里。
顺带提一句"json用什么打开"这类高频搜索——JSON 是纯文本,记事本、VS Code、Sublime 都可以打开。VS Code 里装个 JSON Tools 插件能格式化、压缩、排序键,比在线工具更顺手。离线的格式化需求用 jq、python -m json.tool 都能搞定,没必要非得连网。
3. JSON-RPC 协议到底规定了什么:一个信封、四个字段、三类消息
聊完了 JSON 这层"语法",接下来进入正题:JSON-RPC 作为协议,它的设计到底是怎么样的。理解了它的每个字段和每条规则,你就能明白它和 JSON 的关系是"用语法承载协议约定"。
3.1 为什么需要 RPC:程序之间互相"调函数"
先追溯一下 RPC 的动机。REST 的思维是面向资源的:你有用户、订单、商品这些"东西",通过 GET/POST/PUT/DELETE 去操作它们。但很多场景你想做的不是操作资源,而是"执行一个动作"——比如"帮我把这两笔账合并""把这个文本翻译成英文"。这种动作很难被优雅地映射成资源,强行映射就会造出/api/merge-accounts这种视觉噪音接口。
RPC 的思路完全不同:客户端把远程服务当成一个普通对象,调用它的方法、传参数、拿返回值。JSON-RPC 就是把"方法名 + 参数"用 JSON 编码,通过网络传给服务端,服务端找到对应函数执行完,再把返回值用 JSON 编码传回来。整个过程对客户端来说,跟调用本地函数没有本质区别。
3.2 请求、响应、通知的消息结构
JSON-RPC 2.0 规范很短,核心消息结构就四种:请求(Request)、响应(Success Response)、错误响应(Error Response)、通知(Notification)。
请求对象必须有三个字段:
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}jsonrpc:固定字符串"2.0",表示协议版本。method:要调用的方法名,字符串。params:参数。可以是数组(位置参数),也可以是对象(命名参数)。可选项。id:请求标识,用于把响应和请求对应起来。可以是字符串、数字或 null,但不应是 null(因为 null 被用于通知的判定)。这个字段是协议里最容易被忽视又最要命的设计。
响应对象分两种。成功时:
{"jsonrpc": "2.0", "result": 19, "id": 1}失败时:
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": "1"}注意result和error是互斥的,成功响应绝不能带error,错误响应绝不能带result。
通知是不带id的请求:
{"jsonrpc": "2.0", "method": "update", "params": [1, 2, 3]}服务端收到通知后必须执行方法,但不返回任何响应。这个设计的价值在于:客户端只想触发一个动作、不在乎结果时(比如日志上报、心跳、缓存预热),可以减少一次往返。代价是客户端永远无法知道通知是否成功——所以通知只适合那些"丢了也无所谓"的操作。
3.3 命名参数与批量调用
位置参数和命名参数各有适用场景。位置参数简洁,但调用方必须严格按顺序传值,方法签名一变,所有调用方都得跟着改。命名参数用对象传参,多了字段名做"自描述",代码可读性和可维护性更好,缺点是多写几个字符。我的习惯是:方法参数超过两个就强制走命名参数。
批量调用是 JSON-RPC 2.0 引入的重要能力。一次请求可以携带一个数组:
[ {"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"} ]服务端收到数组后,逐条处理,返回一个响应数组。这里有几个规则容易踩坑:
- 服务端可以按任意顺序处理批量请求,响应数组的顺序不要求和请求数组一致,客户端必须根据
id来匹配。 - 如果批量请求里全是通知,服务端不应返回任何内容(连空数组都不返回)。
- 空数组
[]本身是无效请求,必须返回错误。 - 批量请求里某个元素出错,不影响其他元素正常处理——服务端要把错误结果也放进响应数组。
我见过不少团队在批量场景里踩的坑:客户端发了 10 个请求,其中混了一个通知,结果客户端傻等响应数组等了 10 个元素,实际上服务端只返回了 9 个。解决方法是养成习惯:批量请求要么全是可响应请求,要么全是通知,别混着来。
3.4 协议级错误码:错误也要有"共同语言"
JSON-RPC 2.0 定义了七个协议级错误码,这是 JSON 绝对给不了的东西,也是体现"协议"身份的核心。
| 错误码 | 含义 | 说明 |
|---|---|---|
| -32700 | 解析错误(Parse error) | 服务端收到的消息不是合法 JSON |
| -32600 | 无效请求(Invalid Request) | JSON 合法,但不符合请求对象结构,比如缺method |
| -32601 | 方法不存在(Method not found) | 服务端没有注册这个方法 |
| -32602 | 参数无效(Invalid params) | 参数类型、数量不匹配方法签名 |
| -32603 | 内部错误(Internal error) | 服务端执行方法时抛了未捕获异常 |
| -32000 到 -32099 | 服务端错误(Server error) | 保留给服务端自定义的错误范围 |
| 其他 | 应用级错误 | 这是协议允许的,但要用正数或不在保留范围内的代码,留给业务层定义 |
注意一个细节:error对象除了code和message,还有个可选字段data,用来携带堆栈、调试信息等。我建议服务端在data里塞一个内部错误跟踪 ID,客户端报错时把 ID 贴过来,排查效率能提升一个量级。
4. 一张表看穿 JSON 与 JSON-RPC 的本质差异
把前面几节的内容浓缩成一张对照表,是理解两者差异最快的方式。建议收藏,遇到混淆时翻出来看一眼。
| 对比维度 | JSON | JSON-RPC |
|---|---|---|
| 本质 | 数据交换格式(语法) | 远程过程调用协议(约定) |
| 规范层级 | 表示层 | 应用层 |
| 是否定义请求/响应 | 否 | 是(请求、响应、错误、通知) |
| 是否定义错误码 | 否 | 是(-32700 到 -32099 等) |
| 是否关心"方法"概念 | 否 | 是(method字段) |
| 能否独立完成一次远程调用 | 不能,只是个载体 | 能,自身就是完整协议 |
| 消息结构要求 | 只要符合语法即可 | 必须符合jsonrpc/method/params/id约定 |
| 跨平台性 | 极强,几乎一切语言可解析 | 依赖 JSON 编码,传播范围受实现影响 |
| 典型用途 | 配置、日志、存储、传输 | 区块链节点接口、IDE 语言服务、内部 RPC |
4.1 逐项拆解:别被"都叫 JSON"带偏
第一眼看这张表,最扎眼的一行是"能否独立完成一次远程调用"。一段合法的 JSON 放在那儿,它顶多是一份数据;但一个合法的 JSON-RPC 请求对象放在那儿,它就是一次"待执行的方法调用"的完整描述。服务端拿到它知道该找哪个函数、传什么参数、把结果往哪回。这是本质差异。
再看"消息结构要求"这一行。JSON 只要求你写的每个键值对语法正确,至于method是什么意思、id该不该回显,JSON 一概不管。JSON-RPC 则把这些字段的含义直接焊死在协议里。你可以写一段完全合法的 JSON 但它不是合法的 JSON-RPC 请求;反过来,任何合法的 JSON-RPC 请求必然是合法的 JSON。这就是"协议建立在格式之上、但远大于格式"的最好证明。
4.2 "语法"和"语义"的分工,决定了两者的应用边界
从工程角度看,JSON 天然适合"静态的、无状态的数据搬运"——我把数据从那台机器挪到这台机器,中途不需要任何约定,双方只要都认识 JSON 就行。而 JSON-RPC 天然适合"动态的、有语义的远程协作"——我需要你执行某个动作、返回某个结果、报告某个错误。
打个比方,JSON 是英语这门语言的词汇和语法,JSON-RPC 是开会时的会话脚本:先有主持人点名(id)、陈述议题(method)、提供材料(params)、得到结论或驳回意见(result/error)。你学会英语不代表你就会开会;你写出一段规范的 JSON,不代表客户端和服务端就能对上话。这个比喻我每次在团队内部分享时,都说得很直白:JSON 解决的是"怎么写字",JSON-RPC 解决的是"对话怎么进行"。
5. 实战选型:三种姿势,别用错地方
理解了理论差异之后,更要紧的问题是:我手上这个项目到底该用哪种?结合我多年的接口设计经验,给你一套可以直接抄的选型参考。
5.1 只用 JSON 就够的场景
很多场景根本不需要协议层的语义,引入 JSON-RPC 反而是过度设计。
- 配置文件:package.json、tsconfig.json、ESLint 配置,纯静态数据,用 JSON。
- 结构化日志:日志是"写出去"的单向数据,没有请求-响应概念,用 JSON。
- 数据存储交换:数据库导入导出、消息队列的消息体、ETL 工具(比如 Kettle 解析 JSON、转 JSON)处理的数据,用 JSON。
- REST API 的请求和响应体:REST 本身就是协议层,HTTP 方法、状态码、URL 已经承担了语义,body 里的 JSON 只是数据载体,用 JSON。
在这些场景里,"JSON-RPC"这个名字甚至都不该出现在你的技术方案里。
5.2 JSON-RPC 的典型战场:三个不得不提的实例
如果你对 JSON-RPC 的实际应用没有体感,看下面三个例子就够了。
公链节点接口。以太坊等区块链节点对外暴露的 API 就是 JSON-RPC,比如eth_getBalance、eth_sendTransaction。这类接口的特点是"动作密集"——调用方不是要操作资源,而是要节点执行一堆链上操作,参数多样且语义复杂,和 RPC 的模型高度契合。几乎所有主流 Web3 工具库都在跟 JSON-RPC 打交道。
IDE 的语言服务协议 LSP。VS Code 的智能提示、跳转定义、错误诊断,走的就是 LSP,而 LSP 的消息层正是 JSON-RPC 2.0。编辑器把textDocument/hover这类方法发给语言服务器,语言服务器返回提示内容。这个场景的巧妙之处在于传输层可以是标准输入输出、TCP 或 WebSocket,协议本身与传输解耦,非常灵活。
模型上下文协议 MCP。这几年大模型应用火起来以后,MCP 成了让大模型调用外部工具的热门方案,它的消息格式也明显带有 JSON-RPC 的风格。这说明 JSON-RPC 这套约定在"请求-响应-通知"的模型下生命力依然很强。
5.3 REST 和 JSON-RPC 怎么选:给出一套决策标准
很多人在 REST 和 JSON-RPC 之间纠结,我给一个相对清醒的视角。
| 决策维度 | 倾向 REST | 倾向 JSON-RPC |
|---|---|---|
| 建模方式 | 资源导向(用户、订单) | 动作导向(合并、翻译) |
| HTTP 语义利用 | 充分(GET/POST/状态码/缓存) | 基本不关心,只当传输通道 |
| 对外公开 | 推荐 | 不太推荐 |
| 内部服务 | 可以,但动作接口难设计 | 推荐 |
| 长连接/双向通信 | 需额外方案 | 天然适合配 WebSocket |
| 工具生态 | 极丰富(Postman、OpenAPI) | 较少,需要自建 |
| 学习成本 | 中 | 低(协议极简) |
我的个人经验是:对外的开放 API 一律优先 REST。原因很简单,外部开发者的心智模型是"资源 + HTTP 动词",REST 生态下的文档工具、SDK 生成、缓存策略都成熟得多。而内部服务之间、尤其是动作密集型调用,JSON-RPC 能让代码结构非常清爽,客户端像调本地函数一样调远程方法,省掉一堆"造 REST 动作端点"的尴尬。
6. 排错实录:我踩过的 JSON 与 JSON-RPC 的坑
下面这部分全是真金白银的实战经验。两类问题我分开讲:一类是纯 JSON 数据层的,另一类是 JSON-RPC 协议层的。排查链路完全不同。
6.1 "JSON parse error" 类报错的完整排查链路
热搜里那个高频报错JSON parse error: cannot deserialize value of type java.util.Date from String,我至少帮人排查过十几次。这类报错最迷惑人的地方在于:它叫 parse error,但 JSON 字符串本身语法完全合法。问题的根子在"数据契约不匹配"。
完整排查链路是这样的:
- 先确认消息本身是不是合法 JSON。把报错消息里的原始 JSON 复制出来,丢进格式化工具或
python -m json.tool验证。如果这里就挂了,那是纯语法问题;如果没问题,跳到下一步。 - 看字段类型是否与服务端 schema 一致。报错说 "cannot deserialize value of type java.util.Date from String",说明服务端期望 Date,但收到的是字符串。此时要看字符串格式。比如服务端是 Jackson 默认配置,期望的可能是
"2024-06-01T00:00:00.000+00:00",你传"2024-06-01"就不行。 - 检查服务端反序列化配置。全局搜一下项目里有没有
@JsonFormat、Jackson 的ObjectMapper是否配置了setDateFormat、是否注册了 JavaTimeModule。很多项目对日期格式的处理是隐式的,不查不知道。 - 统一契约。我后来在团队里定了一条死规矩:跨服务传输日期一律用 ISO 8601 字符串,带时区,不许用时间戳、不许用自定义格式。这条规矩省掉了无穷无尽的扯皮。
6.2 JSON-RPC 协议层的四个常见坑
坑一:id类型不一致。客户端发id: 1(数字),服务端返回id: "1"(字符串)。异步场景下,客户端拿响应对照请求数组,全部匹配失败。这个坑几乎都是因为客户端和服务端用了不同语言的 JSON 库,数字和字符串的序列化行为不一致导致的。排查时就盯一个点:id 的往返一致性。
坑二:Content-Type 不对。JSON-RPC 规范对 HTTP 传输的 Content-Type 没有强制规定,但实际服务端实现普遍要求application/json。我踩过用text/plain发请求导致服务端直接拒收的案例。所以统一用application/json,别整幺蛾子。
坑三:通知当请求,傻等超时。前面说了,通知没有id,服务端不响应。很多新手写客户端时发了个不带id的请求,然后同步阻塞等待响应,直到超时。排查时先看请求里有没有id:没有的话,按协议你本来就不该等响应。
坑四:批量请求和通知混用。客户端发了一个数组,里面既有普通请求又有通知,期望服务端回同样长度的数组。但服务端按规范只对带id的元素返回结果,最终就出现"返回数量对不上"的诡异问题。这个坑的教训就是:写批量逻辑时,对空响应做兜底,别假设返回数组长度一定等于请求数组长度。
6.3 拿到一个 JSON-RPC 报错,三步定位法
我在团队里带新人时经常讲这套排查三步法:
- 剥外壳:先把原始消息丢进 JSON 校验器。如果 JSON 本身挂了,恭喜,问题最小,是序列化层的问题。
- 查信封:看
jsonrpc是不是"2.0",method在不在,id有没有、类型对不对。信封错了,服务端直接给你返回-32600 Invalid Request,这时候别去查业务代码,先查调用方代码。 - 看内容:如果信封没问题,看返回的
error.code。-32601就去查方法注册列表;-32602就去比对参数类型和数量;-32603就去服务端翻堆栈;-32700说明客户端发来的根本不是合法 JSON,回到第一步。
这三步走完,90% 的 JSON-RPC 报错能在五分钟内定位。剩下的 10% 基本是框架层、网络层或自定义错误码的问题,那就该抓包看原始字节流了。
7. 手写一个 20 行的 JSON-RPC 核心分发器
理论说再多不如动手。下面我用 Python 写一个最小但完整的 JSON-RPC 2.0 核心分发器,不依赖任何第三方库。它演示了方法查找、参数绑定、异常转错误码、通知处理这些核心逻辑。你把这套逻辑理解透了,对接任何语言、任何框架的 JSON-RPC 服务都不会慌。
7.1 核心代码
import json import traceback INVALID_REQUEST = -32600 METHOD_NOT_FOUND = -32601 INVALID_PARAMS = -32602 INTERNAL_ERROR = -32603 def make_error(code, message, data=None): err = {"code": code, "message": message} if data is not None: err["data"] = data return err def dispatch(raw_message, methods): # 第 1 层:剥外壳,处理 JSON 解析错误 try: req = json.loads(raw_message) if isinstance(raw_message, str) else raw_message except json.JSONDecodeError as e: return {"jsonrpc": "2.0", "error": make_error(-32700, "Parse error", str(e)), "id": None} # 第 2 层:查信封,处理无效请求 if not isinstance(req, dict) or req.get("jsonrpc") != "2.0" or "method" not in req: return {"jsonrpc": "2.0", "error": make_error(INVALID_REQUEST, "Invalid Request"), "id": req.get("id") if isinstance(req, dict) else None} method = req["method"] params = req.get("params", []) rid = req.get("id") # 通知:没有 id,执行但不返回响应 if rid is None: handler = methods.get(method) if handler: try: handler(params) except Exception: pass # 通知没有响应通道,异常只能吞掉或记日志 return None # 第 3 层:看内容,查找方法并执行 handler = methods.get(method) if handler is None: return {"jsonrpc": "2.0", "error": make_error(METHOD_NOT_FOUND, f"Method not found: {method}"), "id": rid} try: if isinstance(params, dict): result = handler(**params) # 命名参数 elif isinstance(params, list): result = handler(*params) # 位置参数 else: result = handler(params) return {"jsonrpc": "2.0", "result": result, "id": rid} except TypeError as e: return {"jsonrpc": "2.0", "error": make_error(INVALID_PARAMS, str(e)), "id": rid} except Exception: traceback.print_exc() return {"jsonrpc": "2.0", "error": make_error(INTERNAL_ERROR, "Internal error"), "id": rid}这段代码的三个分层和上一节的排查三步法一一对应:先解析 JSON、再校验信封、最后执行并转换异常。逻辑非常清晰。
7.2 测试各种消息形态
注册几个测试方法,然后看各种输入怎么被处理:
methods = { "add": lambda *a: sum(a), "concat": lambda prefix, suffix: f"{prefix}-{suffix}", } # 位置参数请求 print(dispatch('{"jsonrpc": "2.0", "method": "add", "params": [2, 3], "id": 1}')) # 输出: {'jsonrpc': '2.0', 'result': 5, 'id': 1} # 命名参数请求 print(dispatch('{"jsonrpc": "2.0", "method": "concat", "params": {"prefix": "api", "suffix": "v1"}, "id": "abc"}')) # 输出: {'jsonrpc': '2.0', 'result': 'api-v1', 'id': 'abc'} # 通知:没有 id print(dispatch('{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}')) # 输出: None(服务端不返回任何响应) # 方法不存在 print(dispatch('{"jsonrpc": "2.0", "method": "nope", "params": [], "id": 2}')) # 输出: {'jsonrpc': '2.0', 'error': {'code': -32601, 'message': 'Method not found: nope'}, 'id': 2} # 参数类型不匹配 print(dispatch('{"jsonrpc": "2.0", "method": "add", "params": "abc", "id": 3}')) # 输出: {'jsonrpc': '2.0', 'error': {'code': -32602, 'message': ...}, 'id': 3} # 非法 JSON print(dispatch('{"jsonrpc": "2.0", "method": "add", ')) # 输出: {'jsonrpc': '2.0', 'error': {'code': -32700, 'message': 'Parse error'}, 'id': None}7.3 接上 HTTP 就能对外服务
这个分发器本身不关心传输层。你要同时支持位置参数和命名参数,要自行在方法实现里约束类型,要对接 WebSocket 做长连接推送,只把这层 dispatch 挂在 WebSocket 的 message 事件上,请求-响应模型天然成立。我之前在一个内部监控系统里就这么干过:客户端通过 WebSocket 连接,实时上报数据(通知),服务端推送告警(通知),需要查询时发带id的请求,通信模型简洁得让人想哭。
如果以后要做生产级服务,建议在此基础上补几件事:批量请求支持、方法注册表用装饰器管理、data字段里带上错误跟踪 ID、请求上下文透传链路追踪信息。核心的分层逻辑不用动,这 20 行就是你理解一切 JSON-RPC 实现的钥匙。
我自己在踩完这一圈坑之后最大的体会是:凡是带 "协议" 两个字的东西,你一定要先分清它管的是"语义"还是"语法"。JSON 管语法,JSON-RPC 管语义,搞混了这个边界,你会连报错都看不懂。先把这篇文章里那个表格记牢,再回去看你手头那些接口文档,很多困惑当场就能解开。