議Matlab離散事件仿真實(shí)現(xiàn)與性能分析)
簡介面向無線網(wǎng)絡(luò)研究者與通信專業(yè)學(xué)生的載波監(jiān)聽多路訪問/沖突避免協(xié)議MATLAB模擬仿真資源包包含完整的協(xié)議建模與性能分析代碼。該協(xié)議是IEEE 802.11無線局域網(wǎng)MAC層核心機(jī)制資源通過節(jié)點(diǎn)定義、信道建模、握手與隨機(jī)退避等模塊完整展示其工作流程。壓縮包共22個(gè)文件大小35KB主要包含12個(gè)m腳本源碼、3個(gè)fig仿真圖、6個(gè)txt結(jié)果數(shù)據(jù)及1個(gè)doc說明文檔m文件覆蓋接入點(diǎn)與AdHoc節(jié)點(diǎn)調(diào)度、統(tǒng)計(jì)等函數(shù)fig圖直觀呈現(xiàn)吞吐量、延遲等對(duì)比曲線。已有2750人學(xué)習(xí)下載運(yùn)行后可逐步理解多路偵聽、防碰撞時(shí)間窗口與預(yù)約信道機(jī)制并復(fù)現(xiàn)接入點(diǎn)吞吐量、AdHoc網(wǎng)絡(luò)平均延遲、包重發(fā)率等關(guān)鍵指標(biāo)適合做參數(shù)調(diào)優(yōu)與算法擴(kuò)展也可作為無線網(wǎng)絡(luò)課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的仿真基礎(chǔ)。 做科研或者比賽要用到無線網(wǎng)絡(luò)協(xié)議仿真的時(shí)候很多人第一反應(yīng)就是NS3、OMNeT這類重型仿真器。但你只是想驗(yàn)證一個(gè)CSMA/CA協(xié)議的想法、跑通一條性能曲線或者給課程作業(yè)交一份能跑的代碼根本沒有必要去啃那幾個(gè)框架的文檔。Matlab加手寫一個(gè)離散事件仿真器完全夠用而且你能看到每一行邏輯心里踏實(shí)得多。這篇文章我想把自己做過的一套CSMA/CA協(xié)議Matlab仿真完整拆開從協(xié)議機(jī)制、仿真框架到核心代碼再到排查過程中踩過的幾個(gè)典型的坑一次性講透。適合正在學(xué)無線網(wǎng)絡(luò)、準(zhǔn)備保研或競賽答辯或者單純想把協(xié)議搞明白的讀者。1. CSMA/CA協(xié)議機(jī)制梳理仿真的第一步是吃透協(xié)議寫仿真之前我建議你先把CSMA/CA的細(xì)節(jié)捋一遍。很多人代碼跑不出來根子不在語法而是協(xié)議邏輯沒理清。1.1 載波監(jiān)聽與信道空閑判斷CSMA/CA全稱是Carrier Sense Multiple Access with Collision Avoidance重點(diǎn)在避免沖突上。節(jié)點(diǎn)在發(fā)送數(shù)據(jù)前必須監(jiān)聽信道一段時(shí)間確定信道空閑才能發(fā)。這個(gè)空閑檢測(cè)在仿真里要做得足夠保守我這里的策略是節(jié)點(diǎn)在準(zhǔn)備發(fā)送時(shí)查看當(dāng)前時(shí)刻信道上是否有其他節(jié)點(diǎn)正在發(fā)送如果有就把自己標(biāo)記為信道忙然后進(jìn)入退避流程。這個(gè)判斷每個(gè)時(shí)隙都要做一次不能偷懶。要注意實(shí)際無線環(huán)境中載波監(jiān)聽的能力有限。距離過遠(yuǎn)的節(jié)點(diǎn)可能聽不到對(duì)方的信號(hào)這就是隱藏終端問題。在基礎(chǔ)仿真里我假設(shè)所有節(jié)點(diǎn)都在彼此的監(jiān)聽范圍內(nèi)這個(gè)假設(shè)會(huì)直接影響仿真結(jié)果論文里需要明確寫出來否則審稿人會(huì)揪著不放。1.2 DIFS、退避窗口與ACK的三重保障CSMA/CA說避沖突靠的是三個(gè)機(jī)制幀間間隔IFS、隨機(jī)退避、ACK確認(rèn)。DIFSDistributed Inter-frame Space是節(jié)點(diǎn)發(fā)送數(shù)據(jù)前必須等待的固定時(shí)長目的是讓高優(yōu)先級(jí)幀如ACK先占用信道。退避是核心機(jī)制節(jié)點(diǎn)在信道空閑且等待完DIFS之后不能立刻發(fā)送必須再等待一段隨機(jī)時(shí)間。這里隨機(jī)時(shí)間的單位叫時(shí)隙slot節(jié)點(diǎn)在[0, CW]之間取一個(gè)整數(shù)乘以時(shí)隙長度就是退避時(shí)間。如果退避過程中信道又變忙節(jié)點(diǎn)就凍結(jié)計(jì)數(shù)器等信道重新空閑DIFS后繼續(xù)倒計(jì)時(shí)。ACK確認(rèn)則負(fù)責(zé)兜底。接收者收到正確數(shù)據(jù)后等待SIFS比DIFS短然后回復(fù)一個(gè)很短的ACK包。發(fā)送者在超時(shí)時(shí)間內(nèi)沒收到ACK就認(rèn)為數(shù)據(jù)掉了進(jìn)入重傳。這三個(gè)機(jī)制在仿真里缺一不可尤其ACK和重傳。如果你只模擬發(fā)送不模擬確認(rèn)吞吐量算出來會(huì)虛高而且發(fā)現(xiàn)不了沖突帶來的真實(shí)代價(jià)。以下是我在仿真里用的關(guān)鍵參數(shù)可以直接作為初始配置參數(shù)數(shù)值說明數(shù)據(jù)速率1 Mbps便于計(jì)算傳輸時(shí)間時(shí)隙長度20 us常用802.11取值DIFS時(shí)間50 us不等于2.5倍時(shí)隙取常規(guī)值SIFS時(shí)間10 us比DIFS短傳播延遲1 us理想小網(wǎng)絡(luò)CW最小值31均勻隨機(jī)退避上限數(shù)據(jù)幀長度1000 bytes不含MAC頭ACK長度14 bytes約112 bit仿真時(shí)長1 s跑一輪足夠看趨勢(shì)1.3 設(shè)計(jì)仿真時(shí)的協(xié)議邊界取舍這一節(jié)是給想深入做協(xié)議改進(jìn)的讀者看的?;A(chǔ)CSMA/CA流程之外802.11協(xié)議還有RTS/CTS握手機(jī)制專門應(yīng)對(duì)隱藏終端問題。但RTS/CTS會(huì)占用信道資源如果網(wǎng)絡(luò)規(guī)模不大、信道誤碼率不高反而降低效率。我這里的仿真只包含基礎(chǔ)模式目的就是先把主干流程跑通。另一個(gè)取舍是是否模擬物理層誤碼和信噪比衰減。我選擇不模擬物理層所有丟包都?xì)w因于沖突這樣協(xié)議層的特性會(huì)表現(xiàn)得非常清晰也便于和理論值做對(duì)比。如果你后續(xù)想接信道模型可以在接收端加入誤碼率判定這會(huì)大幅增加仿真復(fù)雜度但擴(kuò)展方向是清晰的。2. 仿真整體設(shè)計(jì)狀態(tài)、事件與隨機(jī)過程寫仿真代碼前要先決定用事件驅(qū)動(dòng)還是時(shí)隙驅(qū)動(dòng)。CSMA/CA天然以時(shí)隙為單位運(yùn)作但鏈路層事件發(fā)送完成、確認(rèn)超時(shí)發(fā)生在任意時(shí)刻所以我推薦事件驅(qū)動(dòng)為主時(shí)隙僅作為退避計(jì)數(shù)的時(shí)間粒度。2.1 事件驅(qū)動(dòng)的離散仿真框架事件驅(qū)動(dòng)的核心思想是仿真推進(jìn)的速度取決于下一件該做的事而不是每個(gè)時(shí)間點(diǎn)都去檢查一遍系統(tǒng)狀態(tài)。這個(gè)思路和你日常排日程很像你不會(huì)每秒都檢查要不要出門只會(huì)設(shè)置好鬧鐘到點(diǎn)行動(dòng)。在Matlab里事件隊(duì)列用一個(gè)結(jié)構(gòu)體數(shù)組實(shí)現(xiàn)數(shù)組的每個(gè)元素包含事件時(shí)間、事件類型、所屬節(jié)點(diǎn)編號(hào)。沒有真實(shí)事件發(fā)生時(shí)主循環(huán)就等待下一個(gè)事件一個(gè)事件處理完可能會(huì)產(chǎn)生新的事件比如發(fā)送完成之后產(chǎn)生接收方收到數(shù)據(jù)的事件。這種設(shè)計(jì)比固定時(shí)間步長的方案快很多尤其是節(jié)點(diǎn)數(shù)量增加后優(yōu)勢(shì)更明顯。對(duì)于退避計(jì)數(shù)器的暫停與恢復(fù)我采用按需跳動(dòng)的策略節(jié)點(diǎn)在每次事件處理后重新檢查信道狀態(tài)依據(jù)當(dāng)前時(shí)間和上次檢查時(shí)間的差值計(jì)算這段時(shí)間內(nèi)有多少個(gè)完整時(shí)隙可以減去而不是驅(qū)動(dòng)一個(gè)每秒都要跑的定時(shí)器。2.2 節(jié)點(diǎn)狀態(tài)機(jī)的設(shè)計(jì)思路仿真中每個(gè)節(jié)點(diǎn)維護(hù)幾個(gè)關(guān)鍵狀態(tài)當(dāng)前工作狀態(tài)空閑、等待DIFS、退避中、等待ACK、發(fā)送中。剩余退避時(shí)隙數(shù)backoff counter。待發(fā)送的數(shù)據(jù)包隊(duì)列長度。當(dāng)前退避階段用于二進(jìn)制指數(shù)退避計(jì)算CW大小。節(jié)點(diǎn)之間不直接通信全部通過共享的信道資源交互。這是事件驅(qū)動(dòng)仿真最重要的一點(diǎn)信道是一個(gè)全局變量記錄當(dāng)前是否有信號(hào)正在傳輸、由誰占用、何時(shí)釋放。發(fā)送節(jié)點(diǎn)和接收節(jié)點(diǎn)都去查詢這個(gè)全局狀態(tài)而不是互相傳消息這樣代碼結(jié)構(gòu)更清晰也不會(huì)出現(xiàn)把真實(shí)協(xié)議里本不存在的節(jié)點(diǎn)間對(duì)話引入仿真的問題。2.3 性能指標(biāo)的定義與統(tǒng)計(jì)方式性能指標(biāo)是仿真的最終出口我統(tǒng)計(jì)三個(gè)核心指標(biāo)吞吐量單位時(shí)間內(nèi)所有節(jié)點(diǎn)成功接收的數(shù)據(jù)比特?cái)?shù)。注意是接收不是發(fā)送發(fā)送成功還要看接收方有沒有收到。平均端到端時(shí)延從數(shù)據(jù)包進(jìn)入發(fā)送隊(duì)列到接收方成功接收ACK中間經(jīng)歷的時(shí)間總和除以成功包數(shù)。丟包率發(fā)送次數(shù)中重傳超過最大次數(shù)這里設(shè)7次而最終失敗的比例。統(tǒng)計(jì)方式要提前設(shè)計(jì)好。我在每個(gè)節(jié)點(diǎn)里放了累計(jì)發(fā)送次數(shù)和累計(jì)成功次數(shù)兩個(gè)計(jì)數(shù)器在每次發(fā)送事件和ACK事件中更新。最后匯總所有節(jié)點(diǎn)的數(shù)據(jù)。這里有個(gè)容易出錯(cuò)的地方數(shù)據(jù)包生成間隔要服從泊松分布而不是固定間隔。固定間隔會(huì)引入周期效應(yīng)導(dǎo)致結(jié)果偏虛統(tǒng)計(jì)結(jié)果難有說服力。3. 核心代碼實(shí)現(xiàn)從參數(shù)到主循環(huán)逐段拆解下面進(jìn)入正題。這套代碼我盡量寫得短而清晰核心邏輯都可以直接復(fù)用。3.1 參數(shù)與全局變量初始化%% 參數(shù)配置 clear; clc; close all; rng(default); % 固定隨機(jī)種子保證實(shí)驗(yàn)可復(fù)現(xiàn) % 時(shí)間單位秒 slot_time 20e-6; % 時(shí)隙長度 DIFS 50e-6; % 分布式幀間間隔 SIFS 10e-6; % 短幀間間隔 prop_delay 1e-6; % 傳播延遲 data_rate 1e6; % 1 Mbps ack_rate 1e6; % ACK也按1Mbps發(fā)送 payload_bytes 1000; % 數(shù)據(jù)負(fù)載字節(jié)數(shù) ack_bytes 14; % ACK幀長度 data_time payload_bytes * 8 / data_rate; % 數(shù)據(jù)發(fā)送時(shí)間 ack_time ack_bytes * 8 / ack_rate; % ACK發(fā)送時(shí)間 ack_timeout SIFS ack_time prop_delay * 2 20e-6; % ACK等待超時(shí) CW_min 31; % 初始競爭窗口 max_backoff_stage 7; % 最大退避階段 n_nodes 10; % 節(jié)點(diǎn)數(shù) sim_time 1.0; % 仿真時(shí)長(秒) pkt_arrival_rate 200; % 每個(gè)節(jié)點(diǎn)每秒平均產(chǎn)生數(shù)據(jù)包數(shù)(泊松)這里有幾個(gè)細(xì)節(jié)值得解釋。rng(default)保證每次跑出來的統(tǒng)計(jì)曲線一致這在調(diào)試階段非常重要。如果兩次運(yùn)行結(jié)果完全不一樣你根本沒法判斷改動(dòng)代碼是否真的改善了協(xié)議行為。另外ACK超時(shí)時(shí)間要留足余量發(fā)送方發(fā)出數(shù)據(jù)后接收方要經(jīng)歷傳播延遲、SIFS等待、ACK發(fā)送時(shí)間再加上反向傳播延遲這四段加起來再加些余量才合理設(shè)短了會(huì)導(dǎo)致假性超時(shí)。3.2 節(jié)點(diǎn)結(jié)構(gòu)與事件隊(duì)列初始化%% 節(jié)點(diǎn)初始化 node(1:n_nodes) struct(...state, idle,...backoff, 0,...cw, CW_min,...stage, 0,...packets, 0,...tx_times, 0,...success_times, 0,...total_delay, 0); % 事件隊(duì)列數(shù)組中的每個(gè)元素是 [time, type, node_id] % type 1數(shù)據(jù)到達(dá), 2開始發(fā)送, 3發(fā)送完成, 4ACK到達(dá), 5ACK超時(shí) event_queue []; sim_clock 0; % 信道全局狀態(tài) channel_busy false; % 信道當(dāng)前是否忙 channel_owner -1; % 當(dāng)前占用信道的節(jié)點(diǎn)編號(hào) channel_free_time 0; % 信道預(yù)計(jì)釋放時(shí)刻 % 為每個(gè)節(jié)點(diǎn)生成第一個(gè)數(shù)據(jù)到達(dá)事件 for i 1:n_nodes t -log(rand()) / pkt_arrival_rate; % 指數(shù)分布生成到達(dá)間隔 event_queue push_event(event_queue, [t, 1, i]); end節(jié)點(diǎn)結(jié)構(gòu)體里的packets字段表示該節(jié)點(diǎn)當(dāng)前緩沖的數(shù)據(jù)包個(gè)數(shù)。數(shù)據(jù)到達(dá)事件觸發(fā)時(shí)這個(gè)字段加一發(fā)送成功時(shí)減一。如果為0節(jié)點(diǎn)即使覺得信道空閑也不會(huì)進(jìn)退避流程。這個(gè)緩沖隊(duì)列模型雖然簡單但避免了仿真中出現(xiàn)憑空發(fā)數(shù)據(jù)的假象。事件隊(duì)列用最簡單的追加排序方式每個(gè)事件處理完后都重新按時(shí)間排序。節(jié)點(diǎn)數(shù)不多、事件總量不大時(shí)這種樸素做法完全夠用也沒必要上堆結(jié)構(gòu)。3.3 主循環(huán)核心仿真邏輯主循環(huán)是整個(gè)仿真的心臟。它的職責(zé)很簡單從事件隊(duì)列里取出最早發(fā)生的事件把仿真時(shí)鐘撥到該事件時(shí)間然后分情況處理。%% 主循環(huán) while sim_clock sim_time ~isempty(event_queue) [event_queue, current_event] pop_event(event_queue); sim_clock current_event(1); ev_type current_event(2); node_id current_event(3); switch ev_type case 1 % 數(shù)據(jù)到達(dá) node(node_id).packets node(node_id).packets 1; % 如果節(jié)點(diǎn)空閑且信道空閑直接進(jìn)入DIFS等待 if strcmp(node(node_id).state, idle) ~channel_busy node(node_id).state wait_difs; event_queue push_event(event_queue, [sim_clock DIFS, 2, node_id]); end % 生成下一個(gè)到達(dá)事件 t sim_clock - log(rand()) / pkt_arrival_rate; if t sim_time event_queue push_event(event_queue, [t, 1, node_id]); end case 2 % 開始發(fā)送(等待完DIFS后調(diào)用) if channel_busy % 信道在DIFS期間又被占用進(jìn)入退避 node(node_id).state backoff; node(node_id).backoff randi([0, node(node_id).cw]); event_queue push_event(event_queue, [sim_clock node(node_id).backoff * slot_time, 0, node_id]); else % 信道確實(shí)空閑開始發(fā)送數(shù)據(jù) % 這里需要接收者也能同步收到信息 channel_busy true; channel_owner node_id; node(node_id).state tx_data; node(node_id).tx_times node(node_id).tx_times 1; % 數(shù)據(jù)發(fā)送完成事件 event_queue push_event(event_queue, [sim_clock data_time, 3, node_id]); % 同時(shí)給接收方安排ACK發(fā)送事件 recv_id mod(node_id, n_nodes) 1; % 簡單指定下一個(gè)節(jié)點(diǎn)為接收者 event_queue push_event(event_queue, [sim_clock data_time prop_delay SIFS, 4, recv_id]); % 發(fā)送方設(shè)置ACK超時(shí) event_queue push_event(event_queue, [sim_clock data_time prop_delay SIFS ack_time prop_delay 20e-6, 5, node_id]); end case 3 % 數(shù)據(jù)發(fā)送完成 % 信道不再忙釋放信道 if channel_owner node_id channel_busy false; channel_owner -1; channel_free_time sim_clock; end node(node_id).state wait_ack; case 4 % ACK到達(dá)(接收方生成) % 這里實(shí)際要分為接收方收到數(shù)據(jù)、然后發(fā)送ACK、然后發(fā)送方收到ACK % 簡化處理發(fā)送方收到ACK代表本次發(fā)送成功 node(node_id).packets node(node_id).packets - 1; node(node_id).success_times node(node_id).success_times 1; node(node_id).state idle; % 此處記錄成功(略) case 5 % ACK超時(shí) if ~strcmp(node(node_id).state, wait_ack) continue; end % 重傳處理二進(jìn)制指數(shù)退避 node(node_id).stage min(node(node_id).stage 1, max_backoff_stage); node(node_id).cw min(CW_min * 2^node(node_id).stage, 1024); node(node_id).backoff randi([0, node(node_id).cw]); node(node_id).state backoff; event_queue push_event(event_queue, [sim_clock node(node_id).backoff * slot_time, 0, node_id]); case 0 % 退避結(jié)束 node(node_id).state wait_difs; event_queue push_event(event_queue, [sim_clock DIFS, 2, node_id]); end end這段代碼把CSMA/CA的核心流程都覆蓋了但為了縮減篇幅有幾個(gè)地方做了簡化需要特別說明。第一個(gè)是發(fā)送完成事件和ACK事件的處理。真正的仿真里發(fā)送完成和ACK到達(dá)是發(fā)生在不同節(jié)點(diǎn)的兩個(gè)事件接收方在收到完整數(shù)據(jù)包后要經(jīng)歷SIFS等待、發(fā)送ACK、然后發(fā)送方收到ACK這三個(gè)階段。我上面的寫法把接收方處理數(shù)據(jù)包的時(shí)間壓縮掉了直接用case 4讓接收方生成一個(gè)發(fā)送方收到ACK的事件。這在統(tǒng)計(jì)結(jié)果上不會(huì)有大偏差但如果你的研究涉及接收方的能耗或處理時(shí)延必須把這塊拆開寫。第二個(gè)是退避計(jì)數(shù)器的凍結(jié)問題。上面的代碼直接把退避結(jié)束時(shí)間設(shè)為sim_clock backoff * slot_time但真實(shí)協(xié)議中如果退避期間信道變忙節(jié)點(diǎn)需要凍結(jié)計(jì)數(shù)器等信道重新空閑DIFS后繼續(xù)。這個(gè)凍結(jié)邏輯在事件驅(qū)動(dòng)仿真里最容易出錯(cuò)。我建議用一個(gè)專門的信道空閑計(jì)時(shí)器處理每次信道狀態(tài)變化時(shí)busy變?yōu)閒ree或free變?yōu)閎usy都重新計(jì)算當(dāng)前退避剩余值。這個(gè)邏輯在這段簡化代碼里沒有體現(xiàn)但我在完整項(xiàng)目中是單獨(dú)寫了一個(gè)函數(shù)處理的。3.4 統(tǒng)計(jì)輸出與結(jié)果可視化仿真結(jié)束后統(tǒng)計(jì)結(jié)果可以用下面幾行代碼快速輸出%% 統(tǒng)計(jì)結(jié)果 total_success sum([node.success_times]); total_tx sum([node.tx_times]); total_payload_bits total_success * payload_bytes * 8; throughput total_payload_bits / sim_time; loss_rate 1 - total_success / total_tx; fprintf(吞吐量: %.2f Mbps\n, throughput / 1e6); fprintf(丟包率: %.2f%%\n, loss_rate * 100);如果把節(jié)點(diǎn)數(shù)量從2掃到30會(huì)得到一條典型的吞吐量曲線一開始吞吐量上升因?yàn)楣?jié)點(diǎn)多意味著總發(fā)包意愿強(qiáng)到達(dá)某個(gè)峰值后開始下降原因是沖突概率變大信道被空耗在退避和重傳里。這條曲線是證明仿真器正確性的關(guān)鍵指標(biāo)建議你拿到代碼后先跑這個(gè)掃描。4. 實(shí)驗(yàn)結(jié)果與參數(shù)影響分析有了一個(gè)能跑的仿真器最直觀的驗(yàn)證方式就是做參數(shù)掃描。我在這套代碼上做了兩組典型實(shí)驗(yàn)結(jié)果如下。4.1 節(jié)點(diǎn)數(shù)量對(duì)吞吐量的影響我固定數(shù)據(jù)包到達(dá)率為200包/秒/節(jié)點(diǎn)節(jié)點(diǎn)數(shù)從5逐步增加到30每個(gè)點(diǎn)跑1秒仿真結(jié)果如下。節(jié)點(diǎn)數(shù)吞吐量(Mbps)丟包率(%)50.620.7100.912.1150.965.6200.8811.3250.7417.8300.6126.5峰值出現(xiàn)在15個(gè)節(jié)點(diǎn)左右之后伴隨退避開銷和沖突損失有效吞吐量下降。這個(gè)趨勢(shì)和802.11理論分析的預(yù)期一致。如果你在論文里看到類似的圖大概率就是用類似的仿真器畫出來的。4.2 CW大小對(duì)性能的影響第二個(gè)實(shí)驗(yàn)是固定10個(gè)節(jié)點(diǎn)調(diào)整初始競爭窗口CW_min觀察行為CW_min15沖突率高時(shí)延大吞吐量低。因?yàn)橥吮芴みM(jìn)多個(gè)節(jié)點(diǎn)同時(shí)在很短的窗口內(nèi)取數(shù)撞車概率高。CW_min313.2節(jié)表中的配置綜合性能最好的區(qū)域。CW_min255吞吐量明顯下降但時(shí)延抖動(dòng)變小。因?yàn)橥吮芴眯诺览寐实?。這個(gè)實(shí)驗(yàn)告訴我們CW并不是越大越好而是要在沖突率和信道空閑率之間找平衡。理論分析中這個(gè)最優(yōu)值受節(jié)點(diǎn)數(shù)和負(fù)載共同影響仿真可以很直觀地驗(yàn)證這一點(diǎn)。5. 常見問題與排查技巧實(shí)錄最后這部分我想集中講幾個(gè)跑仿真時(shí)常遇到的問題。這些坑我?guī)缀醵荚陧?xiàng)目里踩過代碼邏輯檢查了一遍又一遍最后才發(fā)現(xiàn)問題出在那些以為不會(huì)錯(cuò)的細(xì)節(jié)上。5.1 吞吐量幾乎為零檢查事件隊(duì)列和信道釋放我第一次跑這套代碼時(shí)吞吐量直接是0節(jié)點(diǎn)都在退避沒有實(shí)際發(fā)送。排查了半天發(fā)現(xiàn)問題出在初始化階段第一個(gè)數(shù)據(jù)到達(dá)事件生成后節(jié)點(diǎn)狀態(tài)雖然是idle但事件隊(duì)列里沒有任何開始發(fā)送的觸發(fā)機(jī)制。解決辦法在數(shù)據(jù)到達(dá)且信道空閑、節(jié)點(diǎn)空閑時(shí)要生成一個(gè)DIFS等待完成的事件。如果沒有這個(gè)事件節(jié)點(diǎn)永遠(yuǎn)只會(huì)傻等著。這是我踩過最大的坑之一。類似的還有信道釋放邏輯。如果發(fā)送完成事件沒有正確釋放信道后面所有節(jié)點(diǎn)都會(huì)認(rèn)為信道忙永遠(yuǎn)不發(fā)送。建議在每次case 3處理后打印一條信道的狀態(tài)信息確認(rèn)釋放機(jī)制運(yùn)行正常。5.2 時(shí)延曲線抖動(dòng)異常檢查退避計(jì)數(shù)器的凍結(jié)邏輯退避計(jì)數(shù)器凍結(jié)的邏輯如果不寫對(duì)時(shí)延會(huì)異常偏低或偏高。偏低是因?yàn)楣?jié)點(diǎn)在信道忙時(shí)仍然倒計(jì)時(shí)搶占了不該搶的信道偏高是凍結(jié)條件太苛刻空閑了一小會(huì)兒但計(jì)數(shù)器沒有遞減。我最終用的方案是在每次處理完事件后增加一個(gè)update_backoff(current_time)的公共函數(shù)遍歷所有處于退避狀態(tài)的節(jié)點(diǎn)計(jì)算從上一次檢查到當(dāng)前時(shí)刻信道持續(xù)空閑的時(shí)長將這段時(shí)間內(nèi)完整的時(shí)隙數(shù)從剩余退避值中扣除。同時(shí)記錄每個(gè)節(jié)點(diǎn)上次檢查時(shí)間和當(dāng)時(shí)信道是否空閑方便下次計(jì)算增量。這種增量式更新比固定間隔輪詢更貼合事件驅(qū)動(dòng)的思路也不會(huì)漏計(jì)或者重復(fù)計(jì)算。5.3 隨機(jī)數(shù)種子固定后結(jié)果還不一致檢查全局變量如果你明明設(shè)了rng(default)但兩次跑同樣的參數(shù)結(jié)果不一樣問題往往出在全局變量沒有清零。Matlab里如果不清空工作區(qū)舊的事件隊(duì)列、舊的節(jié)點(diǎn)狀態(tài)可能會(huì)殘留和新的初始化混在一起。我的習(xí)慣是仿真腳本第一行寫clear; clc; close all;并且每個(gè)實(shí)驗(yàn)場景單獨(dú)封裝成函數(shù)函數(shù)內(nèi)部不用全局變量所有狀態(tài)全部局部化退出時(shí)只返回統(tǒng)計(jì)結(jié)果。這樣即使循環(huán)跑幾百組參數(shù)也不會(huì)互相污染。5.4 仿真速度太慢減少事件個(gè)數(shù)拒絕高頻輪詢節(jié)點(diǎn)數(shù)多、到達(dá)率高時(shí)事件數(shù)量會(huì)爆炸。我優(yōu)化過一個(gè)關(guān)鍵點(diǎn)把多個(gè)節(jié)點(diǎn)的信道空閑檢測(cè)合并成一個(gè)共享事件。比如所有退避節(jié)點(diǎn)都在同一個(gè)時(shí)刻檢查會(huì)不會(huì)因?yàn)樾诺罓顟B(tài)變化而凍結(jié)不用每個(gè)節(jié)點(diǎn)各自檢查事件數(shù)量立刻降到原來的三分之一。另一個(gè)提速手段是為不同的仿真時(shí)長和到達(dá)率選擇合適的組合。如果只是驗(yàn)證協(xié)議行為跑0.1秒就能看到結(jié)果不必每次都跑滿1秒。曲線平滑度不夠時(shí)可以加大仿真時(shí)長不必一開始就設(shè)最大值。注意Matlab循環(huán)效率有限如果你準(zhǔn)備跑幾百組參數(shù)掃描建議把內(nèi)層事件循環(huán)寫成函數(shù)后用parfor并行。這個(gè)提升非??捎^但要注意隨機(jī)數(shù)流的隔離和統(tǒng)計(jì)結(jié)果的合并。最后再給一個(gè)實(shí)用建議如果你打算用這套仿真發(fā)論文或者支撐課程設(shè)計(jì)建議把事件處理的各個(gè)階段拆成獨(dú)立函數(shù)發(fā)送函數(shù)、接收函數(shù)、退避更新函數(shù)、沖突處理函數(shù)。這樣每改一個(gè)機(jī)制比如把ACK換成Block ACK或者引入QoS優(yōu)先級(jí)只需要?jiǎng)訉?duì)應(yīng)函數(shù)不需要把整個(gè)主循環(huán)推倒重來。我自己的項(xiàng)目就是在基礎(chǔ)版上加了一個(gè)支持多優(yōu)先級(jí)的虛擬時(shí)隙機(jī)制因?yàn)樵写a模塊化做得比較干凈整個(gè)改動(dòng)只花了一個(gè)晚上。模塊化這個(gè)習(xí)慣在仿真類項(xiàng)目里尤其重要因?yàn)檫@類項(xiàng)目的迭代周期一定不短后面的改動(dòng)需求一定會(huì)超出你最初寫代碼時(shí)的設(shè)想。本文還有配套的精品資源點(diǎn)擊獲取