P2P的原理和常見的實現方式
感慨網上很多資料要麼太過於糾結協議(如STUN、ICE等)實現細節,要麼中間有很多紕漏。
最後去偽存真,歸納總結了一下,希望對以後的同行有些許幫助。
如果有什麼需要討論或者指正的,歡迎留言或者郵件[email protected]
一、P2P實現的原理
1.1 基本概念
首先先介紹一些基本概念,
NAT(Network Address Translators),網路地址轉換:
網路地址轉換是在IP地址日益缺乏的情況下產生的,它的主要目的就是為了能夠地址重用。
NAT從歷史發展上分為兩大類:
基本的NAT和NAPT(Network Address/Port Translator)。
. 基本的NAT
最先提出的是基本的NAT(peakflys注:剛開始其實只是路由器上的一個功能模組),
它的產生基於如下事實:
一個私有網路(域)中的節點中只有很少的節點需要與外網連線(這是在上世紀90年代中期提出的)。
那麼這個子網中其實只有少數的節點需要全球唯一的IP地址,其他的節點的IP地址應該是可以重用的。
因此,基本的NAT實現的功能很簡單,在子網內使用一個保留的IP子網段,這些IP對外是不可見的。
子網內只有少數一些IP地址可以對應到真正全球唯一的IP地址。
如果這些節點需要訪問外部網路,那麼基本NAT就負責將這個節點的子網內IP轉化為一個全球唯一的IP然後傳送出去。
(基本的NAT會改變IP包中的原IP地址,但是不會改變IP包中的埠)
它又可分為靜態和動態兩種:
靜態NAT: 它將內部私網地址與合法公網地址進行一對一的轉換,且每個內部地址的轉換都是確定的;
動態NAT: 它也是將內部私網地址與合法公網地址進行一對一的轉換,
但是它的合法公網是從合法公網地址池中動態選擇一個未使用的地址來對內部私有地址進行轉換;
關於基本的NAT可以參看RFC 1631
. NAPT(Network Address/Port Translator)
另外一種NAT叫做NAPT,從名稱上我們也可以看得出,
NAPT不但會改變經過這個NAT裝置的IP資料報的IP地址,還會改變IP資料報的TCP/UDP埠。
基本NAT的裝置可能我們見的不多(基本已經淘汰了),NAPT才是我們真正需要關注的。
它也是一種動態轉換,而且多個內部地址被轉換成同一個合法公網地址,
使用不同的埠號來區分不同的主機,不同的程序;
看下圖:
Fig-1
有一個私有網路10.*.*.*,
Client A是其中的一臺計算機,
這個網路的閘道器(一個NAT裝置)的外網IP是155.99.25.11(應該還有一個內網的IP地址,比如10.0.0.10)。
如果Client A中的某個程序(這個程序建立了一個UDP Socket,這個Socket繫結1234埠)
想訪問外網主機18.181.0.31的1235埠,那麼當資料包通過NAT時會發生什麼事情呢?
. 首先
NAT會改變這個資料包的原IP地址,改為155.99.25.11。
. 接著
NAT會為這個傳輸建立一個Session,(Session是一個抽象的概念,
如果是TCP,也許Session是由一個SYN包開始,以一個FIN包結束。
而UDP呢,以這個IP的這個埠的第一個UDP開始,結束呢,呵呵,也許是幾分鐘,也許是幾小時,這要看具體的實現了)
並且給這個Session分配一個埠,比如62000,然後改變這個資料包的源埠為62000。
所以本來是:
(10.0.0.1:1234->18.181.0.31:1235)
的資料包到了網際網路上變為了:
(155.99.25.11:62000->18.181.0.31:1235)。
. 最後
一旦NAT建立了一個Session後,NAT會記住62000埠對應的是10.0.0.1的1234埠,
以後從18.181.0.31傳送到62000埠的資料會被NAT自動的轉發到10.0.0.1上。
(注意:這裡是說18.181.0.31傳送到62000埠的資料會被轉發,其他的IP傳送到這個埠的資料將被NAT拋棄)
這樣Client A就與Server S1建立以了一個連線。
1.2 NAT型別
上面的是一些基礎知識,下面的才是關鍵的部分了。
看看下面的情況:
Fig-2
接上面的例子,如果Client A的原來那個Socket(綁定了1234埠的那個UDP Socket)
又接著向另外一個Server S2傳送了一個UDP包,那麼這個UDP包在通過NAT時會怎麼樣呢?
這時可能會有兩種情況發生:
一種是NAT再次建立一個Session,並且再次為這個Session分配一個埠號(比如:62001)。
一種是NAT再次建立一個Session,但是不會新分配一個埠號,而是用原來分配的埠號62000。
前一種NAT叫做Symmetric NAT(對稱型NAT),
後一種叫做Cone NAT(圓錐型NAT)。
如果你的NAT剛好是第一種,那麼很可能會有很多P2P軟體失靈。
(可以慶幸的是,現在絕大多數的NAT屬於後者,即Cone NAT)
peakflys注:Cone NAT具體又分為3種:
(1)全圓錐( Full Cone) :
NAT把所有來自相同內部IP地址和埠的請求對映到相同的外部IP地址和埠。
任何一個外部主機均可通過該對映傳送IP包到該內部主機。
(2)限制性圓錐(Restricted Cone) :
NAT把所有來自相同內部IP地址和埠的請求對映到相同的外部IP地址和埠。
但是,只有當內部主機先給IP地址為X的外部主機發送IP包,該外部主機才能向該內部主機發送IP包。
(3)埠限制性圓錐( Port Restricted Cone) :
埠限制性圓錐與限制性圓錐類似,只是多了埠號的限制,
即只有內部主機先向IP地址為X,埠號為P的外部主機發送1個IP包,
該外部主機才能夠把源埠號為P的IP包傳送給該內部主機。
好了,我們看到,通過NAT,子網內的計算機向外連結是很容易的
(NAT相當於透明的,子網內的和外網的計算機不用知道NAT的情況)。
但是如果外部的計算機想訪問子網內的計算機就比較困難了(而這正是P2P所需要的)。
那麼我們如果想從外部發送一個數據報給內網的計算機有什麼辦法呢?
首先,我們必須在內網的NAT上打上一個“洞”(也就是前面我們說的在NAT上建立一個Session),
這個洞不能由外部來打,只能由內網內的主機來打。
而且這個洞是有方向的,
比如從內部某臺主機(比如:192.168.0.10)向外部的某個IP(比如:219.237.60.1)傳送一個UDP包,
那麼就在這個內網的NAT裝置上打了一個方向為219.237.60.1的“洞”,
(這就是稱為UDP Hole Punching的技術)以後219.237.60.1就可以通過這個洞與內網的192.168.0.10聯絡了。
(但是其他的IP不能利用這個洞)。
二、P2P的常用實現
2.1 普通的直連式P2P實現
通過上面的理論,實現兩個內網的主機通訊就差最後一步了:
那就是雞生蛋還是蛋生雞的問題了,兩邊都無法主動發出連線請求,誰也不知道誰的公網地址,那我們如何來打這個洞呢?
我們需要一箇中間人來聯絡這兩個內網主機。
現在我們來看看一個P2P軟體的流程,以下圖為例:
Fig-3
. 首先,
Client A登入伺服器,NAT A為這次的Session分配了一個埠60000,
那麼Server S收到的Client A的地址是202.187.45.3:60000,這就是Client A的外網地址了。
. 同樣,
Client B登入Server S,NAT B給此次Session分配的埠是40000,
那麼Server S收到的B的地址是187.34.1.56:40000。
. 此時,
Client A與Client B都可以與Server S通訊了。
如果Client A此時想直接傳送資訊給Client B,
那麼他可以從Server S那兒獲得B的公網地址187.34.1.56:40000,
是不是Client A向這個地址傳送資訊Client B就能收到了呢?
答案是不行,因為如果這樣傳送資訊,NAT B會將這個資訊丟棄
(因為這樣的資訊是不請自來的,為了安全,大多數NAT都會執行丟棄動作)。
. 現在
我們需要的是在NAT B上打一個方向為202.187.45.3(即Client A的外網地址)的洞,
那麼Client A傳送到187.34.1.56:40000的資訊,Client B就能收到了。這個打洞命令由誰來發呢?
自然是Server S。
. 總結
一下這個過程:如果Client A想向Client B傳送資訊,那麼Client A傳送命令給Server S,
請求Server S命令Client B向Client A方向打洞。然後Client A就可以通過Client B的外網地址與Client B通訊了。
注意:
以上過程只適合於Cone NAT的情況,
如果是Symmetric NAT,那麼當Client B向Client A打洞的埠已經重新分配了,
Client B將無法知道這個埠(如果Symmetric NAT的埠是順序分配的,那麼我們或許可以猜測這個埠號,
可是由於可能導致失敗的因素太多,這種情況下一般放棄P2P ---peakflys)。
2.2 STUN方式的P2P實現
STUN是RFC3489規定的一種NAT穿透方式,它採用輔助的方法探測NAT的IP和埠。
毫無疑問的,它對穿越早期的NAT起了巨大的作用,並且還將繼續在NAT穿透中佔有一席之地。
STUN的探測過程需要有一個公網IP的STUN server,
在NAT後面的UAC必須和此server配合,互相之間傳送若干個UDP資料包。
UDP包中包含有UAC需要了解的資訊,比如NAT外網IP,PORT等等。
UAC通過是否得到這個UDP包和包中的資料判斷自己的NAT型別。
假設有如下:
UAC(B),NAT(A),SERVER(C),
UAC的IP為 IPB,
NAT的IP為 IPA ,
SERVER的 IP為IPC1 、IPC2。
請注意,伺服器C有兩個IP,後面你會理解為什麼需要兩個IP。
NAT的探測過程:
STEP1:
B 向 C 的IPC1的port1埠傳送一個UDP包。
C 收到這個包後,會把它收到包的源IP和port寫到UDP包中,然後把此包通過 IPC1 和 port1 發還給B。
這個IP和port也就是NAT的外網IP和port,
也就是說你在STEP1中就得到了NAT的外網IP。
熟悉NAT工作原理的應該都知道,C返回給B的這個UDP包B一定收到。
如果在你的應用中,向一個STUN伺服器傳送資料包後,你沒有收到STUN的任何迴應包,
那只有兩種可能:
1)、STUN伺服器不存在,或者你弄錯了port。
2)、你的NAT裝置拒絕一切UDP包從外部向內部通過,
如果排除防火牆限制規則,那麼這樣的NAT裝置如果存在,那肯定是壞了„„
當B收到此UDP後,把此UDP中的IP和自己的IP做比較,
如果是一樣的,就說明自己是在公網,下步NAT將去探測防火牆型別,就不多說了(下面有圖)。
如果不一樣,說明有NAT的存在,系統進行STEP2的操作。
STEP2:
B 向 C的IPC1傳送一個UDP包,
請求C通過另外一個IPC2和PORT(不同與SETP1的IP1)向B返回一個UDP資料包
(現在知道為什麼C要有兩個IP了吧,為了檢測cone NAT的型別)。
我們來分析一下,如果B收到了這個資料包,那說明什麼?
說明NAT來著不拒,不對資料包進行任何過濾,這也就是STUN標準中的full cone NAT。
遺憾的是,full cone nat太少了,這也意味著你能收到這個資料包的可能性不大。
如果沒收到,那麼系統進行STEP3的操作。
STEP3:
B 向 C 的IPC2的port2傳送一個數據包,C收到資料包後,
把它收到包的源IP和port寫到UDP包中,然後通過自己的IPC2和port2把此包發還給B。
和step1一樣,B肯定能收到這個迴應UDP包。
此包中的port是我們最關心的資料,下面我們來分析:
如果這個port和step1中的port一樣,那麼可以肯定這個NAT是個CONE NAT,否則是對稱NAT。
道理很簡單:
根據對稱NAT的規則,當目的地址的IP和port有任何一個改變,那麼NAT都會重新分配一個port使用,
而在step3中,和step1對應,我們改變了IP和port。因此,如果是對稱NAT,那這兩個port肯定是不同的。
如果在你的應用中,到此步的時候PORT是不同的,那就只能放棄P2P了,原因同上面實現中的一樣。
如果不同,那麼只剩下了restrict cone 和port restrict cone。系統用step4探測是是那一種。
STEP4:
B 向 C 的IP2的一個埠PD傳送一個數據請求包,要求C用IP2和不同於PD的port返回一個數據包給B。
我們來分析結果:
如果B收到了,那也就意味著只要IP相同,即使port不同,NAT也允許UDP包通過。
顯然這是restrict cone NAT。如果沒收到,沒別的好說,port restrict NAT.
協議實現的演算法執行圖如下:
Fig-4
一旦路經到達紅色節點時,UDP的溝通是沒有可能性的
(peakflys注:準備來說除了包被防火牆blocked之外,其他情況也是有可能建立P2P的,只是代價太大,一般放棄)。
一旦通過黃色或是綠色的節點,就有連線的可能。
最終通過STUN伺服器得到自己的NAT型別和公網IP、Port,以後建立P2P時就非常容易了
peakflys注:Libjingle正是通過ICE&STUN方式,建立的P2P連線。
關於libjingle的介紹,待續……
參考資料:
1、維基百科之STUN
2、http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt(shootingstars)
原文連結:
http://www.cppblog.com/peakflys/archive/2013/01/25/197562.html
相關推薦
前端跨域原理和常見解決方式
理解跨域必須先了解同源策略,所以—— 何謂同源策略(same origin policy)? 瀏覽器的同源策略,出於防範跨站指令碼的攻擊,禁止客戶端指令碼對不同域的服務進行跨站呼叫。簡單來講就是,從一個域上載入的指令碼不允許訪問另外一個域的文件屬性。
P2P的原理和常見的實現方式
為了專案的後期IM應用,最近在研究libjingle,中間看了也收集了很多資料,感慨網上很多資料要麼太過於糾結協議(如STUN、ICE等)實現細節,要麼中間有很多紕漏。最後去偽存真,歸納總結了一下,希望對以後的同行有些許幫助。如果有什麼需要討論或者指正的,歡迎留言或者郵件[
常見素書篩選方法原理和Python實現
1. 普通篩選(常用於求解單個素數問題) 自然數中,除了1和它本身以外不再有其他因數。 import math def func_get_prime(n): func = lambda x: not [x%i for i in range(2, int(ma
數據結構-排序算法原理和Python實現
遞歸 pivot 依次 新的 樹形 希爾排序 image pso 代碼 排序算法概覽 插入排序 基本思想是每次講一個待排序的記錄,按其關鍵字大小插入到前面已拍好的子序列中,直到全部完成。 直接插入排序 講元素L(i)插入到有序序列L[1,…,i-1]中,執行以下操作: 1
對數損失函數(Logarithmic Loss Function)的原理和 Python 實現
NPU 技術分享 blog 入參 rom __main__ bsp nat () 原理 對數損失, 即對數似然損失(Log-likelihood Loss), 也稱邏輯斯諦回歸損失(Logistic Loss)或交叉熵損失(cross-entropy Loss), 是在
今日頭條面試題——LRU原理和Redis實現
unsigned dom bestv acs 大量 不存在 技術 aci 頭條 很久前參加過今日頭條的面試,遇到一個題,目前半部分是如何實現 LRU,後半部分是 Redis 中如何實現 LRU。 我的第一反應應該是內存不夠的場景下,淘汰舊內容的策略。LRU ... Leas
LRU原理和Redis實現——一個今日頭條的面試題
滿了 存儲空間 當前 node 硬盤 java 原理 remove http 看了評論,發現有些地方有問題,更新了圖和一些描述,希望可以更清晰一些,也歡迎關註,還會有幹貨文章 -------- 很久前參加過今日頭條的面試,遇到一個題,目前半部分是如何實現 LRU,後半部
今天我們來說說Ajax的原理和怎麼實現非同步的!
純JavaScript實現非同步Ajax的基本原理 Ajax實際就是XMLHttpRequest物件和DOM、(X)HTML和CSS的簡稱,用於概括非同步載入頁面內容的技術。 Ajax例項 HTML程式碼如下,包含一個h5標題和一個按鈕: JS程式碼如下: 上述程
Hibernate 和 MyBatis 實現方式的區別
Hibernate 和 MyBatis 都是持久層框架,都會涉及資料庫,所以先定義一個數據庫表,先從程式碼編寫角度對比兩者。 新建一個 POJO 類,和表的欄位對應起來。 package com.learn.chapter1.pojo; implements java.io.Serial
Jenkins 關閉、重啟和過載實現方式
我們用jar -jar jenkins.war來啟動jenkins伺服器,那麼我們如何關閉或者重啟jenkins伺服器呢?經過搜尋找到了相應的方法. 1.關閉jenkins伺服器 只需要在訪問jenkins伺服器的網址url地址後加上exit。例如我jenkins的地址http://loca
HashTable原理和底層實現
1. 概述 上次討論了HashMap的結構,原理和實現,本文來對Map家族的另外一個常用集合HashTable進行介紹。HashTable和HashMap兩種集合非常相似,經常被各種面試官問到兩者的區別。 對於兩者的區別,主要有以下幾點: HashMap是非同步的,
LRU 原理和 Redis 實現
轉自:https://baijiahao.baidu.com/s?id=1595292420641966263&wfr=spider&for=pc 很久前參加過今日頭條的面試,遇到一個題,目前半部分是如何實現 LRU,後半部分是 Redis 中如何實現 LRU。 我的第一反應
LBP小結:LBP及改進版本的原理和opencv實現原始碼
一、LBP特徵的背景介紹 LBP指區域性二值模式,英文全稱:Local Binary Pattern,是一種用來描述影象區域性特徵的運算元,LBP特徵具有灰度不變性和旋轉不變性等顯著優點。它是由T. Ojala, M.Pietikäinen, 和 D. Harwood
gdb工作原理和核心實現
gdb主要功能的實現依賴於一個系統函式ptrace,通過man手冊可以瞭解到,ptrace可以讓父程序觀察和控制其子程序的檢查、執行,改變其暫存器和記憶體的內容,主要應用於打斷點(也是gdb的主要功能)和列印系統呼叫軌跡。 一、ptrace函式 函式原型如下:
TensorFlow Serving的原理和程式碼實現
本文介紹tensorflow Serving的原理和程式碼實現, 並提供簡要的程式碼閱讀指導. 如何serve一個模型 具體的步驟可以參考官方文件. 主要包括兩個部分: 1. 匯出模型 1. 啟動服務 需要說明的是匯出模型部分. 如果要把我們訓
SpringMVC:攔截器實現原理和登入實現
SpringMVC 攔截器的原理圖 springMVC攔截器的實現一般有兩種方式 第一種方式是要定義的Interceptor類要實現了Spring的HandlerInterceptor 介面 &n
樹的深度優先遍歷和廣度優先遍歷的原理和java實現程式碼
樹的深度優先遍歷需要用到額外的資料結構--->棧;而廣度優先遍歷需要佇列來輔助;這裡以二叉樹為例來實現 import java.util.ArrayDeque;publicclassBinaryTree{staticclassTreeNode{int value;
分類演算法-----樸素貝葉斯原理和python實現
本文主要介紹一下內容:1貝葉斯,2 樸素貝葉斯的推導,3 最大似然估計的推到過程,4樸素貝葉斯的計算步驟 ,5 貝葉斯估計 1 貝葉斯 假設有兩類資料p1(x,y)表示(x,y)屬於類別1,用p2(x,y)表示(x,y)屬於類別2,那麼對於一個新的資料集(x,y),可以
[Android] 徹底瞭解Binder機制原理和底層實現
1.Binder通訊機制介紹 這篇文章會先對比Binder機制與Linux的通訊機制的差別,瞭解為什麼Android會另起爐灶,採用Binder。接著,會根據 Binder的機制,去理解什麼是Service Manager,在C/S模型中扮演什麼角色。最後,會從一次完整的通訊活動中,去理解Binder通訊
雜湊表之簡易數學原理和簡易實現(史上最簡單易懂的雜湊表介紹)
什麼是雜湊表呢? 我先不說, 但其思想確實厲害。 下面, 我以最簡單易懂的方式來介紹雜湊表。 你要是去看教科書啊, 還沒有理解雜湊表的原理, 他就給你介紹近10種防衝突的方法, 這就是中國的教育。 你要是去網上搜點資料問為什麼雜湊表查詢的時間複雜