端實(shí)現(xiàn):CommonAPI與vsomeip配置、Stub及排障實(shí)踐)
做SOME/IP通信如果沒(méi)被服務(wù)端折騰過(guò)真不好意思說(shuō)做過(guò)車載通信。CommonAPI和vsomeip這套組合客戶端側(cè)只要調(diào)方法、收事件看起來(lái)還算清爽真落到服務(wù)端實(shí)現(xiàn)難點(diǎn)就變成了另一件事怎么讓客戶端穩(wěn)定地找到你、正確地回包、不丟事件、不崩進(jìn)程。這篇是服務(wù)端部分的第三篇默認(rèn)你已經(jīng)對(duì)CommonAPI的C接口和vsomeip的SD機(jī)制有基本概念下面專門寫實(shí)現(xiàn)服務(wù)端時(shí)真正花時(shí)間的那些事角色分工、版本匹配、配置、Stub骨架還有幾個(gè)我實(shí)測(cè)過(guò)才敢寫出來(lái)的坑。1. 服務(wù)端到底是什么角色先理清三層的關(guān)系再動(dòng)手1.1 SOME/IP、vsomeip、CommonAPI各自管哪一段很多剛開(kāi)始做車載通信的同事會(huì)把SOME/IP、vsomeip、CommonAPI混為一談排錯(cuò)的時(shí)候經(jīng)常把協(xié)議問(wèn)題當(dāng)成代碼問(wèn)題、把版本問(wèn)題當(dāng)成網(wǎng)絡(luò)問(wèn)題。這三個(gè)東西其實(shí)分工相當(dāng)明確SOME/IP是協(xié)議規(guī)范規(guī)定消息怎么封裝、方法怎么請(qǐng)求、事件怎么通知、SD服務(wù)發(fā)現(xiàn)報(bào)文長(zhǎng)什么樣。它是一套約定。vsomeip是SOME/IP的開(kāi)源實(shí)現(xiàn)負(fù)責(zé)網(wǎng)絡(luò)收發(fā)、SD報(bào)文、路由、TCP/UDP連接管理。你可以把它理解為快遞網(wǎng)點(diǎn)網(wǎng)絡(luò)它知道包裹怎么從這個(gè)節(jié)點(diǎn)運(yùn)到那個(gè)節(jié)點(diǎn)。CommonAPI是跨進(jìn)程通信的上層接口框架它定義了你寫代碼時(shí)用的方法、事件、字段這些抽象。它把底層SOME/IP細(xì)節(jié)屏蔽掉讓你像調(diào)本地函數(shù)一樣調(diào)用遠(yuǎn)程服務(wù)。我的建議是遇到任何通信異常先判斷它發(fā)生在哪一層。是CommonAPI的接口簽名對(duì)不上是vsomeip的配置把IP/端口搞錯(cuò)了還是SOME/IP協(xié)議本身在該場(chǎng)景下的機(jī)制問(wèn)題比如事件組訂閱關(guān)系層與層之間互相甩鍋是最常見(jiàn)的浪費(fèi)時(shí)間方式。1.2 服務(wù)端和客戶端關(guān)注點(diǎn)完全不同客戶端側(cè)的核心訴求是怎么把請(qǐng)求發(fā)出去、結(jié)果拿回來(lái)而服務(wù)端側(cè)的任務(wù)更重服務(wù)端是權(quán)威數(shù)據(jù)的持有者是事件生成的源頭是多個(gè)客戶端并發(fā)訪問(wèn)的匯聚點(diǎn)。用表格看更直觀關(guān)注點(diǎn)客戶端側(cè)服務(wù)端側(cè)核心目標(biāo)找到并調(diào)用遠(yuǎn)程服務(wù)被穩(wěn)定發(fā)現(xiàn)、持續(xù)提供數(shù)據(jù)方法調(diào)用發(fā)請(qǐng)求、等返回接收請(qǐng)求、解析參數(shù)、回Reply事件收發(fā)訂閱事件組、收廣播按事件組廣播數(shù)據(jù)字段操作遠(yuǎn)程讀取、遠(yuǎn)程設(shè)置維護(hù)本地權(quán)威狀態(tài)并同步失敗場(chǎng)景服務(wù)下架后重試或切換備用避免誤發(fā)、崩潰、掉線時(shí)造成客戶端超時(shí)服務(wù)端具體要做的事歸納起來(lái)就四件注冊(cè)和發(fā)布服務(wù)、處理方法請(qǐng)求、廣播事件、維護(hù)字段狀態(tài)。很多坑都源于沒(méi)有區(qū)分客戶端視角和服務(wù)端視角。比如我明明注冊(cè)成功了為什么客戶端看不到——這類問(wèn)題幾乎都集中在SD協(xié)商環(huán)節(jié)而不是代碼有沒(méi)有寫對(duì)。2. 第一道坎環(huán)境準(zhǔn)備和版本匹配先解決“跑不起來(lái)”2.1 依賴庫(kù)和編譯順序CommonAPI vsomeip的服務(wù)端工程常規(guī)依賴包括Boost、CMake、Python3部分代碼生成工具依賴它以及兩個(gè)核心庫(kù)CommonAPI核心庫(kù)和CommonAPI SOME/IP綁定層綁定層又依賴vsomeip本身。我的編譯順序是固定的先編CommonAPI核心庫(kù)再編vsomeip然后編CommonAPI的SOME/IP綁定層最后編譯你自己的服務(wù)端程序。順序一旦顛倒后編譯的庫(kù)可能在鏈接階段找不到前一個(gè)庫(kù)的符號(hào)。大致命令如下# 1. 安裝基礎(chǔ)依賴 sudo apt-get install cmake build-essential libboost-system-dev libboost-thread-dev # 2. 編譯 commonapi-core git clone commonapi-core倉(cāng)庫(kù) cd commonapi-core mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/opt/automotive .. make -j$(nproc) sudo make install # 3. 編譯 vsomeip git clone vsomeip倉(cāng)庫(kù) cd vsomeip mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/opt/automotive .. make -j$(nproc) sudo make install # 4. 編譯 commonapi-someip-tools 或綁定庫(kù) git clone commonapi-someip-tools倉(cāng)庫(kù) cd commonapi-someip-tools mkdir build cd build cmake -DCMAKE_INSTALL_PREFIX/opt/automotive .. make -j$(nproc) sudo make install # 5. 編譯你自己的服務(wù)端工程 cd /path/to/your-service mkdir build cd build cmake -DCMAKE_PREFIX_PATH/opt/automotive .. make如果你不是源碼編譯而是用包管理器直接安裝那么省事不少但要注意包管理器里的版本往往不是最新且多個(gè)庫(kù)之間未必匹配。我個(gè)人傾向固定版本的源碼構(gòu)建因?yàn)榻桓盾囕d環(huán)境時(shí)開(kāi)發(fā)者需要完全可復(fù)現(xiàn)的版本鎖定。2.2 版本矩陣別迷信“最新版”CommonAPI vsomeip最讓人頭疼的就是版本匹配。很多人喜歡各庫(kù)都拉最新版本結(jié)果運(yùn)行時(shí)出現(xiàn)各種invalid cast、找不到符號(hào)。它們的接口在演進(jìn)綁定層生成的代碼和運(yùn)行庫(kù)之間如果太新太舊即使編譯過(guò)了運(yùn)行時(shí)也會(huì)在加載階段或第一次調(diào)用時(shí)翻車。我整理了一份示例組合注意是示例具體以你所在代碼倉(cāng)庫(kù)官方文檔的版本矩陣為準(zhǔn)commonapi-corecommonapi-someip-toolsvsomeip建議3.2.x3.1.x3.1.x / 3.3.x同代組合兼容性較好3.1.x3.0.x2.10.x老項(xiàng)目常用跨大版本混用跨大版本混用跨大版本混用不推薦編譯過(guò)也可能運(yùn)行崩我的經(jīng)驗(yàn)是拿到一個(gè)新工程先把頂層README里L(fēng)ocked Versions或Dependencies章節(jié)看一遍確認(rèn)這組庫(kù)是哪幾個(gè)版本配在一起的再據(jù)此決定自己項(xiàng)目鎖哪一版。不要在今天編譯通過(guò)后過(guò)兩個(gè)月突然升級(jí)其中一個(gè)庫(kù)這往往是黑暗調(diào)試的開(kāi)始。2.3 LD_LIBRARY_PATH和示例工程綁定層運(yùn)行時(shí)要通過(guò)dlopen加載vsomeip共享庫(kù)如果LD_LIBRARY_PATH沒(méi)包含安裝目錄進(jìn)程會(huì)在啟動(dòng)階段報(bào)錯(cuò)或崩潰。這個(gè)坑特別隱蔽因?yàn)榫幾g時(shí)能找到庫(kù)運(yùn)行時(shí)卻找不到。我用一個(gè)腳本固定環(huán)境變量每次編譯運(yùn)行前source一次export LD_LIBRARY_PATH/opt/automotive/lib:$LD_LIBRARY_PATH export PKG_CONFIG_PATH/opt/automotive/lib/pkgconfig:$PKG_CONFIG_PATH另外不要一上來(lái)就寫自己的服務(wù)端業(yè)務(wù)代碼。先把綁定層倉(cāng)庫(kù)里自帶的examples跑通。比如先跑通一個(gè)簡(jiǎn)單的服務(wù)端注冊(cè)、客戶端發(fā)現(xiàn)并調(diào)用方法的example再開(kāi)始改造成自己的服務(wù)。這個(gè)習(xí)慣幫我省了大量時(shí)間——因?yàn)槿绻鹐xample都跑不通問(wèn)題基本在環(huán)境或版本不在你的業(yè)務(wù)邏輯。3. 服務(wù)端主流程怎么設(shè)計(jì)從配置文件到Stub實(shí)現(xiàn)3.1 先看vsomeip的網(wǎng)絡(luò)配置服務(wù)端到底綁定在哪張網(wǎng)卡上服務(wù)端實(shí)現(xiàn)的第一步不是寫代碼是寫配置。vsomeip需要一個(gè)JSON格式的配置文件里面至少指定服務(wù)端的IP地址、診斷地址、端口等信息。一個(gè)極簡(jiǎn)示意見(jiàn)下面注意不同版本字段名稱可能有出入以你的vsomeip版本自帶的樣例配置為準(zhǔn){ unicast: 192.168.0.11, diagnostic_address: 0x1051, ports: { someip-ipc-port: 7001, someip-sd-port: 30490 }, applications: [ { name: my-audio-service, id: 0x1100 } ] }這個(gè)配置里unicast是最容易被忽略的。如果開(kāi)發(fā)機(jī)有多塊網(wǎng)卡比如以太網(wǎng)、Wi-Fi、Docker虛擬網(wǎng)卡而vsomeip拿到的主機(jī)IP不是你客戶端能訪問(wèn)的那個(gè)網(wǎng)段SD報(bào)文會(huì)從錯(cuò)誤網(wǎng)卡出去客戶端永遠(yuǎn)發(fā)現(xiàn)不了服務(wù)。我剛做這個(gè)的時(shí)候排查了一下午才發(fā)現(xiàn)是unicast沒(méi)顯式指定vsomeip取了默認(rèn)路由的第一塊網(wǎng)卡而那塊網(wǎng)卡恰好和測(cè)試環(huán)境不在同一網(wǎng)段。端口部分一般不用動(dòng)但你要知道someip-sd-port默認(rèn)是30490服務(wù)事件和方法數(shù)據(jù)也都走這里或TCP對(duì)應(yīng)端口。很多人抓包只抓業(yè)務(wù)端口忘了SD也走UDP導(dǎo)致看到的現(xiàn)象是客戶端在找服務(wù)服務(wù)端完全沒(méi)響應(yīng)。3.2 CommonAPI綁定層的映射配置服務(wù)ID、方法ID、事件IDCommonAPI接口定義文件*.fidl里只有邏輯上的方法名、事件名但它最終要通過(guò)SOME/IP綁定層映射到數(shù)字ID上。綁定層的部署配置文件負(fù)責(zé)完成邏輯接口到SOME/IP ID的映射。大致結(jié)構(gòu)類似下面這樣具體格式隨綁定層工具版本變化但概念一致{ someip: { service: { audio-controller: { service_id: 0x60F0, instance_id: 0x0001, method_ids: { SetVolume: 0x0A01, GetVolume: 0x0A02 }, event_ids: { VolumeChanged: 0x0B01 }, eventgroup_ids: { eg-volume: 0x0001 } } } } }這些ID不是隨便填的。服務(wù)ID區(qū)分不同服務(wù)實(shí)例ID區(qū)分同一服務(wù)的不同實(shí)例方法ID用于路由請(qǐng)求事件ID和事件組ID決定事件從哪來(lái)、往哪去。一個(gè)小失誤比如方法ID沖突就會(huì)造成A調(diào)用了方法結(jié)果B服務(wù)收到了這是最詭異的一類通信錯(cuò)亂。我的建議是把服務(wù)和事件ID做成一個(gè)中央清單不要散落在各個(gè)服務(wù)模塊的配置里。多個(gè)人并行開(kāi)發(fā)的時(shí)候服務(wù)ID沖突這種事比你想象的常見(jiàn)。3.3 繼承Stub骨架實(shí)現(xiàn)業(yè)務(wù)邏輯CommonAPI用工具根據(jù)fidl接口定義生成兩套骨架客戶端用的Proxy服務(wù)端用的Stub。服務(wù)端要做的是繼承生成的StubDefault類或者對(duì)應(yīng)版本生成的類實(shí)現(xiàn)里面的虛方法。下面是一段用戶自定義Stub的示意代碼// AudioControllerStubImpl.h #include AudioControllerStubDefault.hpp class AudioControllerStubImpl : public AudioControllerStubDefault { public: AudioControllerStubImpl() { init(); } // 方法客戶端遠(yuǎn)程調(diào)用SetVolume時(shí)會(huì)進(jìn)入這里 void SetVolume( const std::shared_ptrCommonAPI::ClientId clientId, uint16_t volume, SetVolumeReply_t reply) override { // 處理業(yè)務(wù)邏輯比如校驗(yàn)、落盤、更新內(nèi)部狀態(tài) if (volume maxVolume_) { volume maxVolume_; } volume_ volume; // 回復(fù)調(diào)用方 reply(CommonAPI::CallStatus::SUCCESS, volume_); // 如果需要通知所有訂閱了該事件的客戶端 fireVolumeChanged(volume_); } private: uint16_t volume_ 0; uint16_t maxVolume_ 100; };注意fireVolumeChanged是生成代碼里的公共事件廣播方法名實(shí)際項(xiàng)目里它可能叫fireVolumeChangedEvent以你生成的代碼為準(zhǔn)。服務(wù)端要實(shí)現(xiàn)的就是更新自己的狀態(tài)、回復(fù)調(diào)用方、廣播變化。這里有個(gè)重要理念服務(wù)端是權(quán)威數(shù)據(jù)源不要讓遠(yuǎn)端值繞過(guò)你直接寫內(nèi)存否則多個(gè)客戶端讀到的狀態(tài)會(huì)不一致。3.4 main()里怎么注冊(cè)服務(wù)服務(wù)端程序的入口核心動(dòng)作是加載綁定層、創(chuàng)建工廠、創(chuàng)建Stub、注冊(cè)服務(wù)。不同版本的CommonAPI注冊(cè)寫法略有差異但核心流程一致#include CommonAPI/CommonAPI.hpp int main() { // 加載某個(gè)綁定層vsomeip就是 vsomeip auto runtime CommonAPI::Runtime::load(vsomeip); if (!runtime) { // 處理加載失敗 return -1; } auto factory runtime-createFactory(); auto stub std::make_sharedAudioControllerStubImpl(); // 注冊(cè)服務(wù)。不同版本地址字符串格式不同以生成代碼/示例為準(zhǔn) bool registered factory-registerService( stub, local:audio-controller:AudioController:someIP:0x60F0); if (!registered) { // 注冊(cè)失敗處理 return -1; } // 主線程不能退出讓后臺(tái)IO線程持續(xù)運(yùn)行 while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); } return 0; }這里重點(diǎn)提醒注冊(cè)成功不代表已經(jīng)可以提供服務(wù)。CommonAPI的registerService返回成功表示綁定層的路由信息已經(jīng)準(zhǔn)備好了但SD的Offer上報(bào)可能還要等下一輪SD周期??蛻舳丝傄?jīng)過(guò)一個(gè)發(fā)現(xiàn)窗口才能看到服務(wù)這在測(cè)試?yán)锖苷e一看到客戶端慢了幾百毫秒就以為出故障。3.5 方法、事件、字段服務(wù)端的三種接口要區(qū)別對(duì)待方法Method是請(qǐng)求-響應(yīng)模式客戶端調(diào)用服務(wù)端處理并回復(fù)。事件Event是服務(wù)端主動(dòng)通知客戶端訂閱某個(gè)事件組后服務(wù)端通過(guò)fireXxx廣播。字段Field是屬性操作底層其實(shí)拆成了Get方法和Set方法但CommonAPI把語(yǔ)義包裝成RemoteGet/RemoteSet。三種接口在服務(wù)端實(shí)現(xiàn)時(shí)的注意點(diǎn)不同方法不要在回調(diào)里做耗時(shí)的同步操作。CommonAPI分發(fā)到回調(diào)的線程池大小有限如果一個(gè)方法的處理里sleep了100毫秒同一時(shí)間其他方法的響應(yīng)也會(huì)被堵住。復(fù)雜業(yè)務(wù)應(yīng)該把任務(wù)投遞到獨(dú)立的工作線程。事件只有在客戶端訂閱了事件組之后廣播才有意義。很多事件系統(tǒng)的坑不是服務(wù)端沒(méi)發(fā)而是服務(wù)端根本不知道哪些客戶端訂了什么一股腦發(fā)、發(fā)完了發(fā)現(xiàn)全被丟棄。字段服務(wù)端是權(quán)威狀態(tài)??蛻舳薙et字段后要不要廣播變化給所有訂閱者取決于接口語(yǔ)義。我的習(xí)慣是只要字段真正變化了就主動(dòng)廣播一次寧可多一次通知也不要讓其他客戶端拿到過(guò)期數(shù)據(jù)。4. 服務(wù)端真正容易踩的坑排查手法和現(xiàn)場(chǎng)記錄4.1 抓包是服務(wù)端排障的第一手段在沒(méi)有抓包的情況下猜SOME/IP問(wèn)題基本等于閉著眼修車。服務(wù)端注冊(cè)不成功、方法沒(méi)響應(yīng)、事件沒(méi)送到這些問(wèn)題的答案幾乎都在網(wǎng)絡(luò)報(bào)文里。wireshark自帶SOME/IP和SOME/IP-SD解析器抓到報(bào)文后能直接看到OfferService、FindService、SubscribeEventgroup這些關(guān)鍵動(dòng)作。抓包過(guò)濾最簡(jiǎn)單的辦法udp.port 30490SD端口業(yè)務(wù)流量看對(duì)應(yīng)UDP/TCP業(yè)務(wù)端口。重點(diǎn)觀察三步服務(wù)端有沒(méi)有周期性發(fā)送OfferService如果沒(méi)有問(wèn)題在服務(wù)端配置或注冊(cè)流程??蛻舳擞袥](méi)有發(fā)FindService如果沒(méi)有問(wèn)題在客戶端服務(wù)端可能壓根沒(méi)收到請(qǐng)求。雙方服務(wù)ID、實(shí)例ID是否一致不一致會(huì)在報(bào)文里看到在找另一個(gè)服務(wù)。我踩過(guò)最窩囊的一次服務(wù)端和客戶端用的服務(wù)ID完全對(duì)不上服務(wù)端注冊(cè)的是0x60F0客戶端配置寫的是0x60F1。抓包后兩秒鐘看出來(lái)代碼層面卻查了一個(gè)多小時(shí)。4.2 服務(wù)已注冊(cè)但客戶端找不到排查鏈路這類問(wèn)題高發(fā)我一般按順序檢查現(xiàn)象最可能原因驗(yàn)證方法客戶端一直FindService服務(wù)端沒(méi)有Offer服務(wù)端沒(méi)注冊(cè)成功或vsomeip配置錯(cuò)誤抓包看OffService報(bào)文是否存在服務(wù)端發(fā)Offer了客戶端不收網(wǎng)段不通ping對(duì)端檢查多網(wǎng)卡和unicast配置Offer和Find都通了方法調(diào)用無(wú)響應(yīng)方法ID或接口版本不匹配抓包看Request/Response的method_id服務(wù)端網(wǎng)卡配置了DHCPIP變化導(dǎo)致客戶端老連接失效靜態(tài)IP或檢測(cè)IP變化后重新Offer服務(wù)端進(jìn)程注冊(cè)本身是運(yùn)行時(shí)動(dòng)作不是啟動(dòng)時(shí)自動(dòng)完成的。我見(jiàn)過(guò)有人把服務(wù)ID寫進(jìn)配置文件就以為萬(wàn)事大吉結(jié)果代碼里根本沒(méi)調(diào)用registerService。所以見(jiàn)到客戶端找不到服務(wù)時(shí)先確認(rèn)服務(wù)端有沒(méi)有真的執(zhí)行到注冊(cè)那行代碼再看SD報(bào)文。4.3 事件組訂閱的坑為什么事件“發(fā)”了客戶端收不到事件的路線不是服務(wù)端廣播給所有客戶端而是客戶端先訂閱某個(gè)事件組服務(wù)端根據(jù)訂閱關(guān)系向該組內(nèi)的客戶端發(fā)送事件。如果事件ID映射錯(cuò)了或者事件組ID兩邊不一致wireshark里能看到客戶端發(fā)了SubscribeEventgroup但服務(wù)端后續(xù)的Publish事件報(bào)文卻發(fā)不出或發(fā)錯(cuò)目的地。排查事件類問(wèn)題我建議抓包順序看三處客戶端有沒(méi)有發(fā)送訂閱請(qǐng)求到服務(wù)端IP服務(wù)端是否收到訂閱請(qǐng)求事件組ID對(duì)得上嗎服務(wù)端在事件觸發(fā)時(shí)有沒(méi)有實(shí)際發(fā)出Publish報(bào)文如果第3步已經(jīng)發(fā)出但客戶端收不到優(yōu)先懷疑多播/路由/防火墻。如果第3步?jīng)]發(fā)出那是服務(wù)端邏輯問(wèn)題——最可能是事件沒(méi)有真正觸發(fā)或者事件組ID映射錯(cuò)了。4.4 方法回調(diào)里的隱形阻塞問(wèn)題有一次我給一個(gè)服務(wù)端加了日志落盤每次方法回調(diào)都寫一行日志。測(cè)試時(shí)發(fā)現(xiàn)客戶端超時(shí)率飆升。抓包看Request和Response之間的時(shí)間間隔正常應(yīng)該幾毫秒實(shí)際到了幾百毫秒。原因是日志是同步刷盤而回調(diào)線程只有那么幾個(gè)所有方法請(qǐng)求都在排隊(duì)等磁盤IO。當(dāng)時(shí)還把問(wèn)題想復(fù)雜了以為是隊(duì)列滿了、以為是網(wǎng)絡(luò)抖動(dòng)。后來(lái)把日志改成異步超時(shí)率立刻歸零。這件事給我的教訓(xùn)是服務(wù)端方法處理必須異步化回調(diào)里只做最輕量的參數(shù)解析和狀態(tài)更新真正的業(yè)務(wù)邏輯和IO丟到獨(dú)立線程池。想做同步的時(shí)候先問(wèn)問(wèn)自己如果這個(gè)操作耗時(shí)100毫秒其他客戶端的請(qǐng)求誰(shuí)處理5. 從Demo到量產(chǎn)穩(wěn)定性和可維護(hù)性的幾個(gè)習(xí)慣5.1 注冊(cè)服務(wù)前的“設(shè)備狀態(tài)初始化”registerService一旦生效客戶端隨時(shí)可能發(fā)現(xiàn)服務(wù)并調(diào)用方法。所以注冊(cè)前服務(wù)端的內(nèi)部狀態(tài)必須完成初始化包括從存儲(chǔ)里恢復(fù)的配置、傳感器校準(zhǔn)值、默認(rèn)參數(shù)等。否則客戶端可能第一次調(diào)用就讀到一個(gè)未初始化的中間狀態(tài)。我見(jiàn)過(guò)一個(gè)服務(wù)端把初始化放在注冊(cè)之后異步去做結(jié)果客戶端在100毫秒內(nèi)就調(diào)用了Getter讀到全是0。我后來(lái)把初始化邏輯全部移到注冊(cè)之前并且加了一個(gè)ready標(biāo)志作為字段字段的第一個(gè)判斷條件未初始化直接返回錯(cuò)誤碼。5.2 進(jìn)程退出時(shí)先注銷再關(guān)閉其余資源服務(wù)端進(jìn)程被kill -9和正常退出客戶端感知速度完全不同。正常退出時(shí)應(yīng)該先把服務(wù)從SD中撤回讓客戶端盡快知道這個(gè)服務(wù)下架了然后銷毀CommonAPI運(yùn)行時(shí)和vsomeip資源。如果不做這一步客戶端只能通過(guò)超時(shí)機(jī)制發(fā)現(xiàn)服務(wù)消失這段時(shí)間里所有方法調(diào)用都在等一個(gè)不可能回來(lái)的回復(fù)。實(shí)踐中我在SIGTERM和SIGINT信號(hào)處理函數(shù)里先調(diào)用注銷邏輯再退出主循環(huán)。注意信號(hào)處理函數(shù)里不要做復(fù)雜操作只置一個(gè)退出標(biāo)志主循環(huán)檢查后執(zhí)行清理退出。5.3 服務(wù)ID和事件ID集中管理車載通信場(chǎng)景里服務(wù)很多參與方也很多服務(wù)ID一旦沖突SD路由會(huì)亂套。我的習(xí)慣是建立一個(gè)配置中心或至少一個(gè)集中的頭文件/JSON目錄把服務(wù)ID、實(shí)例ID、方法ID、事件ID、事件組ID全部登記并在評(píng)審時(shí)把ID變更當(dāng)作接口變更來(lái)對(duì)待。有一次我們兩個(gè)模塊并行開(kāi)發(fā)A模塊把服務(wù)ID寫成0x6001B模塊也寫了0x6001結(jié)果測(cè)試環(huán)境里客戶端一會(huì)兒連到A、一會(huì)兒連到B。抓包看到兩個(gè)服務(wù)端都在Offer同一個(gè)服務(wù)ID問(wèn)題才暴露。從那以后我再也不相信大家各自注意點(diǎn)就行。5.4 事件廣播頻率與訂閱狀態(tài)要結(jié)合服務(wù)端事件不是越快越好也不是越多越好。高頻周期事件如果不看客戶端訂閱情況會(huì)大量占用CPU和網(wǎng)絡(luò)。比如一個(gè)傳感器數(shù)據(jù)事件客戶端沒(méi)訂閱時(shí)你也在周期廣播浪費(fèi)還發(fā)現(xiàn)不了。實(shí)現(xiàn)時(shí)要先查事件組里有沒(méi)有活躍訂閱者有訂閱再?gòu)V播。還有一點(diǎn)周期性事件建議在廣播前判斷數(shù)據(jù)有沒(méi)有變化。變化才發(fā)能大幅降低總線負(fù)載。比如音量事件客戶端只關(guān)心音量變化你每秒發(fā)10次相同音量值既占帶寬又增加對(duì)端解析負(fù)擔(dān)。真正要做到變化才發(fā)可能需要增加一條比較邏輯但收益非常明顯。最后再分享一個(gè)小技巧我個(gè)人做這套東西最大的感受是不要在寫業(yè)務(wù)代碼之前跳過(guò)驗(yàn)證鏈路。無(wú)論你多熟練先跑通官方examples再抓包看一輪SD交互最后才寫自己的Stub實(shí)現(xiàn)。因?yàn)镃ommonAPI和vsomeip的坑太依賴版本和環(huán)境跳過(guò)驗(yàn)證直接寫業(yè)務(wù)出問(wèn)題時(shí)你根本分不清是自己的代碼問(wèn)題還是鏈路問(wèn)題。抓包工具用順手之后你會(huì)發(fā)現(xiàn)自己定位問(wèn)題的速度翻倍。還有一個(gè)小習(xí)慣每次跑服務(wù)端前先清一次可能殘留的舊進(jìn)程否則舊實(shí)例還在Offer服務(wù)新實(shí)例注冊(cè)時(shí)會(huì)和舊實(shí)例爭(zhēng)搶造成服務(wù)在卻連不上的幻覺(jué)。