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

P2P 內(nèi)網(wǎng)穿透通信與端口復(fù)用|Golang 代碼示例
[Gbuy id='18608']請(qǐng)注意這里使用到了端口復(fù)用技術(shù)。因?yàn)槲覀兊亩丝诓粌H要監(jiān)聽一個(gè)服務(wù),并且這個(gè)端口還能進(jìn)行復(fù)用發(fā)送網(wǎng)絡(luò)請(qǐng)求。
具體代碼示例如下:
代碼我把它托管到了 Github 上,并且有完整說(shuō)明,鏈接如下
https://github.com/xhyonline/p2p-demo
server.go
代碼其實(shí)很簡(jiǎn)單,server.go 只做一件事,交換兩個(gè)內(nèi)網(wǎng)節(jié)點(diǎn)臨時(shí)生成的公網(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("一個(gè)客戶端連接進(jìn)去了,他的公網(wǎng)IP是", conn.RemoteAddr().String())
// 暫時(shí)只接受兩個(gè)客戶端,多余的不處理
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 // 對(duì)方的 UID
data["address"] = client.Address // 對(duì)方的公網(wǎng)地址
body, _ := json.Marshal(data)
if _, err := c.Conn.Write(body); err != nil {
fmt.Println("交換地址時(shí)出現(xiàn)了錯(cuò)誤", 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é)點(diǎn)連接,交換彼此的公網(wǎng) IP 和端口
h.Handle()
time.Sleep(time.Hour) // 防止主線程退出
}
client.go
客戶端得到對(duì)方的臨時(shí)生成的公網(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ā)送通知告知我們另一個(gè)用戶的公網(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("客戶端獲取到了對(duì)方的地址:", data["address"])
// 斷開服務(wù)器連接
defer s.ServerConn.Close()
// 請(qǐng)求用戶的臨時(shí)公網(wǎng) IP
go s.DailP2PAndSayHello(data["address"], data["dst_uid"])
}
// DailP2PAndSayHello 連接對(duì)方臨時(shí)的公網(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("請(qǐng)求第", errCount, "次地址失敗,用戶地址:", address)
errCount++
continue
}
break
}
if errCount > 3 {
panic("客戶端連接失敗")
}
s.P2PConn = conn
go s.P2PRead()
go s.P2PWrite()
}
// P2PRead 讀取 P2P 節(jié)點(diǎn)的數(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("來(lái)自地址", s.P2PConn.RemoteAddr())
fmt.Println("=============")
}
}
// P2PWrite 向遠(yuǎn)程 P2P 節(jié)點(diǎn)寫入數(shù)據(jù)
func (s *Handler) P2PWrite() {
for {
if _, err := s.P2PConn.Write([]byte("你好呀~")); err != nil {
fmt.Println("客戶端寫入錯(cuò)誤")
}
time.Sleep(time.Second)
}
}
func main() {
// 指定本地端口
localPort := RandPort(10000, 50000)
// 向 P2P 轉(zhuǎn)發(fā)服務(wù)器注冊(cè)自己的臨時(shí)生成的公網(wǎng) IP (請(qǐng)注意,Dial 這里撥號(hào)指定了自己臨時(shí)生成的本地端口)
serverConn, err := reuseport.Dial("tcp", fmt.Sprintf(":%d", localPort), "你自己的公網(wǎng)服務(wù)器IP:6999")
if err != nil {
panic("請(qǐng)求遠(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用戶/無(wú)限容量
【外貿(mào)版】500元/年/5用戶/無(wú)限容量
其它服務(wù):網(wǎng)站建設(shè)、企業(yè)郵箱、數(shù)字證書ssl、400電話、
聯(lián)系方式:電話:13714666846 微信同號(hào)
聲明:本站所有作品(圖文、音視頻)均由用戶自行上傳分享,或互聯(lián)網(wǎng)相關(guān)知識(shí)整合,僅供網(wǎng)友學(xué)習(xí)交流,若您的權(quán)利被侵害,請(qǐng)聯(lián)系 管理員 刪除。
本文鏈接:http://www.goalq.com.cn/article_32638.html