p2p內(nèi)網(wǎng)穿透

P2P

當(dāng)今互聯(lián)網(wǎng)到處存在著一些中間件(MIddleBoxes)渔呵,如NAT和防火墻,導(dǎo)致兩個(gè)(不在同一內(nèi)網(wǎng))中的客戶端無法直接通信砍鸠。 這些問題即便是到了IPV6時(shí)代也會存在扩氢,因?yàn)榧词共恍枰狽AT,但還有其他中間件如防火墻阻擋了鏈接的建立爷辱。 目前部署的中間件多都是在C/S架構(gòu)上設(shè)計(jì)的录豺,其中相對隱匿的客戶機(jī)主動向周知的服務(wù)端(擁有靜態(tài)IP地址和DNS名稱)發(fā)起鏈接請求。 大多數(shù)中間件實(shí)現(xiàn)了一種非對稱的通訊模型饭弓,即內(nèi)網(wǎng)中的主機(jī)可以初始化對外的鏈接双饥,而外網(wǎng)的主機(jī)卻不能初始化對內(nèi)網(wǎng)的鏈接, 除非經(jīng)過中間件管理員特殊配置弟断。

在中間件為常見的NAPT的情況下(也是本文主要討論的)咏花,內(nèi)網(wǎng)中的客戶端沒有單獨(dú)的公網(wǎng)IP地址, 而是通過NAPT轉(zhuǎn)換阀趴,和其他同一內(nèi)網(wǎng)用戶共享一個(gè)公網(wǎng)IP昏翰。這種內(nèi)網(wǎng)主機(jī)隱藏在中間件后的不可訪問性對于一些客戶端軟件如瀏覽器來說 并不是一個(gè)問題苍匆,因?yàn)槠渲恍枰跏蓟瘜ν獾逆溄樱瑥哪撤矫鎭砜捶炊€對隱私保護(hù)有好處棚菊。然而在P2P應(yīng)用中锉桑, 內(nèi)網(wǎng)主機(jī)(客戶端)需要對另外的終端(Peer)直接建立鏈接,但是發(fā)起者和響應(yīng)者可能在不同的中間件后面窍株, 兩者都沒有公網(wǎng)IP地址民轴。而外部對NAT公網(wǎng)IP和端口主動的鏈接或數(shù)據(jù)都會因內(nèi)網(wǎng)未請求被丟棄掉。本文討論的就是如何跨越NAT實(shí)現(xiàn)內(nèi)網(wǎng)主機(jī)直接通訊的問題球订。

網(wǎng)絡(luò)模型

假設(shè)客戶端A和客戶端B的地址都是內(nèi)網(wǎng)地址后裸,且在不同的NAT后面。A冒滩、B上運(yùn)行的P2P應(yīng)用程序和服務(wù)器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

現(xiàn)在假設(shè)客戶端A打算與客戶端B直接建立一個(gè)UDP通信會話开睡。如果A直接給B的公網(wǎng)地址120.26.10.118:3000發(fā)送UDP數(shù)據(jù)因苹,NAT B將很可能會無視進(jìn)入的 數(shù)據(jù)(除非是Full Cone NAT),因?yàn)樵吹刂泛投丝谂cS不匹配篇恒,而最初只與S建立過會話扶檐。B往A直接發(fā)信息也類似。

假設(shè)A開始給B的公網(wǎng)地址發(fā)送UDP數(shù)據(jù)的同時(shí)胁艰,給服務(wù)器S發(fā)送一個(gè)中繼請求款筑,要求B開始給A的公網(wǎng)地址發(fā)送UDP信息。A往B的輸出信息會導(dǎo)致NAT A打開 一個(gè)A的內(nèi)網(wǎng)地址與與B的外網(wǎng)地址之間的新通訊會話腾么,B往A亦然奈梳。一旦新的UDP會話在兩個(gè)方向都打開之后,客戶端A和客戶端B就能直接通訊解虱, 而無須再通過引導(dǎo)服務(wù)器S了攘须。

UDP打洞技術(shù)有許多有用的性質(zhì)。一旦一個(gè)的P2P鏈接建立殴泰,鏈接的雙方都能反過來作為“引導(dǎo)服務(wù)器”來幫助其他中間件后的客戶端進(jìn)行打洞于宙, 極大減少了服務(wù)器的負(fù)載。應(yīng)用程序不需要知道中間件具體是什么(如果有的話)艰匙,因?yàn)橐陨系倪^程在沒有中間件或者有多個(gè)中間件的情況下 也一樣能建立通信鏈路限煞。

打洞流程

假設(shè)A現(xiàn)在希望建立一條到B的udp會話,那么這個(gè)建立基本流程是:

1. A,B分別建立到Server S的udp會話,那么Server S此時(shí)是知道A,B各自的外網(wǎng)ip+端口
2. Server S在和B的udp會話里告訴A的地址(外網(wǎng)ip+端口: 120.27.209.161:6000),同理把B的地址(120.26.10.118:3000)告訴A
3. B向A地址(120.27.209.161:6000)發(fā)送一個(gè)"握手"udp包,打通A->B的udp鏈路
4. 此時(shí)A可以向B(120.26.10.118:3000)發(fā)送udp包,A->B的會話建立成功

先決條件

能夠完成打洞有幾個(gè)先決條件:

1. A,B所在的nat網(wǎng)絡(luò)類型(Full cone, Restricted cone, Port-restricted cone, Symmetric NAT)
2. 在一次udp會話期間,nat設(shè)備(路由器)會保持內(nèi)網(wǎng)進(jìn)程 inner_ip:inner_port <-> share_public_ip:share_port的映射關(guān)系,一般根據(jù)具體路由器實(shí)現(xiàn),這個(gè)映射關(guān)系可以維持幾分鐘到幾個(gè)小時(shí)不等
3. 流程中第3步,nat A收到這個(gè)握手包后并不會轉(zhuǎn)發(fā)給A,因?yàn)樗l(fā)現(xiàn)自己的沒有保存過B的地址,認(rèn)為這是一個(gè)來歷不明的包而直接丟棄,然而這個(gè)包的作用在于在nat B留下了A的記錄,使得nat B認(rèn)為A是可達(dá)或者說可通過了,這樣當(dāng)A->B再發(fā)送udp包時(shí)就可以真正到達(dá)B了员凝。所以這個(gè)"握手"包的作用是可以打通A->B的通路,是必要的

源碼示例

使用三臺設(shè)備模擬,外網(wǎng)設(shè)備207.148.70.129模擬Server S,執(zhí)行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("進(jìn)行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("中轉(zhuǎn)服務(wù)器退出,仍不影響peers間通信")
            return
        }
    }
}

另外兩臺分別位于不同內(nèi)網(wǎng)后的設(shè)備,均運(yùn)行相同代碼peer.go:

package main

import (
    "fmt"
    "log"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)

var tag string

const HAND_SHAKE_MSG = "我是打洞消息"

func main() {
    // 當(dāng)前進(jìn)程標(biāo)記字符串,便于顯示
    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()
    // 向另一個(gè)peer發(fā)送一條udp消息(對方peer的nat設(shè)備會丟棄該消息,非法來源),用意是在自身的nat設(shè)備打開一條可進(jìn)入的通道,這樣對方peer就可以發(fā)過來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("收到數(shù)據(jù):%s\n", data[:n])
        }
    }
}

注意代碼僅模擬打洞基礎(chǔ)流程,如果讀者測試網(wǎng)絡(luò)情況較差發(fā)生udp丟包,可能看不到預(yù)期結(jié)果,此時(shí)簡單重啟server,peer即可.

完整代碼參考server peer

udp打洞轉(zhuǎn)tcp通信

通常,由于udp打洞實(shí)現(xiàn)簡單,p2p的實(shí)現(xiàn)采用udp打洞較多,然而當(dāng)通路建立起來后使用tcp進(jìn)行節(jié)點(diǎn)間通信可以獲取更好的通信效果。因?yàn)閡dp打洞完成后形成的nat映射是和tcp/udp無關(guān)的,所以此時(shí)可以轉(zhuǎn)為使用tcp建立連接,達(dá)到最終的p2p的tcp通信.由于代碼較簡單,這里就不給出示例了奋献。

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末健霹,一起剝皮案震驚了整個(gè)濱河市旺上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌糖埋,老刑警劉巖宣吱,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瞳别,居然都是意外死亡征候,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門祟敛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疤坝,“玉大人,你說我怎么就攤上這事馆铁∨苋啵” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵埠巨,是天一觀的道長历谍。 經(jīng)常有香客問我,道長辣垒,這世上最難降的妖魔是什么望侈? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮勋桶,結(jié)果婚禮上甜无,老公的妹妹穿的比我還像新娘。我一直安慰自己哥遮,他們只是感情好岂丘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著眠饮,像睡著了一般奥帘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仪召,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天寨蹋,我揣著相機(jī)與錄音,去河邊找鬼扔茅。 笑死已旧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的召娜。 我是一名探鬼主播运褪,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秸讹?” 一聲冷哼從身側(cè)響起檀咙,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎璃诀,沒想到半個(gè)月后弧可,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡劣欢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年棕诵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凿将。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡校套,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丸相,到底是詐尸還是另有隱情搔确,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布灭忠,位于F島的核電站膳算,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏弛作。R本人自食惡果不足惜涕蜂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望映琳。 院中可真熱鬧机隙,春花似錦、人聲如沸萨西。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谎脯。三九已至葱跋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間源梭,已是汗流浹背娱俺。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留废麻,地道東北人荠卷。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像烛愧,于是被迫代替她去往敵國和親油宜。 傳聞我的和親對象是個(gè)殘疾皇子掂碱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354