golang實現p2p之UDP打洞
當今網際網路到處存在著一些中介軟體(MIddleBoxes),如NAT和防火牆,導致兩個(不在同一內網)中的客戶端無法直接通訊。 這些問題即便是到了IPV6時代也會存在,因為即使不需要NAT,但還有其他中介軟體如防火牆阻擋了連結的建立。 目前部署的中介軟體多都是在C/S架構上設計的,其中相對隱匿的客戶機主動向周知的服務端(擁有靜態IP地址和DNS名稱)發起連結請求。 大多數中介軟體實現了一種非對稱的通訊模型,即內網中的主機可以初始化對外的連結,而外網的主機卻不能初始化對內網的連結, 除非經過中介軟體管理員特殊配置。
在中介軟體為常見的NAPT的情況下(也是本文主要討論的),內網中的客戶端沒有單獨的公網IP地址, 而是通過NAPT轉換,和其他同一內網使用者共享一個公網IP。這種內網主機隱藏在中介軟體後的不可訪問性對於一些客戶端軟體如瀏覽器來說 並不是一個問題,因為其只需要初始化對外的連結,從某方面來看反而還對隱私保護有好處。然而在P2P應用中, 內網主機(客戶端)需要對另外的終端(Peer)直接建立連結,但是發起者和響應者可能在不同的中介軟體後面, 兩者都沒有公網IP地址。而外部對NAT公網IP和埠主動的連結或資料都會因內網未請求被丟棄掉。本文討論的就是如何跨越NAT實現內網主機直接通訊的問題。
網路模型
假設客戶端A和客戶端B的地址都是內網地址,且在不同的NAT後面。A、B上執行的P2P應用程式和伺服器S都使用了UDP埠9982,A和B分別初始化了 與Server的UDP通訊,地址對映如圖所示:
Server S 207.148.70.129:9981 | | +----------------------|----------------------+ | | NAT A NAT B 120.27.209.161:6000 120.26.10.118:3000 | | | | Client A Client B 10.0.0.1:9982 192.168.0.1:9982
現在假設客戶端A打算與客戶端B直接建立一個UDP通訊會話。如果A直接給B的公網地址120.26.10.118:3000傳送UDP資料,NAT B將很可能會無視進入的 資料(除非是Full Cone NAT),因為源地址和埠與S不匹配,而最初只與S建立過會話。B往A直接發信息也類似。
假設A開始給B的公網地址傳送UDP資料的同時,給伺服器S傳送一箇中繼請求,要求B開始給A的公網地址傳送UDP資訊。A往B的輸出資訊會導致NAT A開啟 一個A的內網地址與與B的外網地址之間的新通訊會話,B往A亦然。一旦新的UDP會話在兩個方向都開啟之後,客戶端A和客戶端B就能直接通訊, 而無須再通過引導伺服器S了。
UDP打洞技術有許多有用的性質。一旦一個的P2P連結建立,連結的雙方都能反過來作為“引導伺服器”來幫助其他中介軟體後的客戶端進行打洞, 極大減少了伺服器的負載。應用程式不需要知道中介軟體具體是什麼(如果有的話),因為以上的過程在沒有中介軟體或者有多箇中間件的情況下 也一樣能建立通訊鏈路。
打洞流程
假設A現在希望建立一條到B的udp會話,那麼這個建立基本流程是:
- A,B分別建立到Server S的udp會話,那麼Server S此時是知道A,B各自的外網ip+埠
- Server S在和B的udp會話裡告訴A的地址(外網ip+埠: 120.27.209.161:6000),同理把B的地址(120.26.10.118:3000)告訴A
- B向A地址(120.27.209.161:6000)傳送一個"握手"udp包,打通A->B的udp鏈路
- 此時A可以向B(120.26.10.118:3000)傳送udp包,A->B的會話建立成功
先決條件
能夠完成打洞有幾個先決條件:
- A,B所在的nat網路型別(Full cone, Restricted cone, Port-restricted cone, Symmetric NAT)
- 在一次udp會話期間,nat裝置(路由器)會保持內網程序 inner_ip:inner_port <-> share_public_ip:share_port的對映關係,一般根據具體路由器實現,這個對映關係可以維持幾分鐘到幾個小時不等
- 流程中第3步,nat A收到這個握手包後並不會轉發給A,因為它發現自己的沒有儲存過B的地址,認為這是一個來歷不明的包而直接丟棄,然而這個包的作用在於在nat B留下了A的記錄,使得nat B認為A是可達或者說可通過了,這樣當A->B再發送udp包時就可以真正到達B了。所以這個"握手"包的作用是可以打通A->B的通路,是必要的
原始碼示例
使用三臺裝置模擬,外網裝置207.148.70.129模擬Server S,執行server.go程式碼:
server.go
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9981})
if err != nil {
fmt.Println(err)
return
}
log.Printf("本地地址: <%s> \n", listener.LocalAddr().String())
peers := make([]net.UDPAddr, 0, 2)
data := make([]byte, 1024)
for {
n, remoteAddr, err := listener.ReadFromUDP(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
log.Printf("<%s> %s\n", remoteAddr.String(), data[:n])
peers = append(peers, *remoteAddr)
if len(peers) == 2 {
log.Printf("進行UDP打洞,建立 %s <--> %s 的連線\n", peers[0].String(), peers[1].String())
listener.WriteToUDP([]byte(peers[1].String()), &peers[0])
listener.WriteToUDP([]byte(peers[0].String()), &peers[1])
time.Sleep(time.Second * 8)
log.Println("中轉伺服器退出,仍不影響peers間通訊")
return
}
}
}
另外兩臺分別位於不同內網後的裝置,均執行相同程式碼peer.go:
package main
import (
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
)
var tag string
const HAND_SHAKE_MSG = "我是打洞訊息"
func main() {
// 當前程序標記字串,便於顯示
tag = os.Args[1]
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9982} // 注意埠必須固定
dstAddr := &net.UDPAddr{IP: net.ParseIP("207.148.70.129"), Port: 9981}
conn, err := net.DialUDP("udp", srcAddr, dstAddr)
if err != nil {
fmt.Println(err)
}
if _, err = conn.Write([]byte("hello, I'm new peer:" + tag)); err != nil {
log.Panic(err)
}
data := make([]byte, 1024)
n, remoteAddr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
conn.Close()
anotherPeer := parseAddr(string(data[:n]))
fmt.Printf("local:%s server:%s another:%s\n", srcAddr, remoteAddr, anotherPeer.String())
// 開始打洞
bidirectionHole(srcAddr, &anotherPeer)
}
func parseAddr(addr string) net.UDPAddr {
t := strings.Split(addr, ":")
port, _ := strconv.Atoi(t[1])
return net.UDPAddr{
IP: net.ParseIP(t[0]),
Port: port,
}
}
func bidirectionHole(srcAddr *net.UDPAddr, anotherAddr *net.UDPAddr) {
conn, err := net.DialUDP("udp", srcAddr, anotherAddr)
if err != nil {
fmt.Println(err)
}
defer conn.Close()
// 向另一個peer傳送一條udp訊息(對方peer的nat裝置會丟棄該訊息,非法來源),用意是在自身的nat裝置開啟一條可進入的通道,這樣對方peer就可以發過來udp訊息
if _, err = conn.Write([]byte(HAND_SHAKE_MSG)); err != nil {
log.Println("send handshake:", err)
}
go func() {
for {
time.Sleep(10 * time.Second)
if _, err = conn.Write([]byte("from [" + tag + "]")); err != nil {
log.Println("send msg fail", err)
}
}
}()
for {
data := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(data)
if err != nil {
log.Printf("error during read: %s\n", err)
} else {
log.Printf("收到資料:%s\n", data[:n])
}
}
}
注意程式碼僅模擬打洞基礎流程,如果讀者測試網路情況較差發生udp丟包,可能看不到預期結果,此時簡單重啟server,peer即可.
udp打洞轉tcp通訊
通常,由於udp打洞實現簡單,p2p的實現採用udp打洞較多,然而當通路建立起來後使用tcp進行節點間通訊可以獲取更好的通訊效果。因為udp打洞完成後形成的nat對映是和tcp/udp無關的,所以此時可以轉為使用tcp建立連線,達到最終的p2p的tcp通訊.由於程式碼較簡單,這裡就不給出示例了。
參考文獻
相關推薦
golang實現p2p之UDP打洞
當今網際網路到處存在著一些中介軟體(MIddleBoxes),如NAT和防火牆,導致兩個(不在同一內網)中的客戶端無法直接通訊。 這些問題即便是到了IPV6時代也會存在,因為即使不需要NAT,但還有其他中介軟體如防火牆阻擋了連結的建立。 目前部署的中介軟體多都是在C/
【原創】IP攝像頭技術縱覽(七)---P2P技術—UDP打洞實現內網NAT穿透
【原創】IP攝像頭技術縱覽(七)—P2P技術—UDP打洞實現內網NAT穿透 本文屬於《IP攝像頭技術縱覽》系列文章之一: Author: chad Mail: [email protected] 本文可以自由轉載,但轉載請務必註明
Python實現簡單的udp打洞(P2P)
UDP穿越NAT的具體設計 首先,Client A登入伺服器,NAT 1為這次的Session分配了一個埠60000,那麼Server S收到的Client A的地址是200.0.0.132:60000,這就是ClientA的外網地址了。同樣,Client
UDP打洞NAT大致分為下面四類 P2P
NAT大致分為下面四類 1) Full Cone 這種NAT內部的機器A連線過外網機器C後,NAT會開啟一個埠.然後外網的任何發到這個開啟的埠的UDP資料報都可以到達A.不管是不是C發過來的. 例如 A:192.168.8.100 NAT:202.100.100.100
P2P之UDP穿透NAT的原理與實現(附C++原始碼)[轉載]
論壇上經常有對P2P原理的討論,但是討論歸討論,很少有實質的東西產生(原始碼)。呵呵,在這裡我就用自己實現的一個原始碼來說明UDP穿越NAT的原理。 首先先介紹一些基本概念: NAT(Network Address Translators),網路地址轉換:網路地址轉換是在IP地址日益缺乏的情況下產生
簡單的p2p-demo,udp打洞
什麼是p2p: peer-to-peer,簡單來說,就是兩個使用者可以直接進行網路通訊。 為什麼我們需要p2p: 1.大多數的網路狀態都是使用者A和使用者B互相通訊,需要一箇中間伺服器來做訊息的中轉。如果可以使用者對使用者直接通訊,那麼可以減輕伺服器壓力。 2.一定程度上
P2P之UDP穿透NAT的原理與實現(附原始碼)
原文連結 關於UDP穿透NAT的中文資料在網路上是很少的,僅有<<P2P之UDP穿透NAT的原理與實現(shootingstars)>>這篇文章有實際的參考價值。 本人近兩年來也一直從事P2P方面的開發工作,比較有代表性的是個人開發的BitTorr
NAT穿透(UDP打洞)
會有 work true icmp sea 類型判斷 無法 部分 什麽 1、NAT(Network Address Translator)介紹 NAT有兩大類,基本NAT和NAPT。 1.1、基本NAT 靜態NAT:一個公網IP對應一個內部IP,一對一轉換 動態NAT:N
Darwin Streaming Server 支援UDP打洞
RTSP客戶端點播Darwin 視訊時,SDP協商後的客戶端埠可能是在NAT後面,所以需要Darwin支援NAT打洞的功能,從Darwin的原始碼看,官方的原始碼是不支援這個能力的。 通過抓取VLC客戶端的包發現,VLC在播放RTSP流時,兩次SETUP(音訊流和視訊分別協商埠)之後,
C#與python UDP打洞通訊
本標題的應用場景是C#系統服務端和基於linux的python裝置在不同的區域網下通訊,通常C#系統端在辦公室內部wifi下,裝置在室外利用4G上網。 打洞原理網上蠻多的,隨便一搜就是好多,實際將如何打洞的確很少。這裡需要理論的推薦一篇部落格,個人覺得寫的很好。 https://blog.csd
試驗UDP打洞穿透NAT
目標 路由穿透,實現廣域網P2P通訊。 4種典型NAT型別 按照NAT裝置在進行地址對映時行為的不同,NAT可以分為以下四種: Full Cone Restricted Cone Port Restricted Con
內網穿透&UDP打洞
文章轉載自:http://www.cnblogs.com/cinlap/articles/2684330.html 這兩天找度度重新回憶了一下關於內網穿透的事情,在百度文庫上找到了兩三篇寫的比較通俗易懂的文章,把內網穿透做個簡單總結。 首先文章建議 Cone NA
NAT穿透技術詳解(udp打洞精髓附程式碼)
以前自己寫的程式碼都只是在本地進行c/s通訊,今天想寫一個可以跨越外網的c/s通訊,這裡我就用udp實現一個點對點的不同外網的通訊。用到的技術就是nat穿透技術,這裡最直接使用的就是udp打洞技術。文中如有表述不清楚,歡迎提問。如果你需要nat穿透技術的詳解點這裡:nat穿透
P2P技術 TCP打洞 內網穿透
程式語言:C/C++ 主要使用:SOCKET套接字 程式設計軟體:VS2015 技術實現:P2P TCP打洞 內網穿透實驗 原始碼:http://download.csdn.net/download/a
UDP打洞和心跳包設計
一、裝置終端class DeviceClient { int deviceID; int IP; int port; char connectID[16]; time_t lastTime; struct event timeoutEv;//超時器};typedef list<DeviceClient
TCP打洞和UDP打洞的區別
為什麼網上講到的P2P打洞基本上都是基於UDP協議的打洞?難道TCP不可能打洞?還是TCP打洞難於實現? 假設現在有內網客戶端A和內網客戶端B,有公網服務端S。 如果A和B想要進行UDP通訊,則必須穿透雙方的NAT路由。假設為NAT-A和NAT-B。 A傳送資料包到公
NAT穿透,UDP打洞程式
在看NAT穿透和UDP打洞原理,網上都是講原理,沒有程式,我把程式寫出來。 server.py,輔助打洞的伺服器。 peer.server.py,被打洞的節點。 peer.client.py,主動打洞的節點。 基本原理是: 1. peer.client向peer.serve
TCP打洞與UDP打洞的區別
為什麼網上講到的P2P打洞基本上都是基於UDP協議的打洞?難道TCP不可能打洞?還是TCP打洞難於實現? 假設現在有內網客戶端A和內網客戶端B,有公網服務端S。 如果A和B想要進行UDP通訊,則必須穿透雙方的NAT路由。假設為NAT-A和NAT-B。
UDP 構建p2p打洞過程的實現原理
雙方都在區域網內就沒有辦法TCP直連了,所以像QQ等都會盡量使用UDP直連的 IP地址轉換不需要你處理,閘道器預設就已經進行了轉換。伺服器接收到DatagramPacket中getAddress和getPort已經是閘道器的埠 UDP打洞的過程大致如此: 1、雙方都通過UDP
C++從零開始區塊鏈:P2P模組之打洞伺服器
原則上說,打洞伺服器應該是可以返回多個節點的,這裡為了簡化,只返回一個節點。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #i