简介jsonRPC.rar是一份面向Java Web开发者的JSON-RPC入门案例资源包集中演示了轻量级远程调用协议的基本原理与实现方式解决分布式服务通信及前后端API交互场景中的实际搭建问题。资源共35个文件包含25个jar依赖库如jsonrpc4j、Spring、Jackson等、5个class类文件、2个xml配置、1个jsp页面与1个properties配置文件压缩包大小18.86MB结构上涵盖WEB-INF、META-INF及源码目录。已有223人学习下载。通过内置的index.jsp入口和web.xml配置可以了解客户端如何构造JSON-RPC请求、服务器端如何解析并响应掌握jsonrpc4j库的API调用与Tomcat部署方法。同时Spring及Jackson相关jar包为处理JSON序列化和依赖注入提供了完整支撑适合希望快速上手JSON-RPC并理解其错误处理机制的初中级开发者。1. 与 jsonRPC 打交道的第一天与其到处找轮子,不如先把这套协议跑通做内部系统对接时,我常遇到一种尴尬客户端要求调用方按 REST 语义传参,可服务端内部逻辑却是按方法名映射的,接口文档写了一大串,两边对齐成本极高。后来在一次微服务改造中,某开发者丢给我一个 jsonRPC.rar,说“别自己造协议了,直接用这个”。解压后里面是完整的 JSON-RPC 2.0 客户端与服务端样例、消息构造器和一批带注释的测试用例。我问自己这到底能省多少事答案是用它把一个老旧的 TCP 长连接服务改成统一 RPC 入口,只花了一个下午。这套资源更适合两类人一类是被跨语言接口折磨的后端开发,另一类是刚接触 RPC 但不想读一厚本规范文档的初学者。它能解决的核心问题只有一个让方法调用像本地函数一样简单,同时把传输细节和序列化规则一次性定死。2. 拆 jsonRPC.rar先把协议骨架和包内文件对齐2.1 JSON-RPC 2.0 规范要点请求、响应、通知与错误码JSON-RPC 2.0 是一种无状态、轻量级的远程过程调用协议,传输层不绑定具体协议,你可以把它架在 TCP、HTTP、WebSocket 甚至 Unix Socket 上。它比 REST 更贴近“函数调用”语义客户端发送一个 JSON 对象,服务端返回一个 JSON 对象,两者通过 id 字段对齐。请求对象的骨架长这样{ jsonrpc: 2.0, method: user.getProfile, params: {uid: 1024, fields: [name, avatar]}, id: 1 }其中的字段作用分别是jsonrpc固定为“2.0”,表示协议版本,服务端校验时不匹配会返回 -32600 错误。method字符串,服务端路由分发时按这个字段找到对应的处理函数。params结构化的参数,可以是对象也可以是数组,推荐用对象,因为可读性和扩展性更好,后面加字段不会破坏已有调用。id请求标识,服务端响应时必须原样带回,客户端用它把响应和请求对应起来。响应对象有两种形态成功时返回 result,失败时返回 error。error 对象里有 code、message 和 data 三个字段,规范定义了五个保留错误码。错误码含义典型触发场景-32700解析错误收到的 JSON 文本无法被解析-32600无效请求请求对象不是合法 JSON-RPC 结构-32601方法不存在服务端未注册该 method-32602参数无效参数缺失或类型不匹配-32603内部错误处理函数抛出未捕获异常除此之外,规范允许自定义错误码,只要大于 -32000 即可。很多实际项目会把业务错误直接映射到自定义码上,比如 -32001 表示“用户不存在”,-32002 表示“权限不足”。“通知”是 JSON-RPC 2.0 里比较容易被忽略的特性。通知请求不包含 id,客户端不期望收到任何响应,常用于日志上报、心跳、缓存预热这类“发出去就完事”的场景。服务端处理通知时即使出错也不能返回错误,只能默默记录日志。2.2 压缩包内常见模块传输层、消息构造器、调用分发器我发现 jsonRPC.rar 里虽然文件不少,但逻辑分层很干净,没有把协议处理和业务代码混在一起。按常见结构拆分,包含这几类模块。模块文件典型命名职责协议层rpc_protocol.py构造 Request/Response/Error 对象,处理序列化分发器rpc_dispatcher.py维护 method 到处理函数的映射,完成路由传输层tcp_server.py / http_handler.py建立通信,处理收包、解包和回包调用封装rpc_client.py客户端侧的连接管理和请求发起示例examples/*带注释的调用演示和压测脚本协议层通常只做一件事把字典转成严格的 JSON 文本。我一般会重点看它的序列化是否处理了非 ASCII 字符,因为默认 json.dumps 会把中文转成 \uXXXX,虽然不违反协议,但抓包排查时很难看,调试成本高。比较顺手的做法是统一指定 ensure_asciiFalse。分发器决定 method 与函数的映射方式。最简单的是用一个字典def register(method_name): def decorator(func): handlers[method_name] func return func return decorator传输层与协议层要彻底解耦。把 TCP Socket 换成 HTTP 时,只要传输层能读出完整 JSON 文本并写回响应体,协议层完全不需要改动。这个边界如果没守住,后面换传输方式时会吃大亏。3. 把 jsonRPC 接进 Python 服务从 TCP 到 HTTP 两种落地姿势3.1 姿势一TCP Socket 原始套接字实现我用得最多的是 TCP 传输,因为很多内部服务走内网直连,不想多套一层 HTTP。TCP 版服务端要处理的核心问题不是序列化,而是“这个包什么时候才算完整”。JSON-RPC 没有内置长度字段,常见做法是以换行符或空字节作为消息分隔符,或者自定义一个带长度头的帧格式。以下是我从 jsonRPC.rar 里提炼出的一个最简可用版本,工作在 Python 3.8 以上环境import json import socket import threading handlers {} class RPCServer: def __init__(self, host, port): self.host host self.port port self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def serve(self): self.sock.bind((self.host, self.port)) self.sock.listen(16) print(flistening on {self.host}:{self.port}) while True: conn, _ self.sock.accept() t threading.Thread(targetself.handle_conn, args(conn,)) t.daemon True t.start() def handle_conn(self, conn): buffer b while True: data conn.recv(4096) if not data: break buffer data while b\n in buffer: line, buffer buffer.split(b\n, 1) if not line.strip(): continue response self.process_line(line) if response is not None: conn.sendall(response.encode() b\n) conn.close() def process_line(self, line): 解析一行 JSON-RPC 请求并构造响应 try: req json.loads(line.decode(utf-8)) except Exception: return json.dumps({ jsonrpc: 2.0, id: None, error: {code: -32700, message: parse error} }) if not isinstance(req, dict): return json.dumps({ jsonrpc: 2.0, id: None, error: {code: -32600, message: invalid request} }) method req.get(method) params req.get(params, {}) req_id req.get(id) func handlers.get(method) if func is None: return json.dumps({ jsonrpc: 2.0, id: req_id, error: {code: -32601, message: method not found} }) try: result func(**params) if isinstance(params, dict) else func(*params) return json.dumps({ jsonrpc: 2.0, id: req_id, result: result }) except Exception as e: return json.dumps({ jsonrpc: 2.0, id: req_id, error: {code: -32603, message: str(e)} }) def register(method_name): def decorator(func): handlers[method_name] func return func return decorator register(user.getProfile) def get_profile(uid: int, fields: list None): # 模拟一个查询逻辑 if uid 0: raise ValueError(invalid uid) data {uid: uid, name: alice} if fields: return {k: v for k, v in data.items() if k in fields} return data if __name__ __main__: RPCServer(0.0.0.0, 7071).serve()这段代码的逻辑链路是先收字节流,按换行符切出完整请求,再交给 process_line 做 JSON 解析和分发。这里有几个参数值得细说。recv(4096) 单次只读 4KB,但 TCP 是流式协议,一个请求可能分多次到达,多个请求也可能粘连在一起,所以外层 buffer 每次循环都要检查是否有换行符。分片条件 while b\n in buffer 会把 buffer 里所有完整行一次性处理完,避免因为读多读了半行而丢失下次请求。params 的类型决定调用方式是对象就 **params 解包,是数组就 *params 解包。两类调用最好在服务端统一约定,我一般强制所有方法只接受对象参数,因为数组参数在业务扩展后极易出错。SO_REUSEADDR 必须加,否则服务重启时会因为 TIME_WAIT 状态拒绝 bind,这是新手最容易踩的坑。3.2 姿势二HTTP 传输 用 Web 框架暴露统一接口TCP 方案在内网稳,但跨网络、跨防火墙时还是 HTTP 更省事。把同一个分发器挂到 HTTP 端点下,几分钟就能完成转发,方式是用一个 Web 框架接收 POST 请求,把 request body 作为 JSON-RPC 文本交给上面那段 process_line 逻辑处理。我用 Flask 试过,代码可以压缩到极简from flask import Flask, request, Response # handlers 和 process_line 复用上一节的实现 app Flask(__name__) app.route(/rpc, methods[POST]) def rpc_endpoint(): response_body process_line(request.get_data().strip()) if response_body is None: # 通知类请求,HTTP 层直接返回 204 return Response(status204) return Response(response_body, mimetypeapplication/json) if __name__ __main__: app.run(host0.0.0.0, port8080, threadedTrue)这里要注意 process_line 返回 None 的场景请求是通知没有 id时,按规范服务端不能返回任何内容,我把它映射为 HTTP 204。单个请求在 HTTP 层不会沾包,但 process_line 已经承担了解析和分发职责,所以换传输层几乎零成本。HTTP 传输有一个明显利好跨语言客户端可以直接用标准 HTTP 工具调用,调试时不需要自己拿 Socket 拼包。不利的一面是慢,每次调用多出 HTTP 头解析的损耗,但如果是低并发场景完全能接受。参数上我一般强调三点threadedTrue 确保并发请求不阻塞;POST 是唯一允许的请求方法,收到 GET 要返回 405;请求体大小限制要调大,因为 JSON-RPC 批量请求时体积可能到几百 KB,默认 1MB 限制经常不够。4. 数据建模与边界参数方法命名、参数校验与批量请求4.1 方法名与参数校验命名空间如何设计,invalid params 怎么区分method 的字符串看起来随意,但在实际系统里它就是 API 的对外承诺,命名不规范会让后续维护的人抓狂。我见过太多服务端 method 叫 do1、fun2,客户端对接时全靠文档,改一个需求就要全局搜索替换。更稳定的做法是采用“资源.动作”的分层命名风格,例如 user.getProfile、order.create、payment.refund。这种风格的好处是方法名自带归属,路由分发时也能按前缀快速定位到业务模块。参数校验是另一个容易翻车的地方。JSON-RPC 规范把参数类型错误归为 -32602,但很多实现直接把异常吞掉返回内部错误,导致客户端根本不知道是自己传参有问题还是服务端崩了。最稳妥的写法是分发器里单独做一层参数检查def safe_call(func, params): import inspect sig inspect.signature(func) try: # 按函数签名过滤未知参数 filtered {k: v for k, v in params.items() if k in sig.parameters} return func(**filtered) except TypeError as e: return { code: -32602, message: invalid params, data: {detail: str(e)} }注意这里用了 inspect.signature 来过滤未知字段,而不是直接调用让 TypeError 自然发生。设计意图是客户端多传了一个无关字段时,服务端应该忽略还是报错我的经验是严格模式更安全,宁可返回 -32602,也不要默默忽略。因为客户端可能因为拼写错误把一个应该传的参数写歪了,服务端若忽略它,业务逻辑就会用默认值,产生难以排查的脏数据。4.2 批量请求、通知与超时/重试参数JSON-RPC 2.0 支持客户端把一个请求数组放进同一帧里,服务端必须按序处理并返回一个响应数组。这个特性在高并发查询场景特别好用,比如前端页面一次要展示用户信息、订单列表、优惠券状态,用三个独立请求要等三次 RTT,用批量请求一次就连回来了。批量请求的处理逻辑在 process_line 里要单独分支def process_batch(batch): results [] for item in batch: # 复用单请求的处理逻辑 resp process_single(item) if resp is not None: results.append(resp) return json.dumps(results) if results else None参数上需要注意的是即使批量里每个请求都成功,响应也永远是数组,不能用单个对象顶替;如果批量请求里混入一个畸形对象,服务端必须对畸形对象返回一个带 null id 的错误,而不能丢弃整个批次。某些实现图省事直接拒绝整包,这不是规范允许的行为。超时与重试参数是客户端侧要细抠的部分。RPC 调用最容易出现的故障不是逻辑错误,而是对端长时间无响应。TCP 默认超时时间很长,一个挂死的服务端能让客户端连接空等好几分钟。合理的做法是在客户端 socket 上设置读超时import json import socket def call_rpc(host, port, method, params, timeout5.0): payload { jsonrpc: 2.0, method: method, params: params, id: 1 } resp {} with socket.create_connection((host, port), timeouttimeout) as sock: sock.sendall(json.dumps(payload).encode(utf-8) b\n) sock.settimeout(timeout) buffer b while b\n not in buffer: chunk sock.recv(4096) if not chunk: raise ConnectionError(connection closed) buffer chunk resp json.loads(buffer.decode(utf-8)) return resp超时值的选择有讲究。内网 RPC 我一般设 2 到 5 秒,外网调用放 10 秒,批量请求相关方法单独放宽到 15 秒。重试策略必须区分错误类型:method not found 和 invalid params 属于永久性错误,重试一百遍也不会成功,直接抛异常就行;超时和连接断开则属于瞬时错误,可以做最多 3 次指数退避重试。很多项目把永久错误也重试,结果就是把下游服务打得更死。5. jsonRPC 集成避坑五个翻车现场与修复方案5.1 翻车现场一请求 id 重复,异步客户端响应错位现象服务端明明每个事务函数都按规范执行了,但客户端收到的 result 跟发送的请求对不上,数据张冠李戴。原因客户端每次调用都固定用 id1,并发发出多个请求后,响应逐个返回,客户端不知道哪个响应对应哪个请求。解决客户端调用前生成全局递增的 id,或者用 UUID 字符串。服务端在实现上必须无条件把请求的 id 原样带回,这既是规范也是排查问题的锚点。从那以后我养成了习惯,客户端工具里强制打印请求与响应对,id 对不上立刻报警。5.2 翻车现场二TCP 粘包与半包导致 JSON 解析失败现象服务端日志里频繁出现 “parse error skip line”,明明客户端每帧都是合法 JSON,却偶尔有一帧被截断,或者两帧数据拼在一起。原因TCP 是字节流,没有消息边界。客户端 sendall 两次请求,服务端可能一次性接收到两条完整消息合并在一起;也可能一条消息被拆成多个 TCP 段到达。解决服务端必须自定义边界规则。我建议在消息末尾追加 \n 作为帧分隔符,客户端 sendall 时保证每帧以 \n 结尾,服务端按其切分。不要依赖 recv 的返回值判断一条消息是否读完,那是一个永恒的错误假设。5.3 翻车现场三params 用数组时传参顺序被客户端搞反现象接口文档写着 “params: [userId, pageSize]”,客户端错传成 [pageSize, userId],查询结果莫名返回空列表,服务端没有任何异常。原因JSON-RPC 的 params 允许是数组,数组按位置绑定到函数参数。只要有一个调用方搞错顺序,服务端接收后不会报错,只会在业务层产生错误结果,极难察觉。解决协议层约定统一使用对象形态的 params,函数内部用关键字参数调用。如果历史接口已经用了数组,服务端适配层要做好参数重命名,把数组转成字典再进入业务函数。5.4 翻车现场四服务端抛出业务异常,错误码全部落在 -32603现象客户端收到统一内部错误,完全看不出是参数问题还是业务逻辑问题,只能去翻服务端日志。原因分发器捕获所有异常后统一返回 -32603,把业务异常和系统异常混在一起。解决定义业务异常基类,处理进程内部自定义错误码段,例如 -32001 到 -32099 之间用于业务错误。分发器先判断异常类型,业务异常直接用预设 code,其余才归为 -32603。class BizError(Exception): def __init__(self, code, message): self.code code self.message message super().__init__(message) def call_and_catch(func, params): try: return {result: func(**params)} except BizError as e: return {error: {code: e.code, message: e.message}} except Exception: return {error: {code: -32603, message: internal error}}5.5 翻车现场五批量请求整体异常时返回单对象现象客户端发送一个包含 10 个请求的数组,服务端直接吞掉整个批次返回 error,客户端把整个页面判为失败。原因实现者为省事,没有按规范逐条处理。解决严格按照规范,把数组里的每个元素独立处理。哪怕只有一个合法请求,响应也应该是数组结构。我常跟同事强调批量请求的价值就在于部分失败时仍然保留成功结果,这是分布式场景下的基本盘。6. 验证与进阶给 jsonRPC 客户端加一个手动调试入口第 5 章讲的是作业题库,这一章讲怎么把客户端调通。我给 jsonRPC 客户端加的最实用的功能,是支持从命令行直接输入 JSON 请求并立刻打印响应。不要小看这个能力,它让排查问题的时间从十分钟缩短到十秒钟。python rpc_cli.py --server 127.0.0.1:7071 --request {method:user.getProfile,params:{uid:1024},id:1}这个命令背后的客户端封装要做的只是把人可读的 JSON 字符串原样发给服务端,再把响应原样打回到终端,不夹带任何额外的封装噪音。核心逻辑不复杂,关键在于配置项要暴露充分配置项推荐默认值使用场景timeout5.0 秒单请求超时上限retry1 次瞬时故障重试次数batch_modeoff是否自动转为批量请求verboseon打印完整请求与响应 JSON进阶用法是把命令行调试客户端接到自动化测试里。我一般会在测试用例里起一个临时服务端,注册几个固定的方法,然后针对协议层作断言。这时要重点验证三类边界情况发送非法 JSON 文本、发送 method not found、发送畸形批量请求数组。把这三种情况做成回归测试后,后面改传输层代码时就不会提心吊胆。如果要把 jsonRPC 真正部署到生产环境,考虑给服务端加一个通用耗时统计在 handle_conn 入口记录时间戳,在响应发送前计算耗时,把超长耗时的方法打印到日志。实现方式是在分发器执行前后包一层计时函数,不需要侵入业务代码。许多性能瓶颈就是这么被发现的,业务代码看起来正常,但一个方法内部隐式调用了多个外部服务,累计延迟直接击穿超时阈值,这类问题只有时间统计能快速暴露。从那次拆包之后,我每次接入新的 RPC 调用前都会强制做一次小演练先起服务端,运行命令行调试客户端发一个正常请求,再看一下无 id 的通知和随机 id 的批量请求,三种情况都跑通才继续写业务代码。这套流程不复杂,但它逼着我先把协议边界摸清,而不是等到线上被坑了再回头重读规范。希望帮到你,也希望你在用 jsonRPC 时少遇到那些只存在于博客里的暗坑。本文还有配套的精品资源点击获取