项目需要用到组播,所以了解了一下组播与广播的概念,以及 Go 中的实现。

1. 单播、组播与广播

1.1 单播/Unicast

单播 (Unicast) 是最常见的网络通信方式,也是网络中最基本的通信模式。在单播通信中,数据包从一个主机发送到另一个主机,这两个主机之间是一对一的关系。

1.2 组播/多播/Multicast

组播是一种网络通信方式,它允许一个主机向一组特定的主机发送数据包。
多播 (Multicast) 方式的数据传输是基于 UDP 完成的,因此,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据同时传递到加入 (注册) 特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。
多播通信依赖于“多播组”的概念,每个多播组都有一个特殊的IP地址。在IPv4中,224.0.0.0到239.255.255.255的地址段被预留用于多播;在IPv6中,多播地址以FF00::/8开头。只有加入了某个多播组的主机,才能接收发送到该组的数据。
当主机希望接收某个多播组的数据时,它会发送加入请求,IPv4 使用 IGMP(Internet Group Management Protocol) 来向局域网内的路由器报告其多播组成员身份;IPv6 使用 MLD(Multicast Listener Discovery) 协议完成类似的功能。

通过这些协议,网络中的路由器可以了解哪些子网有对某个多播组感兴趣的主机。然后路由器需要根据组成员信息建立一棵从数据源到所有接收者的多播分发树。多播路由协议也有很多,常用的 PIM-DMPIM-SMDVMRP 等,暂时用不到就不做深入研究了。

多播的应用场景有很多,主要包括:

  • 视频会议与直播:多播可以高效地将视频或音频流同时传送给众多用户,降低了视频会议系统的延迟和带宽消耗。
  • IP电视(IPTV):通过多播技术,可以将电视节目流高效地分发给成千上万的用户,避免了重复传输。
  • 在线游戏与数据分发:在多人在线游戏中,实时状态更新可以通过多播方式快速分发给所有玩家,保证同步性和实时性。
  • 股票行情、新闻推送等实时数据:需要同时向大量用户推送相同信息的场景也适合采用多播技术。

1.3 广播/Broadcast

广播是一种网络通信方式,它允许一个主机向网络中的所有主机发送数据包。广播是一种无差别的数据传输方式,即数据包会被发送到网络中的所有主机,而不管这些主机是否需要这些数据。
广播 (Broadcast) 在“一次性向多个主机发送数据”这一点上与多播类似, 但传输数据的范围有区别。多播即使在跨越不同网络的情况下, 只要加入多播组就能接收数据。相反, 广播只能向同一网络中的主机传输数据。

广播是向同一网络中的所有主机传输数据的方法。与多播相同, 广播也是基于 UDP 完成的。但根据传输数据时使用的IP地址的形式, 广播分为如下2种类型:

  • 直接广播 (Directed Broadcast)
  • 本地广播 (Local Broadcast)

二者在代码实现上的差别主要在于IP地址。

  • 直接广播的IP地址中除了网络地址外, 其余主机地址全部设置为 1。例如:希望向网络地址 192.168.50.* 中的所有主机传输数据时, 可以向 192.168.50.255 传输。也就是说, 可以采用直接广播的方式向特定区域内所有主机传输数据。虽然直接广播报文可以被路由器转发到目标网络,但出于安全考虑(防范如 Smurf 攻击的广播放大攻击),许多路由器默认会禁用这种转发。
  • 而本地广播中则是直接通过网卡向特殊IP地址 255.255.255.255 发数据。例如, 192.168.50.10 主机向 255.255.255.255 传输数据时, 数据将传递到 192.168.50.* 网络下的所有主机。这种广播报文不会被路由器转发,因此它只能在本地网络中传播。

本地广播常见用途有,DHCP 客户端在没有获得 IP 地址时,通过本地广播发送 DHCP 请求以寻找 DHCP 服务器。而直接广播由于其可以向特定区域内所有主机传输数据的特性,理论上可以跨网段传输数据,当然在实际应用中,由于网络设备的限制,直接广播的跨网段传输并不常见,并且由于安全需要,也应该被限制。

Smurf 攻击是一种分布式拒绝服务(DDoS)攻击,其基本原理如下:
攻击原理:攻击者伪造 ICMP Echo Request(ping)报文的源 IP 地址,使其看起来像是来自目标受害者。然后,攻击者将这些报文发送到网络的广播地址(例如某个子网的直接广播地址)。

放大效应:当网络中的所有主机接收到这个广播包时,它们都会回复 ICMP Echo Reply 报文给伪造的源地址(即受害者)。这样,大量的回复流量就会集中到受害者身上,从而消耗受害者的带宽和处理能力。

影响:受害者可能会因为无法处理大量无用的 ICMP 响应而导致网络性能下降、服务中断甚至系统崩溃。

防范措施:

  • 禁用对广播地址的 ICMP 请求:在网络设备上配置规则,防止来自外部的 ICMP Echo Request 被转发到广播地址。
  • 路由器过滤:许多路由器默认会禁用直接广播转发,以防止此类攻击。
  • 入侵检测和流量监控:通过监控异常流量,可以及时发现并应对潜在的 Smurf 攻击。
  • 总结来说,Smurf 攻击利用了网络广播的特性,通过伪造源地址发送 ICMP 请求,进而在短时间内对目标受害者造成巨大的流量冲击,是一种典型的放大型拒绝服务攻击。

2. 代码实现

Go 语言实现单播通信,通常使用 TCP 协议或 UDP 协议。可以通过 net 包提供的 net.Dial 函数建立 TCP 连接,或者通过 net.Listennet.Accept 函数建立 TCP 服务器。对于 UDP 协议,可以通过 net.DialUDPnet.ListenUDP 函数实现客户端和服务器端。

Go 语言实现组播通信,需要使用 net 包提供的 net.ListenMulticastUDPnet.Dial 函数。这两个函数分别用于创建组播服务器和客户端。

广播暂时就不研究代码实现了,因为广播的应用场景比较少,而且这次项目也用不到。

2.1 单播服务端/客户端代码实现

单播服务端代码,监听 :5683 端口,接收客户端发送的消息并打印客户端地址和消息内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"net"
"os"
)

const BUF_SIZE int = 8192

func main() {
addr, _ := net.ResolveUDPAddr("udp", ":5683")
listener, err := net.ListenUDP("udp", addr)
if err != nil {
checkError(err)
}

message := make([]byte, BUF_SIZE)

for {
n, src, _ := listener.ReadFromUDP(message)
fmt.Println(src, ": ", string(message[:n]))
}
}

func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

单播客户端代码,向 127.0.0.1:5683 地址发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"net"
"os"
"time"
)

func main() {
conn, err := net.Dial("udp", "127.0.0.1:5683")
if err != nil {
checkError(err)
}

for {
conn.Write([]byte("hello, world!"))
time.Sleep(1 * time.Second)
}
}

func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

运行结果

2.2 组播服务端/客户端代码实现

组播服务端代码,监听 224.0.1.187:5683 组播地址,接收客户端发送的消息并打印客户端地址和消息内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"net"
"os"
)

const BUF_SIZE int = 8192

func main() {
gaddr, _ := net.ResolveUDPAddr("udp", "224.0.1.187:5683")
listener, err := net.ListenMulticastUDP("udp", nil, gaddr)
if err != nil {
checkError(err)
}

message := make([]byte, BUF_SIZE)

for {
n, src, _ := listener.ReadFromUDP(message)
fmt.Println(src, ": ", string(message[:n]))
}
}

func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

组播客户端代码,向 224.0.1.187:5683 组播地址发送消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"net"
"os"
"time"
)

func main() {
conn, err := net.Dial("udp", "224.0.1.187:5683")
if err != nil {
checkError(err)
}

for {
conn.Write([]byte("hello, world!"))
time.Sleep(1 * time.Second)
}
}

func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}

运行结果

3. 总结

Go 语言实现单播、组播通信非常简单,只需要使用 net 包提供的 net.Dialnet.ListenUDPnet.ListenMulticastUDP 函数即可,并且 net 包提供的函数不仅是跨平台的,在 Linux 上的 IO 多路复用也是基于 epoll 实现的,性能也非常好。
只是在该项目中有一个问题,就是端口占用问题,如果需要同时监听同一个端口,会出现端口占用的问题,这个问题需要在项目中解决。