go: 如何編寫一個正確的udp服務端
udp的服務端有一個大坑,即如果收包不及時,在系統緩衝寫滿後,將大量丟包。
在網上通常的示例中,一般在for循環中執行操作邏輯。這在生產環境將是一個隱患。是的,俺就翻車了。
go強大簡易的並發能力可以用在處理udp數據上。
PoolSizeUDP := 1472
listener, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.ParseIP(listenIP),
Port: port,
})
if err != nil {
logrus.Fatalf("RunUdpServer failed to listen: %v", err)
return nil
}
// 如果還不行,考慮把系統的buffer設大一點
// listener.SetReadBuffer(1024 * 1024 * 8)
// listener.SetWriteBuffer(1024 * 1024 * 8)
var data = make([]byte, PoolSizeUDP)
chLimit := make(chan int, 64) // 最多創建64個協程,避免記憶體爆炸
for {
select {
case <-ctx.Done():
return nil
default:
}
n, addr, err := listener.ReadFromUDP(data)
if err != nil {
logrus.Errorf("RunUdpServer ReadFromUDP err: %v", err)
continue
}
raw := make([]byte, n) // 重點注意,每次循環都必須創建新的raw變數,否則踩記憶體
copy(raw, data[:n])
chLimit <- 1
go func(udpMsg []byte) {
// 拿 udpMsg 做點什麼
defer func() {
<-chLimit
}()
DoSth(udpMsg)
}(raw)
}
注意點:
- data可以在循環外創建,復用即可。每次
ReadFromUDP並不會受到上次數據殘留的影響。 - 不要在
for中執行重邏輯,避免等待太久時間udp大量丟包。所以每次收到udpMsg,都交給go協程來處理。 - raw必須每次在循環內創建,否則在後面的
go並發會踩記憶體。 SetReadBuffer這個配置很有用
更新:上面的示例為了避免在後續的go中,有不可控的非同步操作引用了數據導致踩記憶體,每次收消息都分配了新的[]byte。
raw := make([]byte, n) // 重點注意,每次循環都必須創建新的raw變數,否則踩記憶體
copy(raw, data[:n])
經大佬提醒,這其實是一個不小的開銷。如果go中執行的行為可控,引入sync.Pool可以很方便的做記憶體復用。
var udpBytesPool = sync.Pool{
New: func() any {
return make([]byte, PoolSizeUDP)
},
}
......
for {
data := udpBytesPool.Get().([]byte)
n, addr, err := listener.ReadFromUDP(data)
if err != nil {
logrus.Errorf("RunUdpServer ReadFromUDP err: %v", err)
continue
}
go func(){
defer udpBytesPool.Put(data) // 注意,在協程退出執行這個操作時,一定確認 data不會再被引用了
// do sth
}()
}

