每一次 OCPP 对接至少会撞上这道墙一次。充电桩通着电,网络是通的,而 CSMS 上什么也没有。没有 BootNotification,没有报错,只有沉默——或者一行写着 1006 然后就断了的日志。
OCPP-J 跑在 WebSocket 上,而在你的第一条消息之前出的岔子,几乎都出在 WebSocket 握手里。下面是我们见得最多的那些故障的现场指南。
关闭码 1006 的意思是「我没什么能告诉你」
1006 是充电桩日志里最常出现的关闭码,也是规范里信息量最少的一个。它的定义是*异常关闭*:连接死掉了,而且从头到尾没有交换过任何 WebSocket 关闭帧。
关于 1006,要紧的一点是它从不在链路上传输。它是本地造出来的——充电桩上的 WebSocket 库生成它,用来描述「TCP 连接在我脚下消失了」这件事。所以它完全不告诉你 CSMS 的看法,因为 CSMS 根本没机会表达看法。
这就把原因收窄到了 OCPP 之下的那些层:
- TCP 连接被重置或者超时了
- TLS 协商失败,socket 被拆掉
- 某个中间环节——负载均衡器、反向代理、移动运营商 NAT——掐掉了一条空闲连接
- HTTP upgrade 请求被拒,而客户端在读到响应之前就关掉了
如果一连上就得到 1006,那是握手。如果是在几分钟或几小时的正常流量之后才出现,那几乎总是空闲超时,该改的是心跳间隔,而不是你代码里的任何东西。
子协议协商是那个不声不响的杀手
OCPP-J 通过 WebSocket 子协议头来标明自己的版本。充电桩发出:
GET /ocpp/CP001 HTTP/1.1
Host: csms.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: ocpp1.6服务端必须原样回显所提供协议中的恰好一个:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: ocpp1.6这里有三件事会出错,而三件事产生的症状完全一样——连接开了,然后立刻关掉。
服务端干脆不带这个头。 很多 WebSocket 库会愉快地接受 upgrade,却根本不回显 Sec-WebSocket-Protocol。严格的 OCPP 客户端把这个缺失当作协商失败,于是关闭连接。你的 CSMS 记录了一次成功连接,充电桩记录了一次失败。两边说的都是实话。
取值没有完全对上。 子协议令牌是 ocpp1.6,不是 ocpp1.6j、OCPP1.6 或 ocpp-1.6。字符串比较区分大小写,而且没有任何归一化步骤。一台提供 ocpp1.6 的充电桩和一个回答 OCPP1.6 的服务端,什么共识都没达成。
服务端挑了一个对方没提供的协议。 如果充电桩只提供 ocpp1.6,而 CSMS 因为自己偏好 ocpp2.0.1 就回了这个,客户端按规定必须让连接失败。
当充电桩提供多个版本时,它们是按偏好顺序排列的:
Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6服务端该挑的是它支持的第一个,而不是它解析到的最后一个。
看起来像网络问题的 TLS 问题
生产环境里 OCPP 走 wss://,而充电桩的 TLS 栈常常落后它所对接的服务端好几年。这些失败的表现,无助得很一致。
信任库过期或缺失。 嵌入式充电桩出厂时,CA 证书包是烧进固件里的。如果你 CSMS 的证书链到的根证书,是在这台桩的固件编译之后才被加入公共信任库的,那这台桩就没法验证它。服务端这边什么都看不出来——从 CSMS 的角度,客户端只是在握手过程中挂断了。
中间证书链不完整。 浏览器会自己去抓缺失的中间证书,把这个问题糊过去。嵌入式 TLS 客户端一般不会。一个在 Chrome 里打开得好好的站点,可能对你整个车队的充电桩都不可达。要提供完整证书链,而不只是叶子证书。
没有 SNI。 较老的充电桩固件可能不发服务器名称指示(SNI)。如果你的 CSMS 位于一台需要靠 SNI 来挑证书的主机之后,那这条连接拿到的会是默认证书——它对不上,充电桩会拒掉。
协议版本太老。 只会讲 TLS 1.0 或 1.1 的充电桩,连不上要求 1.2 及以上的服务端。这是唯一一种正确答案通常是「升级充电桩」而不是「削弱服务端」的故障。
重连风暴
一旦一批桩开始失败,失败往往会自我放大。连不上的桩会重试;如果每台桩都按同一个固定间隔重试,它们就会同步,于是 CSMS 在同一秒里迎来整支车队,一遍又一遍。
有两样东西能防住这个,而 OCPP 两样都没强制要求,所以得你自己做:
- 带上限的指数退避。 1 秒、2 秒、4 秒、8 秒之后重试,上限定在几分钟。
- 抖动。 把每次延迟随机化 ±20%。没有抖动,退避照样会让车队同步——只是同步到更长的间隔上而已。
CSMS 发版之后也会出现同一个问题。所有桩同时掉线,所有桩又同时重连。退避和抖动,就是把一场踩踏变成一条平缓斜坡的那两样东西。
直接读握手
当日志互相矛盾时,去看链路。一条 curl 就能告诉你服务端有没有正确协商子协议:
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Protocol: ocpp1.6" \
https://csms.example.com/ocpp/CP001你要看到的是 101 Switching Protocols,以及响应里的 Sec-WebSocket-Protocol: ocpp1.6 头。如果拿到了 101 却没有那个头,你的缺陷就找到了——而且它在 CSMS 里,不在充电桩里。
对于 TLS 那一层,openssl s_client -connect csms.example.com:443 -showcerts 会打印服务端实际呈上的完整证书链,这是发现中间证书缺失最快的办法。
模拟器在哪里帮得上
这件事真正难的地方在于,你是在同时调试两个实现,而你只能看见其中一个。一台充电桩模拟器给了你一个已知良好的客户端:如果 SimPilot 连上了你的 CSMS,而你的充电桩连不上,缺陷在充电桩。如果两个都连不上,那就是服务端。
反过来,TestPilot 充当一个已知良好的 CSMS,你可以把一台实物充电桩指向它,在自己的后端还完全没进画面之前,先看握手和第一条 BootNotification 是否干净。
两个方向的价值是同一个:把两个未知量里的一个换成你信得过的东西,剩下的那个故障就只有一个地方可躲了。