让人迷糊的 socket udp 连接问题
公司內部的一個 golang 中間件報 UDP 連接異常的日志,問題很明顯,對端的服務掛了,自然重啟下就可以了。
哈哈,但讓我疑惑的問題是 udp 是如何檢測對端掛了?
err:??write?udp?172.16.44.62:62651->172.16.0.46:29999:?write:?connection?refusederr:??write?udp?172.16.44.62:62651->172.16.0.46:29999:?write:?connection?refusederr:??write?udp?172.16.44.62:62651->172.16.0.46:29999:?write:?connection?refused...UDP 協議既沒有三次握手,又沒有 TCP 那樣的狀態控制報文,那么如何判定對端的 UDP 端口是否已打開?
通過抓包可以發現,當服務端的端口沒有打開時,服務端的系統向客戶端返回 icmp ECONNREFUSED 報文,表明該連接異常。
通過抓包可以發現返回的協議為 ICMP,但含有源端口和目的端口,客戶端系統解析該報文時,通過五元組找到對應的 socket,并 errno 返回異常錯誤,如果客戶端陷入等待,則喚醒起來,設置錯誤狀態.
(上面是 udp 異常下的 icmp,下面是正常 icmp)
當 UDP 連接異常時,可以通過 tcpdump 工具指定 ICMP 協議來抓取該異常報文,畢竟對方是通過 icmp 返回的 ECONNREFUSED。
使用 tcpdump 抓包
請求命令:
先找到一個可以 ping 通的主機,然后用 nc 模擬 udp 客戶端去請求不存在的端口,出現 Connection refused。
[root@ocean?~]#?nc?-vzu?172.16.0.46?8888 Ncat:?Version?7.50?(?https://nmap.org/ncat?) Ncat:?Connected?to?172.16.0.46:8888. Ncat:?Connection?refused.抓包信息如下:
[root@ocean?~]#?tcpdump?-i?any?icmp?-nn tcpdump:?verbose?output?suppressed,?use?-v?or?-vv?for?full?protocol?decode listening?on?any,?link-type?LINUX_SLL?(Linux?cooked),?capture?size?262144?bytes 17:01:14.075617?IP?172.16.0.46?>?172.16.0.62:?ICMP?172.16.0.46?udp?port?8888?unreachable,?length?37 17:01:17.326145?IP?172.16.0.46?>?172.16.0.62:?ICMP?172.16.0.46?udp?port?8888?unreachable,?length?37 17:01:17.927480?IP?172.16.0.46?>?172.16.0.62:?ICMP?172.16.0.46?udp?port?8888?unreachable,?length?37 17:01:18.489560?IP?172.16.0.46?>?172.16.0.62:?ICMP?172.16.0.46?udp?port?8888?unreachable,?length?37還需要注意的是 telnet 不支持 udp,只支持 tcp,建議使用 nc 來探測 udp。
各種case的測試
case小結
當 ip 無法連通時,udp 客戶端連接時,通常會顯示成功。
當 udp 服務端程序關閉,但系統還存在時,對方系統會 `icmp ECONNREFUSE 錯誤。
當對方有操作 iptables udp port drop 時,通常客戶端也會顯示成功。
IP 無法聯通時:
[root@host-46?~?]$?ping?172.16.0.65 PING?172.16.0.65?(172.16.0.65)?56(84)?bytes?of?data. From?172.16.0.46?icmp_seq=1?Destination?Host?Unreachable From?172.16.0.46?icmp_seq=2?Destination?Host?Unreachable From?172.16.0.46?icmp_seq=3?Destination?Host?Unreachable From?172.16.0.46?icmp_seq=4?Destination?Host?Unreachable From?172.16.0.46?icmp_seq=5?Destination?Host?Unreachable From?172.16.0.46?icmp_seq=6?Destination?Host?Unreachable ^C ---?172.16.0.65?ping?statistics?--- 6?packets?transmitted,?0?received,?+6?errors,?100%?packet?loss,?time?4999ms pipe?4[root@host-46?~?]$?nc?-vzu?172.16.0.65?8888 Ncat:?Version?7.50?(?https://nmap.org/ncat?) Ncat:?Connected?to?172.16.0.65:8888. Ncat:?UDP?packet?sent?successfully Ncat:?1?bytes?sent,?0?bytes?received?in?2.02?seconds.另外再次明確一點 udp 沒有類似 tcp 那樣的狀態報文,所以單純對 UDP 抓包是看不到啥異常信息。
那么當 IP 不通時,為啥 NC UDP 命令顯示成功?
netcat nc udp 的邏輯
為什么當 ip 不連通或者報文被 DROP 時,返回連接成功?
因為 nc 默認的探測邏輯很簡單,只要在 2 秒鐘內沒有收到 icmp ECONNREFUSED 異常報文,那么就認為 UDP 連接成功。😅
下面是 nc udp 命令執行的過程。
setsockopt(3,?SOL_SOCKET,?SO_BROADCAST,?[1],?4)?=?0 connect(3,?{sa_family=AF_INET,?sin_port=htons(30000),?sin_addr=inet_addr("172.16.0.111")},?16)?=?0 select(4,?[3],?[3],?[3],?NULL)??????????=?1?(out?[3]) getsockopt(3,?SOL_SOCKET,?SO_ERROR,?[0],?[4])?=?0 write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6 write(2,?"Connected?to?172.16.0.111:29999."...,?33Connected?to?172.16.0.111:29999. )?=?33 sendto(3,?"\0",?1,?0,?NULL,?0)??????????=?1// select 多路復用方法里加入了超時邏輯。 select(4,?[3],?[],?[],?{tv_sec=2,?tv_usec=0})?=?0?(Timeout)write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6 write(2,?"UDP?packet?sent?successfully\n",?29UDP?packet?sent?successfully )?=?29 write(2,?"Ncat:?",?6Ncat:?)???????????????????=?6 write(2,?"1?bytes?sent,?0?bytes?received?i"...,?481?bytes?sent,?0?bytes?received?in?2.02?seconds. )?=?48 close(3)????????????????????????????????=?0使用 golang/ python 編寫的 UDP 客戶端,給無法連通的地址發 UDP 報文時,其實也不會報錯,這時候通常會認為發送成功。
還是那句話,UDP 沒有 TCP 那樣的握手步驟,像 TCP 發送 syn 總得不到回報時,協議棧會在時間退避下嘗試 6 次,當 6 次還得不到回應,內核會給與錯誤的 errno 值。
UDP 連接信息
在客戶端的主機上,通過 ss lsof netstat 可以看到 UDP 五元組連接信息。
[root@host-46?~?]$?netstat?-tunalp|grep?29999 udp????????0??????0?172.16.0.46:44136???????172.16.0.46:29999???????ESTABLISHED?1285966/cccc通常在服務端上看不到 UDP 連接信息,只可以看到 udp listen 信息!
[root@host-62?~?]#?netstat?-tunalp|grep?29999 udp???????0??????0?:::29999????????????????:::*????????????????????????????????4038720/ss客戶端重新實例化問題?
當 client 跟 server 已連接,server 端手動重啟后,客戶端無需再次重新實例化連接,可以繼續發送數據,當服務端再次啟動后,照樣可以收到客戶端發來的報文。
udp 本就無握手的過程,他的 udp connect() 也只是在本地創建 socket 信息。在服務端使用 netstat 是看不到 udp 五元組的 socket。
Golang 測試代碼
服務端代碼:
package?mainimport?("fmt""net" )//?UDP?服務端 func?main()?{listen,?err?:=?net.ListenUDP("udp",?&net.UDPAddr{IP:???net.IPv4(0,?0,?0,?0),Port:?29999,})if?err?!=?nil?{fmt.Println("Listen?failed,?err:?",?err)return}defer?listen.Close()for?{var?data?[1024]byten,?addr,?err?:=?listen.ReadFromUDP(data[:])if?err?!=?nil?{fmt.Println("read?udp?failed,?err:?",?err)continue}fmt.Printf("data:%v?addr:%v?count:%v\n",?string(data[:n]),?addr,?n)} }客戶端代碼:
package?mainimport?("fmt""net""time" )//?UDP?客戶端 func?main()?{socket,?err?:=?net.DialUDP("udp",?nil,?&net.UDPAddr{IP:???net.IPv4(172,?16,?0,?46),Port:?29999,})if?err?!=?nil?{fmt.Println("連接UDP服務器失敗,err:?",?err)return}defer?socket.Close()for?{time.Sleep(1e9?*?2)sendData?:=?[]byte("Hello?Server")_,?err?=?socket.Write(sendData)if?err?!=?nil?{fmt.Println("發送數據失敗,err:?",?err)continue}fmt.Println("已發送")} }總結
當 udp 服務端的機器可以連通且無異常時,客戶端通常會顯示成功。但當有異常時,會有以下的情況:
當 ip 地址無法連通時,udp 客戶端連接時,通常會顯示成功。
當 udp 服務端程序關閉,但系統還存在時,對方系統通過 icmp ECONNREFUSE 返回錯誤,客戶端會報錯。
當對方有操作 iptables udp port drop 時,客戶端也會顯示成功。
客戶端和服務端互通數據,當服務進程掛了時,UDP 客戶端不能立馬感知關閉狀態,只有當再次發數據時才會被對方系統回應 icmp ECONNREFUSE 異常報文,客戶端才能感知對方掛了。
總結
以上是生活随笔為你收集整理的让人迷糊的 socket udp 连接问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用 Go 实现生产者和消费者,Kafk
- 下一篇: Go大型项目实战:项目结构、配置管理、A