一、說明
1.1 標准組播解釋
通信分為單播、多播(即組播)、廣播三種方式
單播指發送者發送之后,IP數據包被路由器發往目的IP指定的唯一一台設備的通信形式,比如你現在與web服務器通信就是單播形式
廣播指發送者發送之后,IP數據包被路由器發給與其連接的所有設備的通信形式
組播指發送者發送之后,IP數據包被路由器發往目的IP對應組播組名下所有主機的通信形式
1.2 個人理解組播解釋
對於標准的組播解釋,說明似乎還算是清楚的,但具體到技術就有很多問題。比如我將數據包發往一個組播地址,這個組播地址對應一台物理設備嗎?如果不是一台物理設備那誰依據什么向哪些主機發送該數據包等等。
結合各資料和自己測試的情況總結出了以下幾點:
1) 編寫發送程序:組播數據包是且只是目的IP是組播保留地址的UDP數據包,與正常UDP數據包的區別只是其目的IP是組播保留IP
2) 發送數據包主機:網卡在看到目的IP是組播保留IP后,自動將目的mac地址改成組播mac地址然后向其各端口都發送出去
3) 交換機:交換機在接收到數據包之后,通過目的mac地址認識到這是一個組播數據包,修改源mac為自己mac、保持目的mac為組播mac不變向其各端口都發送出去(交換機對組播包的處理和廣播包應該是一樣的,或者說對於交換機只有單播包和廣播包)
4) 路由器:路由器在接收到數據包之后,通過目的mac地址或目的ip地址認識到這是一個組播數據包,修改源mac為自己mac、保持目的mac為組播mac不變,保持源IP不變、保持目的IP為組播保留IP不變,依照與路由表類似的“組播組地址表”向與目的IP匹配的一個或多端口將數據包發送出去
5) 接收數據包主機:接收數據包主機要想接收到發送主機發送的數據包,首先他要(向路由器說明)加入發送者發往的組播組,然后他要在本地啟動一個進程監聽發送者發往的端口
6) 組播需要硬件支持,有些路由器是不支持組播的,就直觀感受看如果在全球實現組播那維護“組播組地址表”會給路由器帶來很大負擔路由大廠商應該也不是很願意支持組播;也就是說理論上組播可以在廣域網上實現,但其實一般只在局域網中(能夠)使用。
7) 注意從本質上而言,接收組播數據包的主機只是啟了一個UDP監聽,他本身並不能識別是組播發過來的數據包還是直接發過來的數據包(除非對收到的數據包的目的IP是否為組播IP進行判斷,但獲取目的IP是件很麻煩的事)。也就是說該監聽不只是可以接收組播數據包,任何其他如果主機直接向該監聽的端口發送UDP數據包該主機也是可以接收到的(已確認過)。
假設使用組播地址為239.255.255.252,使用組播端口為23456,通信舉例如下:
發送者S----發送者向239.255.255.252:23456發送一個UDP數據包Packet1
接收者R1(假設其IP地址為134.192.1.100)----第一步啟動進程監聽239.255.255.252:23456;第二步通過setsockopt加入組播組(239.255.255.252)
接收者R2(假設其IP地址為134.192.1.101)----第一步啟動進程監聽239.255.255.252:23456;第二步通過setsockopt加入組播組(239.255.255.252)
最終效果----發送者發往239.255.255.252:23456的udp數據包,R1的23456端口收到一份Packet1,R2的23456端口收到一份Packet1
1.3 誰是服務端引發的混亂
我在前面一直使用“發送者”、“接收者”,而沒有使用“服務端”、“客戶端”,因為“服務端”和“客戶端”在組播中容易引發混亂。
在我們一般的socket編程中都是服務端去bind;但在組播中是反過來,客戶端(接收者)去bind,而發送者(服務端)是不用去bind的(注意是不用而不是不能,你非要bind也是可以的,bind和不bind只是使用固定端口還是使用隨機端口的區別)。
有些小伙伴意識到了這個問題,為了與習慣一致,所以直接將發送者稱為客戶端,接收者稱為服務端。
其實這種叫法是不合適的,以初高中常見的電腦課場景為例:老師控制所有電腦顯示老師的電腦操作,這時作為發送者的老師電腦從認知上確實應該是服務端而不是客戶端才對。
如果你讀了半天沒聽懂這里在說什么,那不必在意,記得組播中盡里使用“發送者”和“接收者”,少用“服務端”和“客戶端”就對了。
1.4 監聽組播地址與監聽本地地址的區別討論
在一般的關於組播的文章中,接收者設置的監聽地址都是本地地址(如134.192.1.100:23456);但在前面1.2中我們要求接收者R1和R2監聽的不是本地地址,而是組播地址239.255.255.252:23456。這是為什么呢?
首先說,這兩種監聽形式都可以接收到發往239.255.255.252:23456的組播數據包。他們之間的區別是,監聽239.255.255.252:23456地址能且只能接收發往239.255.255.252:23456的數據包,而監聽134.192.1.100:23456除了能接收發往239.255.255.252:23456的數據包外還能接收直接發往134.192.1.100:23456的數據包。
這樣造成的安全區別是,假如接收者實現了這么一項功能:接收到一個打開telnet的命令就直接開啟本機的telnet服務。如果監聽的是239.255.255.252:23456那么攻擊者只有通過局域網內主機發出組播數據包才能打開telnet,但如果監聽的是134.192.1.100:23456那么攻擊者可以遠程直接向134.192.1.100:23456發送數據包開啟telnet。
linux上可以通過SO_BINDTODEVICE選項綁定網卡然后設置監聽組播地址,但Windows沒有實現SO_BINDTODEVICE暫時也不知道如何實現類似功能,所以在下邊代碼中我也只是linux系統設置了監聽組播地址其他系統仍只是監聽本地地址。
二、程序實現
2.1 程序代碼
發送者代碼:
import time import socket # 組播組IP和端口 mcast_group_ip = '239.255.255.252' mcast_group_port = 23456 def sender(): # 建立發送socket,和正常UDP數據包沒區別 send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 每十秒發送一遍消息 while True: message = "this message send via mcast !" # 發送寫法和正常UDP數據包的還是完全沒區別 # 猜測只可能是網卡自己在識別到目的ip是組播地址后,自動將目的mac地址設為多播mac地址 send_sock.sendto(message.encode(), (mcast_group_ip, mcast_group_port)) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}: message send finish') time.sleep(10) if __name__ == "__main__": sender()
接收者代碼:
import sys import struct import time import socket # linux能綁定網卡這里綁定組播IP地址不會服錯,windows沒法綁定網卡這里不能綁定組播IP地址只能綁定本網卡IP地址 if "linux" in sys.platform: # 綁定到的網卡名,如果自己的不是eth0則注意修改 nic_name = "eth0" # 監聽的組播地址 mcast_group_ip = "239.255.255.252" else: mcast_group_ip = socket.gethostbyname(socket.gethostname()) mcast_group_port = 23456 def receiver(): # 建立接收socket,和正常UDP數據包沒區別 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # 25是linux上的socket.SO_BINDTODEVICE的宏定義,但由於windows沒實現SO_BINDTODEVICE,所以python索性也就沒有實現SO_BINDTODEVICE,我們直接使用25 # windows沒有實現SO_BINDTODEVICE,所以不能通過這種方式綁定網卡,windows怎么實現綁定網卡暫不清楚 if "linux" in sys.platform: sock.setsockopt(socket.SOL_SOCKET, 25, nic_name) # linux能綁定網卡這里綁定組播IP地址不會服錯,windows沒法綁定網卡這里不能綁定組播IP地址只能綁定本網卡IP地址 sock.bind((mcast_group_ip, mcast_group_port)) # 加入組播組 mreq = struct.pack("=4sl", socket.inet_aton(mcast_group_ip), socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,mreq) # 允許端口復用,看到很多教程都有沒想清楚意義是什么,我這里直接注釋掉 # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 設置非阻塞,看到很多教程都有也沒想清楚有什么用,我這里直接注釋掉 # sock.setblocking(0) while True: try: message, addr = sock.recvfrom(1024) print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())}: Receive data from {addr}: {message.decode()}') except : print("while receive message error occur") if __name__ == "__main__": receiver()
2.2 運行截圖
發送者截圖:
接收者運行截圖:
參考:
https://blog.csdn.net/ztb3214/article/details/19285363
https://blog.csdn.net/s_lisheng/article/details/74938534?locationNum=10&fps=1