在上一篇文章中,我們通過直連電腦測試了CH395在組播環境中進行資料的收發,但在實際的使用場景中更多的是將CH395接入區域網環境中。因此,我們需要使用到一個協定——IGMP(Internet Group Management Protocol)。
IGMP和ICMP一樣,都是IP層的一部分。IGMP報文通過IP封包進行傳輸,由20位元組IP首部和8位元組IGMP報文封裝而成,其中IP首部的協定欄位值為2來指明IGMP報文(重點,後續需要用上)。我們通過wireshark抓包可以發現電腦會不斷的傳送IGMP報文請求加入組播(圖1示),而對比發現上一篇文章CH395的組播例程抓包中並沒有發出該報文,所以CH395在路由環境中會不能正常收發組播資料。
圖1 電腦端傳送的IGMP報文
那如何使用CH395傳送該報文呢?這裡教大家一個簡單的快速使用方法。上面我們講過IGMP報是通過IP資料包進行傳輸的,那我們就可以通過CH395的IP-RAW或者MAC-RAW的方式來自己封裝該報文。那IGMP報文的內容該填寫什麼呢?電腦會發出組播請求報文,那我們就根據該報文來填充IP-RAW模式下的報文內容即可。
用wireshark抓取一個電腦端傳送的IGMP報文,分析後會發現如果使用IP-RAW模式,那前面的MAC地址和IP資訊是不用我們填寫了,該內容會在CH395的協定棧中自動填充(MAC-RAW就需要在程式裡面填充全部內容了)。其中圖2中示IGMP Verrsion:3表示IGMPv3協定;Type報文型別,取值為0x22(畫重點,取值關係到傳送出去的IGMP型別);Reserverd: 00 保留;Checksum:校驗值(這個值可以根據演演算法算出來的,但本篇文章不涉及演演算法,所以後文會用最原始的方法填充);Num Group Records 1:已加入1個組;Group Record 224.0.0.251: 加入的組是224.0.0.251。
圖2 IGMP報文內容
分析完報文型別後,根據IP封包的格式,我們應該需要填充內容為報文型別+保留值+校驗值+加入組數量+保留值+要加入的組地址。接下來通過編寫程式來實現CH395傳送該報文。
首先,根據官方手冊圖三示,設定socket為成IP-RAW模式,也就是協定型別為0。
圖三 IP-RAW設定
程式設定了socket3為IP-RAW模式來進行資料傳送,目的地址是224.0.0.22(這是一個特殊地址空間,應用上有區別,下面會特別說明),設定程式如下:
const UINT8 Socket3DesIP[4] = {224,0,0,22}; /* Socket 3目的IP地址(PC) */
void InitSocketParam(void){
memset(&sockinf[0],0,sizeof(sockinf[0])); /* 將SockInf[0]全部清零*/ memcpy(&sockinf[0].IPAddr, BroadcastIP,sizeof(BroadcastIP)); /* 如果啟用UDP SERVER功能,需將目的IP設為廣播地址255.255.255.255 */ // sockinf[0].DesPort = Socket0DesPort; /* 目的埠 */ sockinf[0].SourPort= Socket0SourPort; /* 源埠 */ sockinf[0].ProtoType=PROTO_TYPE_UDP; /* UDP模式 */ memset(&sockinf[1],0,sizeof(sockinf[1])); /* 將SockInf[1]全部清零*/ memcpy(&sockinf[1].IPAddr, BroadcastIP,sizeof(BroadcastIP)); /* 如果啟用UDP SERVER功能,需將目的IP設為廣播地址255.255.255.255 */ // sockinf[1].DesPort = Socket1DesPort; /* 目的埠 */ sockinf[1].SourPort= Socket1SourPort; /* 源埠 */ sockinf[1].ProtoType=PROTO_TYPE_UDP; memset(&sockinf[2],0,sizeof(sockinf[2])); /* 將SockInf[2]全部清零*/ memcpy(&sockinf[2].IPAddr, BroadcastIP,sizeof(BroadcastIP)); /* 如果啟用UDP SERVER功能,需將目的IP設為廣播地址255.255.255.255 */ // sockinf[1].DesPort = Socket1DesPort; /* 目的埠 */ sockinf[2].SourPort= Socket2SourPort; /* 源埠 */ sockinf[2].ProtoType=PROTO_TYPE_UDP; memset(&sockinf[3],0,sizeof(sockinf[3])); /* 將SockInf[3]全部清零*/ memcpy(CH395Inf.IPAddr,Socket3DesIP,sizeof(Socket3DesIP)); /* 將目的IP地址寫入 */ sockinf[3].ProtoType = PROTO_TYPE_IP_RAW; /* IP RAW模式 */ }
設定socket3時,IPRawProto的協定段為02,上面說過IGMP在IP報中的協定型別為2。
const UINT8 IPRawProto = 0x02;
void CH395SocketInitOpen(void) { UINT8 i; /* socket 0為UDP模式 */ CH395SetSocketDesIP(0,sockinf[0].IPAddr); /* 設定socket 0目標IP地址 */ CH395SetSocketProtType(0,sockinf[0].ProtoType); /* 設定socket 0協定型別 */ CH395SetSocketSourPort(0,sockinf[0].SourPort); /* 設定socket 0源埠 */ i = CH395OpenSocket(0); /* 開啟socket 0 */ mStopIfError(i); CH395SetSocketDesIP(1,sockinf[1].IPAddr); /* 設定socket 1目標IP地址 */ CH395SetSocketProtType(1,sockinf[1].ProtoType); /* 設定socket 1協定型別 */ CH395SetSocketSourPort(1,sockinf[1].SourPort); /* 設定socket 1源埠 */ i = CH395OpenSocket(1); /* 開啟socket 1 */ mStopIfError(i); CH395SetSocketDesIP(2,sockinf[2].IPAddr); /* 設定socket 2目標IP地址 */ CH395SetSocketProtType(2,sockinf[2].ProtoType); /* 設定socket 2協定型別 */ CH395SetSocketSourPort(2,sockinf[2].SourPort); /* 設定socket 2源埠 */ i = CH395OpenSocket(2); /* 開啟socket 2 */ mStopIfError(i); /* socket 0為IP RAW模式 */ CH395SetSocketDesIP(3,CH395Inf.IPAddr); /* 設定socket 3目標地址 */ CH395SetSocketProtType(3,sockinf[3].ProtoType); /* 設定socket 3協定型別 */ CH395SetSocketIPRAWProto(3,IPRawProto); /* 設定協定欄位,重點此時的IP報協定段為2 */ i = CH395OpenSocket(3); /* 開啟socket 3 */ mStopIfError(i); /* 檢查是否成功 */ }
完成socket3的IP-RAW模式初始化後就可以封裝IGMP報文了,根據電腦抓包內容,我們在程式裡定義一個Buffer來填充報文。IGMP中Mac地址和目的Ip都是已經在IP-RAW模式下CH395的內部協定棧自動封裝,IP報型別也已填入。因此,Buffer中中需要填入0x22,0x00(IGMP型別、保留值);校驗值隨機填入兩位0x01,0x02(後面會根據抓包修改);保留值填3個0x00;入組數量填1;最後填入Group Record資訊:型別 :04;Len:00;Num:00;組播地址:e0:00:00:fb。
UINT8 MyBuffer1[16] = { 0x22,0x00, 0x01,0x02, 0x00,0x00,0x00,0x01, 0x04,0x00,0x00,0x00,0xe0,0x00,0x00,0xfb, };
MAC-RAW方式需要自己手動全部新增報文內容(Buffer最後一行為0是為了滿足乙太網的最小幀64位元組):
UINT8 MyBuffer1[] = { 0x54,0xbf,0x64,0x1c,0x2a,0x9c,0xb4,0x05,0x00,0x00, 0x00,0x00,0x08,0x00,0x45,0x00,0x00,0x2c,0x02,0x98, 0x00,0x00,0x01,0x02,0x13,0x16,0xc0,0xa8,0x01,0x0A, 0xe0,0x00,0x00,0x16,0x22,0x00,0xf9,0x02,0x00,0x00, 0x00,0x01,0x04,0x00,0x00,0x00,0xe0,0x00,0x00,0xfb, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, };
在初始化的時候,一定設定一個CH395的TTL值(重點):
InitSocketParam(); /* 初始化socket相關變數 */ CH395SocketInitOpen(); /*開啟socket */ CH395SetTTLNum(3,1); /* 設定TTL值 */
為什麼要設定這個TTL值呢?因為我們的目的IP地址是224.0.0.22,而在TCP\IP協定規定中,這個地址其為特殊地址空間不能超過1跳(可以在TCPIP詳解1卷中瞭解),圖三示該IP時設定TTL為1的原因。
圖三 特殊地址空間的TTL值
設定完成後,可以在主函數迴圈呼叫CH395Senddata函數進行傳送:CH395SendData(3,MyBuffer1,16)。提示:一般這個報文需要定時傳送,因為有些路由或者交換機在一定時間內沒有收到該裝置的組播請求會踢出裝置,所以應用時可以通過定時器來定時傳送該報文。
CH395初始化完成並啟動後,通過Wireshark抓包會發現CH395發出了IGMP報文,但是開啟報文分析會出現如圖四示紅色報錯。
圖四 CH395傳送的IGMP報文
報錯內容為校驗值不對,0x01、0x02應該是0xF9、0x02,那我們就把Buffer中的01、02改成0xF9、0x02再看抓包結果如圖五示。
圖五 CH395的IGMP請求加入報文
這時候wiresahrk的報文分析中沒有出現紅色報錯了。但大家可能會發現電腦1.21發出的報文長度為54位元組,而CH395的IP1.200發出的長度為60位元組,是不是報文還是錯的呢?不是,這是因為乙太網幀最小為64位元組,如果小於64位元組就會有padding段填充,同時wireshark會不顯示4位元組的校驗欄位,減去該4位元組就為60字長,圖六示60位元組的原因。
圖六 CH395的60字長IGMP報文
到這裡CH395的IGMP就沒問題了,CH395的IP-RAW和MAC-RAW方式例程已放到下面自取,關於IGMP報文裡的校驗值其實可以通過演演算法進行填充,但這裡只是教大家一個快速簡單的應用方法,如果感興趣的可以根據網上的演演算法例程自行新增。
例程連結:https://files.cnblogs.com/files/blogs/805237/Socket-UdpMulticast.rar?t=1700655709&download=true