每一次 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 是否乾淨。
兩個方向的價值是同一個:把兩個未知量裡的一個換成你信得過的東西,剩下的那個故障就只有一個地方可躲了。