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

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