简介一份面向 Java Web 开发者的 JSON-RPC 入门案例精选 jsonrpc4j 及配套依赖解决从接口调用到工程部署的实践缺口适合初中级后端工程师快速掌握远程过程调用。资源包共 35 个文件、18.86MB以 25 个 jar 库为主体涵盖 Spring、Jackson、jsonrpc4j 等核心组件同时包含 5 个编译后 class、2 个 xml 配置、1 个 jsp 页面和 properties 元信息并附有 META-INF 下的 MANIFEST.MF 元数据可借 WEB-INF 与 META-INF 目录快速理清 Web 应用结构。案例通过 index.jsp 构建客户端请求配合 web.xml 路由到服务端处理逻辑完整演示 JSON-RPC 2.0 标准中 jsonrpc、method、params、id 字段的组装与解析并展示成功返回 result 与异常返回 error 的响应结构兼顾错误处理机制帮助理解 HTTP 传输层下的远程调用语义。已吸引 223 人学习下载读者可依托该工程在 Tomcat 等常见容器中直接部署验证从请求构造、服务端方法映射到结果回传形成完整闭环是理论与实践衔接的高效入门材料。1. jsonRPC 到底能解决什么问题先看清协议边界再动手干过前后端联调的人都有这种体会REST 接口看着规范实际对接时字段对齐、错误码约定、超时重试全靠嘴上对最后查问题查到最后发现是两边对参数的理解不一样。jsonRPC 就是来治这个病的它把请求和响应的格式固定死服务端只认一个入口客户端按模板填字段出错时错误码是协议级的不再各自发明一套。适合的场景很明确内部服务间调用、前端到网关的轻量透传、多端统一接口契约。不适合大文件传输和复杂查询条件组合。这套资源包就是围绕 JSON-RPC 2.0 做的一套可运行样例解压后能看协议文档、能跑服务端、能调客户端主打一个「照着改就能用」。2. 协议规范与资源包结构JSON-RPC 2.0 的四个字段先吃透2.1 请求对象的四个字段一个都不能含糊JSON-RPC 2.0 的请求就是一个 JSON 对象真正决定协议语义的只有四个字段jsonrpc、method、params、id。jsonrpc固定写成2.0这是版本号协议规定必须存在且必须精确等于这个字符串大小写都不能错。method是方法名字符串规范保留rpc.开头的方法名给内部协议用客户端别用服务端见到这类方法应该直接返回Method not found。params最容易被误解它可以是数组也可以是对象。数组对应按位置传参对象对应按关键字传参两者语义完全不同。服务端拿到params后要先判断类型再决定怎么调用我在后面的服务端代码里会给出两种分支。id是请求的唯一标识规范允许 string、number 或 null但不能没有。客户端用 id 来匹配响应哪个 id 回来了就知道是哪个请求的结果。下面是一份标准请求样例{jsonrpc: 2.0, method: add, params: [2, 3], id: 1}逻辑上这个请求告诉服务端三件事协议版本是 2.0、要调用add方法、参数是2和3。id为 1 表示这是我的第 1 个调用。服务端必须用相同的 id 返回结果。注意两个细节params可以省略但省略后服务端要按空对象处理id如果不存在这就不再是普通请求而是通知下一节讲。2.2 响应结构、错误码与通知语义响应对象同样是一个 JSON 对象包含jsonrpc、result或error、id。result和error不能同时出现也不能同时缺席。error是错误对象固定有三个字段的位置code数字、message字符串、data可选。错误码是协议级约定的客户端可以放心用这些码做分支判断。错误码含义说明-32700解析错误请求不是合法 JSON-32600无效请求JSON 合法但对象结构不对-32601方法未找到method 不存在或保留前缀-32602无效参数params 类型不对或参数缺失-32603内部错误服务端执行方法时抛出异常-32000 至 -32099服务端保留自定义错误用这个区间通知请求和普通请求长得一样唯一区别是没有id。既然没有标识服务端就不需要返回任何内容客户端也不该等响应。在批量请求里通知的处理尤其要小心服务端处理完通知后不能产生响应项否则客户端按 id 配对时会直接错位。2.3 资源包里有什么jsonRPC.rar 解压后是文档加源码加调试脚本的组合。protocol.md是协议要点说明server.py是可直接启动的 Flask 服务端client.py是 Python 客户端封装client.js是浏览器端 fetch 调用示例curl_examples.sh是常用的 curl 调试命令集README.md写明了安装依赖、启动顺序和端口配置。这套结构覆盖了「先读懂协议、再跑通服务端、再调客户端」的完整路径比单纯看协议原文省事得多。3. 服务端实现Python Flask 版本与并发参数怎么设3.1 为什么选 Flask 而不是自己写 TCP 服务JSON-RPC 本质上只是应用层协议底层用 HTTP 传输最常见也最省事。HTTP 帮你解决了连接管理、超时、代理、负载均衡这些基础设施问题自己写 TCP 服务端还得处理粘包、半包、心跳得不偿失。在 Python 环境里我一般直接选 Flask因为路由就是那个入口app.post一行就能把所有 JSON-RPC 请求收进来请求体解析、响应序列化都现成。如果你在资源包里看到threadedTrue这个参数它的含义是每个请求开一个线程处理。JSON-RPC 方法通常是短耗时操作线程模式够用如果你的方法里有大量 IO 等待可以考虑换成异步框架但协议部分不用动协议和传输是解耦的。这里先给资源包里的服务端核心代码import json from flask import Flask, request, Response app Flask(__name__) METHODS {} def rpc(nameNone): def decorator(fn): METHODS[name or fn.__name__] fn return fn return decorator rpc(add) def add(a, b): return a b rpc(get_user) def get_user(user_id): # 实际项目里这里查数据库或缓存 return {id: user_id, name: user_ str(user_id)} def build_error(code, message, rid): return {jsonrpc: 2.0, error: {code: code, message: message}, id: rid} def handle_single(req): # 非对象请求直接返回无效请求 if not isinstance(req, dict): return build_error(-32600, Invalid Request, None) rid req.get(id) # 没有 id 说明是通知不返回响应 if id not in req: method METHODS.get(req.get(method)) if method is None: return None params req.get(params) or {} try: if isinstance(params, list): method(*params) else: method(**params) except Exception: pass # 通知不反馈错误由服务端日志兜底 return None if req.get(jsonrpc) ! 2.0: return build_error(-32600, Invalid Request, rid) method METHODS.get(req.get(method)) if method is None: return build_error(-32601, Method not found, rid) params req.get(params, {}) try: if isinstance(params, list): result method(*params) else: result method(**params) return {jsonrpc: 2.0, result: result, id: rid} except TypeError: # 参数数量或类型不匹配 return build_error(-32602, Invalid params, rid) except Exception: # 业务逻辑异常统一转协议错误 return build_error(-32603, Internal error, rid) app.post(/rpc) def rpc_endpoint(): raw request.get_data(as_textTrue) try: req json.loads(raw) except ValueError: return Response(json.dumps(build_error(-32700, Parse error, None)), content_typeapplication/json; charsetutf-8) # 批量请求数组里每一项都走单个处理 if isinstance(req, list): result [] for item in req: resp handle_single(item) if resp is not None: result.append(resp) return Response(json.dumps(result), content_typeapplication/json; charsetutf-8) resp handle_single(req) if resp is None: return Response(, status204, content_typeapplication/json; charsetutf-8) return Response(json.dumps(resp), content_typeapplication/json; charsetutf-8) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)这段代码有四个地方值得停下来看。第一rpc(add)装饰器把方法名和函数绑定到METHODS字典后面分发时直接用METHODS.get查表新增一个 RPC 方法只需要加一个rpc函数不用改分发逻辑。第二handle_single先判断id in req这一步把通知和普通请求分开通知分支里异常直接吞掉只在日志里留痕迹因为协议规定通知没有响应可回。第三params的两种形态在调用时分别走*params和**params这个分支写错会让参数莫名其妙传串。第四批量请求的响应数组只收集非 None 的结果通知项被自然过滤客户端返回数组的长度很可能不等于请求数组长度这是协议设计如此。3.2 超时、连接数与消息大小边界服务端参数里最容易忽略的是消息大小限制。Flask 默认不限制 POST 体大小但你开在公网上就等于敞开大门。一般做法是在请求入口前加一层判断request.content_length超过阈值直接返回-32600我习惯把上限设在 1MB够传业务数据又能挡住异常报文。threadedTrue也有边界线程爆炸时文件描述符先扛不住所以生产环境建议在前面加一层 Nginx 限制并发Flask 只负责业务。超时分为两个方向服务端处理超时客户端连接超时。客户端如果设置太短的超时批量请求里一旦有一个慢方法整个调用就被掐断。我一般把客户端超时设为服务端快方法的 3 倍比如快方法平均 200ms客户端超时给 10s给足余量又不至于挂死。3.3 启动顺序与依赖安装资源包的 README 里写的安装方式很简单先装 Flask 和 requests 两个依赖再启动服务端最后开另一个终端跑客户端。服务端启动后控制台会打印监听地址默认是0.0.0.0:5000局域网内其他机器也能连。这里有个坑host0.0.0.0意味着所有网卡都监听如果你在云主机上跑记得在安全组里限制 5000 端口的来源 IP不然任何人都能调你的add方法。4. 客户端调用与调试curl 先行代码跟上4.1 用 curl 验证服务端是不是好的写客户端代码之前我永远先用 curl 打一发。这一步排除掉了所有客户端代码层面的干扰能确认服务端是否真的在按 JSON-RPC 协议响应。下面这条命令请求add方法传数组参数[2, 3]curl -s -X POST http://127.0.0.1:5000/rpc \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:add,params:[2,3],id:1}正常响应是{jsonrpc: 2.0, result: 5, id: 1}。注意id回显成 1这是协议要求客户端靠它匹配请求。如果返回的是 HTTP 200 但没有result字段多半是服务端代码里返回结构写错了去查handle_single的返回语句。如果 curl 命令本身报Failed to connect先确认 Flask 进程还活着再确认端口。curl 调试时建议把所有错误码都打一遍传一个畸形 JSON、传一个不存在的方法名、传一个参数缺省的调用把响应收集起来作为客户端联调时的对照表。这套对照表放进资源包正好以后新同事接手时不用踩坑直接照着 curl 命令改。4.2 Python 客户端封装id 自增与超时控制客户端封装的核心是两件事自动生成 id把错误转成异常抛给业务层。我写的JsonRpcClient类里id 用实例变量自增每次调用加一这样同一个客户端实例内不会重复。业务代码调用时只管传方法和参数收到的是已经解包好的result错误码统一转成RuntimeError抛出去业务层用 try 接住判断。import requests class JsonRpcClient: def __init__(self, url, timeout10): self.url url self.timeout timeout self._id 0 def call(self, method, paramsNone): self._id 1 payload { jsonrpc: 2.0, method: method, params: params if params is not None else {}, id: self._id } resp requests.post(self.url, jsonpayload, timeoutself.timeout) resp.raise_for_status() # 通知请求返回 204没有响应体 if resp.status_code 204: return None data resp.json() if error in data: err data[error] raise RuntimeError(f{err[code]}: {err[message]}) return data[result] def notify(self, method, paramsNone): payload {jsonrpc: 2.0, method: method, params: params or {}} requests.post(self.url, jsonpayload, timeoutself.timeout) client JsonRpcClient(http://127.0.0.1:5000/rpc) print(client.call(add, [2, 3])) print(client.call(get_user, {user_id: 9}))这段代码里两个参数值得单独说。timeout10同时覆盖连接和读取两个阶段requests 库如果不传 timeout网络抖动时请求会挂很久不报错这在批量联调时非常难受。params默认传{}而不是None是为了让handle_single里的params req.get(params, {})走 keyword 分支如果你把params设成None服务端拿到的是nullisinstance(params, list)和isinstance(params, dict)都会返回 False结果就是方法被调用了但参数没传进去这个错最难排查。4.3 JavaScript 客户端浏览器里怎么调前端调用 JSON-RPC 最大的区别是没有 requests 这种封装fetch 是标准 API。封装时要注意两点Content-Type必须带application/json对 204 响应不能走response.json()会抛解析异常。下面这一版足够覆盖常规业务async function rpc(method, params {}, id Date.now()) { const response await fetch(/rpc, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ jsonrpc: 2.0, method, params, id }) }); if (response.status 204) return null; const data await response.json(); if (data.error) { throw new Error(${data.error.code}: ${data.error.message}); } return data.result; } // 调用示例 const total await rpc(add, [2, 3]); const user await rpc(get_user, { user_id: 9 });参数说明id Date.now()在单页应用里基本够用但如果同一毫秒内并发多次调用会重复。前端并发请求多的话建议改成闭包内自增计数器。params默认{}和服务端的 keyword 分支是一致的这个默认值建议前后端在项目启动前用文档确认一次因为一旦一端改了参数结构错误码只会是 -32602排查要花不少时间。4.4 调试脚本的推荐路径资源包里的curl_examples.sh不是摆设我建议每次改完服务端代码后先把脚本里五个用例跑一遍正常调用、解析错误、方法不存在、参数错误、通知请求。这五条覆盖了 JSON-RPC 协议最关键的分支跑通了再动客户端。千万不要先写客户端再回头查服务端那样会把协议问题和代码问题混在一起浪费时间。5. 避坑实录id 类型、编码与通知请求的三次翻车5.1 HTTP 500 竟然不是 JSON-RPC 错误现象客户端调用一个故意抛异常的方法期望收到-32603错误对象实际却收到 HTTP 500响应体是一长段 HTML。原因Flask 默认未捕获异常走的是 HTML 错误页不是 JSON-RPC 错误通道。只有把异常捕获到handle_single内部并转成error对象客户端才能按协议解析。解决给app.errorhandler(Exception)加统一兜底返回-32603的 JSON 响应同时检查每个 RPC 方法内部是否还有裸奔的异常没被外层接住。5.2 params 类型不一致导致诡异 TypeError现象客户端用数组传参没问题改成对象传参后同一方法报Invalid params。原因我在服务端代码里对params做了*params和**params两个分支客户端如果混用比如方法定义是def add(a, b)客户端传{a: 1, b: 2}能通传[1, 2]也能通但如果方法定义是def add(a, b0)对象传参缺省时也正常数组传参缺省时反而报错。解决项目启动时约定 params 一律用数组或者一律用对象不要两种混着传。这个约定要写进双方接口文档里代码不会主动提示你。5.3 id 类型不匹配响应和请求永远对不上现象客户端发请求时 id 用数字 1服务端响应也是 1但客户端判断data.id payload.id结果竟然是 false。原因JSON 解析过程中 id 的类型被改变比如服务端把 id 转成了字符串1或者客户端用了id 1而服务端返回的是数字。JavaScript 里1 1是严格不等的。解决全项目统一 id 类型我习惯强制用 number 且从 0 开始自增。资源包的client.py里就是自增数字前端封装里如果用了Date.now()注意它返回的是数字不要手滑加引号。5.4 批量请求带通知响应数组对不齐现象客户端批量发三个请求其中一个不带 id服务端返回了三条响应客户端按序号取下一条结果时拿到的是通知的错误响应。原因服务端处理批量请求时没有过滤通知项把无 id 请求也当成普通请求走了handle_single。解决资源包里的服务端代码已经处理了这一点handle_single对通知返回None批量循环里跳过None项。客户端收到响应数组后不要按下标取结果一定要遍历响应项用id去匹配初始请求。5.5 中文错误信息变成乱码或被转义现象错误信息里写了中文客户端打印出来要么是\uXXXX转义序列要么直接乱码。原因Flask 默认把 JSON 响应的非 ASCII 字符转成\uXXXX某些终端、日志系统不负责还原转义直接把原始序列打出来。另一个并发问题是响应头charset没设置客户端按错误编码解析响应体也会乱码。解决响应头显式写application/json; charsetutf-8Flask 新版用app.json.ensure_ascii False关掉转义旧版本用app.config[JSON_AS_ASCII] False。两个版本我都写在资源包 README 的注释里了。6. 进阶玩法批量请求与通知在真实业务里的落地技巧单个方法调用只是 JSON-RPC 的基础批量请求才是提高效率的关键。比如一个仪表盘页面要同时取用户信息、订单数、未读消息三个接口串行调用要等三倍时间用批量请求一次性发出去服务端并行或顺序处理客户端统一回收整体耗时基本等于最慢的那个方法。资源包里的client.py已经预留了batch_call的扩展点核心逻辑是把多个调用装进一个数组def batch_call(self, calls): payload [] for i, item in enumerate(calls): payload.append({ jsonrpc: 2.0, method: item[method], params: item.get(params, {}), id: i }) resp requests.post(self.url, jsonpayload, timeoutself.timeout) data resp.json() # 返回一个字典按 id 映射结果调用方直接查 return {item[id]: item for item in data if id in item}参数说明calls是{method: ..., params: ...}的列表我从 0 开始给每个调用分配自增 id这样调用方拿到的响应字典键是 0、1、2和calls的下标天然对应。这里刻意绕开了时间戳做 id因为批量调用里多个请求的生成时间有可能落在同一毫秒尤其是前端循环构建批量时id 重复会让整批响应的匹配乱掉。批量请求有一个容易忽视的特性响应数组的顺序不保证与请求数组一致。协议层面允许服务端乱序返回客户端如果按下标取值就翻车了。所以我把响应转成以 id 为 key 的字典调用方用results.get(0)取值不依赖返回顺序。通知的落地场景则完全是另一路子。日志上报、异步任务触发这类操作客户端只负责发出不关心结果用通知可以让调用方的代码更干净。但通知有个代价服务端处理失败时客户端毫无感知所以通知里能带的事件详情要尽量完整服务端日志里必须有方法名、参数摘要、时间戳、来源 IP。资源包的server.py里通知分支吞掉了异常实际项目里我会在except块里加一条logging.exception把失败的调用留痕。使用批量请求和通知时我的习惯是先在压测环境试一遍混合场景一个批量数组里同时混普通请求和通知确认服务端返回项和请求项的对应关系符合预期。资源包里的方案不是唯一但它把 id 匹配、通知过滤、错误码三分这几个坑都铺平了拿来改业务代码时不用重踩。之前我在一个内部系统里把 id 换成时间戳结果并发一上来响应匹配就错乱排查了一天才发现是同一毫秒内生成了两个相同 id。从那以后我每次接 jsonRPC 都强制走一遍标准流程先跑 curl 五个用例、再确认 id 类型统一、最后才写业务逻辑。这套流程写进资源包省得新来的同事再交一遍学费。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?