简介:面向Java Web开发者的JSON-RPC入门案例包,围绕轻量级远程调用协议的核心概念,帮助读者理解客户端与服务端基于JSON格式的通信机制,并掌握jsonrpc4j等库的实战集成方式。压缩包共35个文件,以25个jar依赖为主,辅以5个class类文件、2个xml配置、1个jsp入口页面、1个properties配置和1个mf清单,整体18.86MB,结构清晰。已有223人浏览学习。案例通过index.jsp演示请求构建,WEB-INF下的类文件展示服务器端处理逻辑,META-INF保留应用元数据,并结合Spring、Jackson、jsonrpc4j等组件,覆盖JSON-RPC 2.0的请求/响应结构、方法调用、异常处理等关键知识点。读者可借助该案例快速搭建Java Web环境,体验从发起请求到解析响应的完整流程,为在分布式服务或前后端交互中应用JSON-RPC打下基础。
1. 拿到 jsonRPC.rar 之后,先搞清楚它到底是什么
经常有同学从网上下到或者从前辈手里拷到一个叫jsonRPC.rar的压缩包,解压开发现里面躺着一个工程目录,有client.py、server.py、protocol.py、tcp_transport.py、handler.py、README.md这种结构。第一反应往往是:这玩意儿和我平时用的 HTTP API 有什么区别?它到底解决了什么问题?我直接拿过来能用吗?
这里我先说人话。JSON-RPC 是一种基于 JSON 格式的远程过程调用协议,核心思想是“我这边调用一个函数,但函数实际在另一台机器上执行”。它不像 REST 那样把资源拆成 GET/POST/PUT/DELETE,而是通过一个统一的入口,发送一条指令:方法名 + 参数 + 请求 ID,然后等对方返回“结果”或者“错误”。我用它写过几个内部工具和自动化脚本,最大的感受是:当你要做的不是资源管理,而是“让远程机器执行某个操作”的时候,JSON-RPC 比 REST 直观得多,代码量也能砍掉一半以上。
这份压缩包里封装的其实就是一套“开箱即用”的 JSON-RPC 2.0 实现,覆盖了 TCP 长连接和 HTTP 短连接两种传输方式,还顺手做了请求 ID 管理、超时控制、批量调用、错误码统一这些工程上绕不开的东西。适合谁?适合正在写前后端联调接口、做微服务内部通信、搞设备控制或者写自动化测试平台的同学。你不需要掌握多少底层网络知识,照着 README 把 client 模块 import 进去,填上 IP 和端口就能跑通第一个调用。
我接下来会把压缩包里的每个文件、每段关键逻辑、每个容易踩坑的点全拆开讲。不管你是想直接拿去用,还是想参考它的设计思路自己造轮子,这篇都够用。
2. 压缩包内部模块拆解:每一层都在解决什么问题
2.1 工程结构与设计定位
先看整体设计,这份代码的目录规划是典型的“协议、传输、业务分离”:
jsonRPC/ ├── protocol.py # JSON-RPC 2.0 协议层:请求/响应/错误对象 ├── server.py # 服务端:方法注册、分发、调用 ├── client.py # 客户端:同步/异步调用入口 ├── tcp_transport.py # TCP 传输层:粘包处理、半包读取 ├── http_transport.py # HTTP 传输层:requests 封装 ├── handler.py # 业务处理器基类 ├── exceptions.py # 自定义异常体系 ├── utils.py # ID 生成器、时间戳、参数校验 └── README.md它没有把代码堆在一个文件里,而是把“协议格式”“底层传输”“业务处理”三层拆开。这样做的好处很实在:如果你只写一个 Python 脚本跑通 RPC,全部塞一个文件当然也行,但一旦要加权限校验、日志、请求链路追踪,拆分开的代码能让你在不影响传输层的前提下快速扩展。我在自己的项目里也沿用这个分层思路,后面加个 WebSocket 传输层,只要实现send()和receive()两个方法,上层的 client/server 完全不用动。
protocol.py是整份代码的核心,它严格遵循 JSON-RPC 2.0 规范定义了请求对象和响应对象的结构:
# 请求对象结构示例,实际代码为 dataclass class RPCRequest: jsonrpc: str = "2.0" method: str params: object = None id: int class RPCResponse: jsonrpc: str = "2.0" result: object = None error: dict = None id: int注意一个细节:请求必须有method和id,通知请求可以没有id,但必须有jsonrpc字段并固定为"2.0"。压缩包里的代码对 id 做了强制校验,如果客户端传了 None,服务端会抛InvalidRequestError。从协议设计的角度说,id 的作用是让客户端能把响应和请求对应上,尤其在批量调用时,这个字段不够严谨会导致整套请求响应配对错乱。
2.2 为什么选择“双传输层”而不是只做一种
很多同学看到这肯定会问:为什么 JSON-RPC 项目要同时支持 TCP 和 HTTP?直接用一个不行吗?
这里的选择取决于使用场景。我的实际体会是:
- TCP 传输适合客户端和服务端之间需要保持长连接、大量高频调用的场景,比如设备指令下发、实时数据采集。此时每一条请求复用同一个连接,避免了反复握手的开销,性能会好很多。
- HTTP 传输更适合跨网络、跨防火墙、临时调用的场景,比如第三方系统回调、脚本里偶尔调一次远程方法。HTTP 更容易穿透各种网络限制,也更容易配合现有的负载均衡、日志中间件一起工作。
压缩包里的tcp_transport.py处理了一个我在实践中觉得最讨厌的问题:TCP 流式传输没有消息边界。它采用“4 字节头表示消息体长度”的方案,每次收包先读 4 字节长度,再按长度读取完整的 JSON 字符串。这个思路很经典,和很多消息队列协议(比如 RabbitMQ 的帧格式)是一致的。你要在自己项目里实现长连接通信,直接照抄这一套准没错。
3. 协议层的核心机制:请求、响应、通知与批量调用的闭环
3.1 请求命名的设计意图
JSON-RPC 2.0 规范里,方法名使用类似模块名.方法名的点分格式,比如user.getInfo、order.create。压缩包里的 server 端实现了一个 decorator,让开发者可以这样注册方法:
from server import RPCServer server = RPCServer() @server.register("user.getInfo") def get_user_info(user_id: int): return db.query(user_id)它内部其实是一个字典:self._method_map = {"user.getInfo": get_user_info}。收到请求后,根据method字符串查字典,找不到就返回“方法不存在”错误。用点分命名的好处有两个:一是方便分组和管理,二是在做权限控制的时候可以按前缀匹配。我在实际项目中就遇到过这样的需求:所有带admin.前缀的方法只能允许机器 A 调用,普通方法允许所有人调用。这种设计让权限策略的代码写起来极其直观。
3.2 响应与错误码的统一约定
这份代码里错误码不是随手瞎写的,它遵循了 JSON-RPC 2.0 规范中预定义的错误码范围,并额外补充了一些业务常用的错误码:
| 错误码 | 含义 | 压缩包内说明 |
|---|---|---|
| -32700 | 解析错误 | 收到的 JSON 格式不正确 |
| -32600 | 无效请求 | 请求对象结构不符合规范 |
| -32601 | 方法不存在 | 调用的 method 未注册 |
| -32602 | 无效参数 | 参数类型或数量不匹配 |
| -32603 | 内部错误 | 方法执行过程中抛出异常 |
| -32000 及以后 | 服务端自定义业务错误 | 业务逻辑层自行定义 |
在实际开发中,我最常踩的坑就是:客户端把 HTTP 状态码和 RPC 错误码混为一谈。HTTP 200 只表示 HTTP 层面传输成功,RPC 调用是否成功要看响应体里的error字段是否为 null。压缩包里的client.py做了这一层转换,它会判断error字段,有错误时抛出RPCError,而不是让你自己去解析 JSON。这一点对调用方非常友好,我在写自动化脚本时,只需要try/except RPCError就能捕获到所有的远程调用异常。
3.3 通知请求和批量调用的边界条件
JSON-RPC 2.0 公认最实用但新手最容易搞混的写法有两种:通知(Notification)和批量(Batch)。
- 通知请求:没有
id字段,服务端处理完不会返回任何响应。适合“发个日志”“触发一下重试任务”这种不需要知道结果的场景。 - 批量请求:把多个请求放进一个数组里,一次性发送,服务端也要返回一个数组作为响应。
压缩包里这两块都做了。注意,服务端在处理批量请求时,如果其中一条请求格式错误,不能直接整批返回错误,而是要把可用的请求都处理掉,再在结果数组里混合塞入错误对象。这个设计我在最初写的时候没注意,结果客户端那边经常收到一个“神秘失败”的批量响应,后来对照规范才发现是这个细节。
4. 实操走一遍:从解压到完成第一次远程调用
4.1 快速启动:跑通自带的回环测试
压缩包 README 里给了最简启动方式,实际跑起来只需要三步。先打开两个终端,第一个终端启动服务端:
cd jsonRPC python examples/simple_server.py # 服务启动在 127.0.0.1:8000第二个终端运行客户端示例:
python examples/simple_client.py # 输出: 8我自己试的时候,客户端默认调用math.add(3, 5),返回结果 8。整个过程非常短,你不需要懂网络编程也能看到效果,因为代码已经把 manual 的事情做完了。
如果你不想依赖示例,自己写最小调用也很直接:
from client import JSONRPCClient client = JSONRPCClient(host="127.0.0.1", port=8000) result = client.call("math.add", {"a": 2, "b": 3}) print(result) # 5关键点在于call方法内部做了三件事:生成唯一请求 id、序列化请求对象、通过传输层发送并等待匹配 id 的响应。从使用者的角度看,就像调用本地函数一样简单。
4.2 超时、重试与连接池的参数选择
参数设计是这种框架代码里最容易被忽视、但又是实际运行中最影响稳定性的部分。压缩包里的client.py给了几个关键参数:
client = JSONRPCClient( host="127.0.0.1", port=8000, timeout=10, # 单个请求超时时间,单位秒 max_retries=2, # 失败后重试次数 retry_interval=0.5 # 重试间隔,单位秒 )超时时间我建议按“最慢方法执行时间 + 网络往返时间 + 余量”来设定。比如你的业务方法在最坏情况下执行 3 秒,网络 RTT 在 30ms 左右,那超时设置为 5~6 秒是比较合理的。设太短,稍微一抖动就大面积超时;设太长,客户端缓存一大堆 pending 请求,故障恢复链路会变得非常迟钝。
另外,压缩包里的 TCP 客户端内置了连接池。若你的服务端有多个 worker 进程,客户端会按轮询策略分配连接,避免所有请求都挤在同一根连接上。这点在并发要求高的时候非常关键,我见过太多人 TCP 长连接没做连接池,高并发下一旦重启服务端,一堆 ESTABLISHED 状态变成 TIME_WAIT。
4.3 身份验证与参数校验的接入点
压缩包里的 server 端留了一个before_request钩子,可以让你插入鉴权逻辑。我在实际项目里是在这个钩子里解析请求头里带的一段 token,和远端认证服务校验,通过后才进入方法分发:
server = RPCServer() @server.before_request def auth_check(request): token = request.meta.get("token", "") if not validate_token(token): raise AuthError("invalid token")另外,参数校验我推荐在业务方法内部处理而不是在协议层全盘接管。JSON-RPC 本来就是一种很薄的信令协议,把所有参数规则堆在协议层会让代码变得又臭又硬。用 Python 的话,配合pydantic写个ValidationModel,方法入口直接一行UserCreate(**params)就能完成绝大部分校验工作。
5. 排错实战:我在使用过程中遇到的 5 个典型问题
5.1 服务端老是报“JSON 解析错误”,但客户端明明发送的是合法 JSON
这个问题我印象最深。排查过程非常曲折:客户端打印出来的 payload 是正确的,服务端却拿不到完整字符串。后来用 tcpdump 抓包才发现,客户端在发送后立刻关闭了连接,服务端只收到半截数据。根本原因是客户端发送后没有等待发送缓冲区 flush,就直接关了 socket。
解决办法分两层:
- 短连接场景:客户端在
send()之后,要调用shutdown(SHUT_WR)告诉对方“数据发完了”,再等响应。 - 长连接场景:客户端不能随意关连接,只剩服务端有数据完整性的判断。
压缩包里的tcp_transport.py是长连接模式,它不会主动关闭,所以理论上不该遇到这问题。如果你改造了源码,自己写短连接,一定要留意这个发送时序。
5.2 批量请求返回结果顺序和请求顺序不一致
JSON-RPC 2.0 规范明确说,批量响应里的顺序可以任意排列,客户端必须根据id去匹配,而不能假设数组顺序。压缩包的客户端实现是老老实实按 id 匹配的,所以没问题。
但如果你自己在浏览器控制台调试,直接Promise.all收发批量请求,然后按数组 index 取结果,可能会踩坑。正确做法是:收到响应数组后,先循环一遍,建立id -> result的字典,再按需取用。
5.3 响应内容出现中文乱码,JSON 序列化失败
这个在 Python 2 时代非常头疼,Python 3 也有坑:当json.dumps默认ensure_ascii=True时,会把中文转成\uXXXX序列,TCP 传输本身没问题,但日志和抓包工具里看就是一团乱码。你在打印调试或者落盘的时候,手动转一下编码就行:
print(json.dumps(request, ensure_ascii=False, indent=2))5.4 服务端业务方法抛出异常后,客户端拿到的是超时而非错误信息
出现这个情况,多半是服务端在dispatch的时候把异常吞掉了,但没正确回填error字段。我在一个生产环境碰到过服务端业务代码里自己写了except Exception: pass,导致 RPC 框架以为方法还在执行,最终等到超时。排查方法很简单:临时把服务端的异常处理改成打印 traceback,看方法是否真的跑完。
import traceback from exceptions import InternalError try: result = method(**params) except Exception: traceback.print_exc() raise InternalError(str(e))5.5 TCP 粘包导致第二条请求解析失败
TCP 粘包是经典问题,压缩包用“4 字节长度头”已基本解决。但如果你自己改写了传输层,要特别注意:读 4 字节头时,read 返回的数据可能不足 4 字节;读 body 时,也可能只读到 body 的一部分。必须循环读取直到读满为止,代码里大概是:
def recv_all(sock, n): data = b"" while len(data) < n: chunk = sock.recv(n - len(data)) if not chunk: raise ConnectionError("connection closed") data += chunk return data别看这个函数简单,我见过至少三次线上粘包问题就是因为recv一次没收满导致的。
6. 踩坑后的几点心得和后续扩展建议
这套 JSON-RPC 代码用下来的整体感受是:协议简单,工程不简单。我写自动化脚本、内部系统联动时,使用 JSON-RPC 确实省掉了不少 REST 那种“建资源、改字段、查列表”的样板代码。但协议简单也意味着它不强加给你业务规范,权限、限流、日志、监控、服务发现这些全要靠自己在框架外层补。
在压缩包之外,我认为接下来最值得做的扩展有三个方向:
- 异步化改造:目前 client 是同步阻塞的,如果同时要调几十个远程方法,建议用 Python 的
async/await或concurrent.futures把调用并发出去,能降低一半以上的总耗时。 - 集成注册发现:服务端在多机部署后,客户端直接用 IP 列表会越来越难维护,可以接 ZooKeeper、Consul 或 etcd,让客户端动态感知服务节点的上下线。
- 请求链路追踪:在
before_request钩子里生成trace_id传给业务方法,业务日志和 RPC 日志都带上这个字段,排错成本会大幅降低。
最后再说一个我自己的习惯:凡是项目里引入这种“通用封装型”代码,第一件事不是跑通示例,而是把协议层的单元测试补上。JSON-RPC 的协议层足够简单,测试用例覆盖好请求序列化、响应解析、错误码、批量调用这四块,后面再改传输层、加鉴权逻辑心里都有底。压缩包里的代码我建议你至少跑一遍它的自测脚本,再在这上面改出适合自己的版本。
本文还有配套的精品资源,点击获取