(建立長連接) === " />

亚洲一二区制服无码中字,国产无套露脸在线观看,中文字幕丝袜人妻制服丝袜在线,国内精品自线在拍2020不卡,香蕉影院在线观看

P2P 內網穿透通信與端口復用|Golang 代碼示例

一般情況下,如果要實現聊天即時通訊,都要借助公網服務器作為中繼節(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 類

  1. 完全圓錐型NAT (Full Cone NAT)把一個來自內部IP地址和端口的所有請求,始終映射到相同的外網IP地址和端口;同時,任意外部主機向該映射的外網IP地址和端口發(fā)送報文,都可以實現和內網主機進行通信,就像一個向外開口的圓錐形一樣,故得名。
  2. 地址限制式錐形NAT(Address Restricted Cone NAT)地址限制式圓錐形NAT同樣把一個來自內部IP地址和端口的所有請求,始終映射到相同的外網IP地址和端口;與完全圓錐型NAT不同的是,當內網主機向某公網主機發(fā)送過報文后,只有該公網主機才能向內網主機發(fā)送報文,故得名。相比完全錐形,增加了地址限制,也就是IP受限,而端口不受限。
  3. 端口限制式錐形NAT(Port Restricted Cone NAT)端口限制式圓錐形NAT更加嚴格,在上述條件下,只有該公網主機該端口才能向內網主機發(fā)送報文,故得名。相比地址限制錐形又增加了端口限制,也就是說IP、端口都受限。
  4. 對稱式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()
}
阿里企業(yè)郵箱、網易企業(yè)郵箱、新網企業(yè)郵箱
【標準版】400元/年/5用戶/無限容量
【外貿版】500元/年/5用戶/無限容量
其它服務:網站建設、企業(yè)郵箱、數字證書ssl、400電話、
聯(lián)系方式:電話:18696588163 微信同號

聲明:本站所有作品(圖文、音視頻)均由用戶自行上傳分享,或互聯(lián)網相關知識整合,僅供網友學習交流,若您的權利被侵害,請聯(lián)系 管理員 刪除。

本文鏈接:http://integra-biosciences.com.cn/article_32638.html