一般情況下,如果要實現(xiàn)聊天即時通訊,都要借助公網(wǎng)服務(wù)器作為中繼節(jié)點對消息進(jìn)行轉(zhuǎn)發(fā)。
例如用戶A和用戶B進(jìn)行即時通訊的具體步驟如下所示
首先用戶A和B需要和公網(wǎng)服務(wù)器建立長連接
ClientA ====> (建立長連接) ===> 公網(wǎng)服務(wù)器
`ClientB ====> (建立長連接) ===> 公網(wǎng)服務(wù)器
緊接著用戶A如果想發(fā)送消息給用戶B,就會采用轉(zhuǎn)發(fā)的形式
ClientA => 公網(wǎng)服務(wù)器(消息轉(zhuǎn)發(fā)) => ClientB
但是我們從中可以看到,如果用戶之間進(jìn)行的是語音視頻通話,所有流量將會從中繼服務(wù)器中經(jīng)過。這將會給中繼服務(wù)器帶來巨大挑戰(zhàn)。
那么是否可以存在一種方式可以拋除中繼服務(wù)器的存在,讓用戶A和用戶B進(jìn)行直連通信呢?
我們知道用戶A和用戶B都在各自的內(nèi)網(wǎng)下,雙方都不知道彼此的地址,那么如何進(jìn)行通信成了問題。
二、P2P 通信與NAT類型
緊接上文,其實用戶A在給中繼服務(wù)器發(fā)送長連接請求后,中繼服務(wù)器就能獲取到運營商給用戶A開放的公網(wǎng)IP和端口。
那么如果用戶B知道了用戶A所在的公網(wǎng)IP和端口,是否就能脫離中繼服務(wù)器的限制,直接發(fā)送請求給用戶A所在的IP和端口呢?
答案是,在一定情況下是可以的。這要求用戶A所在的 NAT 是完全錐形。
NAT 的作用是會將內(nèi)網(wǎng)主機(jī)的IP地址映射為一個公網(wǎng)IP,由于 IPV4 地址池不夠用的情況下,運營商不會給每個接入互聯(lián)網(wǎng)的用戶分配公網(wǎng) IP ,而是多個用戶,或者一整個小區(qū)公用一個公網(wǎng) IP 出口。
當(dāng)用戶發(fā)送網(wǎng)絡(luò)請求時, NAT 會將用戶的內(nèi)網(wǎng) IP 轉(zhuǎn)換為公網(wǎng) IP,并且分配一個公網(wǎng)端口。當(dāng)用戶的請求結(jié)束,一段時間后該這些公共資源將會被回收。
Server S1 Server S2
18.181.0.31:1235 138.76.29.7:1235
| |
| |
+----------------------+----------------------+
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 155.99.25.11:62000 v | v 155.99.25.11:62000 v
|
Cone NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
|
用戶內(nèi)網(wǎng)
10.0.0.1:1234
基于這種特性,NAT一般情況被分為 4 類
- 完全圓錐型NAT (Full Cone NAT)把一個來自內(nèi)部IP地址和端口的所有請求,始終映射到相同的外網(wǎng)IP地址和端口;同時,任意外部主機(jī)向該映射的外網(wǎng)IP地址和端口發(fā)送報文,都可以實現(xiàn)和內(nèi)網(wǎng)主機(jī)進(jìn)行通信,就像一個向外開口的圓錐形一樣,故得名。
- 地址限制式錐形NAT(Address Restricted Cone NAT)地址限制式圓錐形NAT同樣把一個來自內(nèi)部IP地址和端口的所有請求,始終映射到相同的外網(wǎng)IP地址和端口;與完全圓錐型NAT不同的是,當(dāng)內(nèi)網(wǎng)主機(jī)向某公網(wǎng)主機(jī)發(fā)送過報文后,只有該公網(wǎng)主機(jī)才能向內(nèi)網(wǎng)主機(jī)發(fā)送報文,故得名。相比完全錐形,增加了地址限制,也就是IP受限,而端口不受限。
- 端口限制式錐形NAT(Port Restricted Cone NAT)端口限制式圓錐形NAT更加嚴(yán)格,在上述條件下,只有該公網(wǎng)主機(jī)該端口才能向內(nèi)網(wǎng)主機(jī)發(fā)送報文,故得名。相比地址限制錐形又增加了端口限制,也就是說IP、端口都受限。
- 對稱式NAT(Symmetric NAT)對稱式NAT把內(nèi)網(wǎng)IP和端口到相同目的地址和端口的所有請求,都映射到同一個公網(wǎng)地址和端口;同一個內(nèi)網(wǎng)主機(jī),用相同的內(nèi)網(wǎng)IP和端口向另外一個目的地址發(fā)送報文,則會用不同的映射(比如映射到不同的端口)。和端口限制式NAT不同的是,端口限制式NAT是所有請求映射到相同的公網(wǎng)IP地址和端口,而對稱式NAT是為不同的請求建立不同的映射。它具有端口受限錐型的受限特性,內(nèi)部地址每一次請求一個特定的外部地址,都可能會綁定到一個新的端口號。也就是請求不同的外部地址映射的端口號是可能不同的。這種類型基本上就告別 P2P 了。
一般情況下,家用 NAT 是NAT3,也就是 端口限制式錐形NAT。我們基于這一特性可以嘗試讓兩臺主機(jī)進(jìn)行內(nèi)網(wǎng)端對端直連。
請注意,P2P通信不意味著全程不需要服務(wù)器的介入。服務(wù)器的介入只是為了讓雙方節(jié)點都獲取到各自穿透的公網(wǎng) IP和端口,實現(xiàn)的具體流程請方法下圖。

P2P 內(nèi)網(wǎng)穿透通信與端口復(fù)用|Golang 代碼示例
[Gbuy id='18608']請注意這里使用到了端口復(fù)用技術(shù)。因為我們的端口不僅要監(jiān)聽一個服務(wù),并且這個端口還能進(jìn)行復(fù)用發(fā)送網(wǎng)絡(luò)請求。
具體代碼示例如下:
代碼我把它托管到了 Github 上,并且有完整說明,鏈接如下
https://github.com/xhyonline/p2p-demo
server.go
代碼其實很簡單,server.go 只做一件事,交換兩個內(nèi)網(wǎng)節(jié)點臨時生成的公網(wǎng) IP 和端口
package main
import (
"encoding/json"
"fmt"
"github.com/go-basic/uuid"
"github.com/libp2p/go-reuseport"
"net"
"time"
)
type Client struct {
UID string
Conn net.Conn
Address string
}
type Handler struct {
// 服務(wù)端句柄
Listener net.Listener
// 客戶端句柄池
ClientPool map[string]*Client
}
func (s *Handler) Handle() {
for {
conn, err := s.Listener.Accept()
if err != nil {
fmt.Println("獲取連接句柄失敗", err.Error())
continue
}
id := uuid.New()
s.ClientPool[id] = &Client{
UID: id,
Conn: conn,
Address: conn.RemoteAddr().String(),
}
fmt.Println("一個客戶端連接進(jìn)去了,他的公網(wǎng)IP是", conn.RemoteAddr().String())
// 暫時只接受兩個客戶端,多余的不處理
if len(s.ClientPool) == 2 {
// 交換雙方的公網(wǎng)地址
s.ExchangeAddress()
break
}
}
}
// ExchangeAddress 交換地址
func (s *Handler) ExchangeAddress() {
for uid, client := range s.ClientPool {
for id, c := range s.ClientPool {
// 自己不交換
if uid == id {
continue
}
var data = make(map[string]string)
data["dst_uid"] = client.UID // 對方的 UID
data["address"] = client.Address // 對方的公網(wǎng)地址
body, _ := json.Marshal(data)
if _, err := c.Conn.Write(body); err != nil {
fmt.Println("交換地址時出現(xiàn)了錯誤", err.Error())
}
}
}
}
func main() {
address := fmt.Sprintf("0.0.0.0:6999")
listener, err := reuseport.Listen("tcp", address)
if err != nil {
panic("服務(wù)端監(jiān)聽失敗" + err.Error())
}
h := &Handler{Listener: listener, ClientPool: make(map[string]*Client)}
// 監(jiān)聽內(nèi)網(wǎng)節(jié)點連接,交換彼此的公網(wǎng) IP 和端口
h.Handle()
time.Sleep(time.Hour) // 防止主線程退出
}
client.go
客戶端得到對方的臨時生成的公網(wǎng)IP和端口后,嘗試進(jìn)行連接,并不停發(fā)送數(shù)據(jù)
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"github.com/libp2p/go-reuseport"
"math"
"math/big"
"net"
"time"
)
type Handler struct {
// 中繼服務(wù)器的連接句柄
ServerConn net.Conn
// p2p 連接
P2PConn net.Conn
// 端口復(fù)用
LocalPort int
}
// WaitNotify 等待遠(yuǎn)程服務(wù)器發(fā)送通知告知我們另一個用戶的公網(wǎng)IP
func (s *Handler) WaitNotify() {
buffer := make([]byte, 1024)
n, err := s.ServerConn.Read(buffer)
if err != nil {
panic("從服務(wù)器獲取用戶地址失敗" + err.Error())
}
data := make(map[string]string)
if err := json.Unmarshal(buffer[:n], &data); err != nil {
panic("獲取用戶信息失敗" + err.Error())
}
fmt.Println("客戶端獲取到了對方的地址:", data["address"])
// 斷開服務(wù)器連接
defer s.ServerConn.Close()
// 請求用戶的臨時公網(wǎng) IP
go s.DailP2PAndSayHello(data["address"], data["dst_uid"])
}
// DailP2PAndSayHello 連接對方臨時的公網(wǎng)地址,并且不停的發(fā)送數(shù)據(jù)
func (s *Handler) DailP2PAndSayHello(address, uid string) {
var errCount = 1
var conn net.Conn
var err error
for {
// 重試三次
if errCount > 3 {
break
}
time.Sleep(time.Second)
conn, err = reuseport.Dial("tcp", fmt.Sprintf(":%d", s.LocalPort), address)
if err != nil {
fmt.Println("請求第", errCount, "次地址失敗,用戶地址:", address)
errCount++
continue
}
break
}
if errCount > 3 {
panic("客戶端連接失敗")
}
s.P2PConn = conn
go s.P2PRead()
go s.P2PWrite()
}
// P2PRead 讀取 P2P 節(jié)點的數(shù)據(jù)
func (s *Handler) P2PRead() {
for {
buffer := make([]byte, 1024)
n, err := s.P2PConn.Read(buffer)
if err != nil {
fmt.Println("讀取失敗", err.Error())
time.Sleep(time.Second)
continue
}
body := string(buffer[:n])
fmt.Println("讀取到的內(nèi)容是:", body)
fmt.Println("來自地址", s.P2PConn.RemoteAddr())
fmt.Println("=============")
}
}
// P2PWrite 向遠(yuǎn)程 P2P 節(jié)點寫入數(shù)據(jù)
func (s *Handler) P2PWrite() {
for {
if _, err := s.P2PConn.Write([]byte("你好呀~")); err != nil {
fmt.Println("客戶端寫入錯誤")
}
time.Sleep(time.Second)
}
}
func main() {
// 指定本地端口
localPort := RandPort(10000, 50000)
// 向 P2P 轉(zhuǎn)發(fā)服務(wù)器注冊自己的臨時生成的公網(wǎng) IP (請注意,Dial 這里撥號指定了自己臨時生成的本地端口)
serverConn, err := reuseport.Dial("tcp", fmt.Sprintf(":%d", localPort), "你自己的公網(wǎng)服務(wù)器IP:6999")
if err != nil {
panic("請求遠(yuǎn)程服務(wù)器失敗" + err.Error())
}
h := &Handler{ServerConn: serverConn, LocalPort: int(localPort)}
h.WaitNotify()
time.Sleep(time.Hour)
}
// RandPort 生成區(qū)間范圍內(nèi)的隨機(jī)端口
func RandPort(min, max int64) int64 {
if min > max {
panic("the min is greater than max!")
}
if min < 0 {
f64Min := math.Abs(float64(min))
i64Min := int64(f64Min)
result, _ := rand.Int(rand.Reader, big.NewInt(max+1+i64Min))
return result.Int64() - i64Min
}
result, _ := rand.Int(rand.Reader, big.NewInt(max-min+1))
return min + result.Int64()
}
【標(biāo)準(zhǔn)版】400元/年/5用戶/無限容量
【外貿(mào)版】500元/年/5用戶/無限容量
其它服務(wù):網(wǎng)站建設(shè)、企業(yè)郵箱、數(shù)字證書ssl、400電話、
聯(lián)系方式:電話:18696588163 微信同號
聲明:本站所有作品(圖文、音視頻)均由用戶自行上傳分享,或互聯(lián)網(wǎng)相關(guān)知識整合,僅供網(wǎng)友學(xué)習(xí)交流,若您的權(quán)利被侵害,請聯(lián)系 管理員 刪除。
本文鏈接:http://www.artemismd.com/article_32638.html