P2P 内网穿透通信与端口复用|Golang 代码示例

一般情况下,如果要实【shí】现【xiàn】聊天即时【shí】通讯,都要借【jiè】助【zhù】公网服务器作为【wéi】中继【jì】节点【diǎn】对消息进行转发。

例如用户A和用户B进行即时通讯的具体步骤如下所示

首先用户A和B需要和公网服务器建立长连接

ClientA ====> (建【jiàn】立长连接) ===> 公网服【fú】务【wù】器

`ClientB ====> (建【jiàn】立长连接) ===> 公网服【fú】务【wù】器

紧接着用户A如果想发送消息给用户B,就会采用转发的形式

ClientA => 公【gōng】网【wǎng】服务器(消息转发【fā】) => ClientB

但是我【wǒ】们从中可以看到,如果用户之间进【jìn】行的是语【yǔ】音【yīn】视频通话,所有流【liú】量【liàng】将会从中继服务器【qì】中经过。这将会【huì】给【gěi】中继服务器带来【lái】巨大挑战【zhàn】。

那么是【shì】否可以存【cún】在一种方式可以抛除中继服务器的存【cún】在,让用户A和用户B进【jìn】行直连【lián】通信呢【ne】?

我们【men】知道用【yòng】户A和【hé】用户B都在各自的内网下,双【shuāng】方【fāng】都不知道【dào】彼此的地址,那么如何进行通信成了问题。

二、P2P 通信与NAT类型

紧【jǐn】接上文,其【qí】实用户【hù】A在给【gěi】中继服务器发【fā】送长连接【jiē】请求【qiú】后,中继【jì】服务【wù】器就能获取到运营商给用户A开放的公网IP和端口。

那么如果用【yòng】户B知道【dào】了用户A所【suǒ】在【zài】的公网IP和【hé】端口,是否就能【néng】脱【tuō】离中【zhōng】继服务【wù】器的限制,直接发【fā】送请求给用户A所在【zài】的IP和端口呢?

答【dá】案是,在一定【dìng】情况【kuàng】下是可以的。这要求用户A所在的【de】 NAT 是完全锥形。

NAT 的作用是会将【jiāng】内【nèi】网主机的【de】IP地址【zhǐ】映射【shè】为一【yī】个公网IP,由于 IPV4 地址池不够用的【de】情【qíng】况下,运【yùn】营商不会给每个【gè】接入互【hù】联网的用户分配公网 IP ,而是多个用户,或者【zhě】一整【zhěng】个小区【qū】公用一个公网 IP 出口。

当用户发送【sòng】网络请【qǐng】求时, NAT 会将用户的内网 IP 转【zhuǎn】换为公【gōng】网 IP,并【bìng】且分配一个公网端口。当用【yòng】户的请【qǐng】求结【jié】束【shù】,一段时间后该这【zhè】些公【gōng】共资源将会【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
                              |
                           用【yòng】户内【nèi】网
                        10.0.0.1:1234

基于这种特性,NAT一般情况被分为 4 类

  1. 完全圆锥型NAT (Full Cone NAT)把一个来自内部IP地址和端口的所有请求,始【shǐ】终【zhōng】映射到相【xiàng】同【tóng】的外网IP地址【zhǐ】和端口;同时,任意【yì】外【wài】部主机向该映射【shè】的外网【wǎng】IP地【dì】址和端口发送报文,都可以实现【xiàn】和内网主机进行通信【xìn】,就【jiù】像【xiàng】一个【gè】向外【wài】开口的圆锥形一样,故【gù】得【dé】名。
  2. 地址限制式锥【zhuī】形NAT(Address Restricted Cone NAT)地【dì】址限制【zhì】式圆锥形NAT同样把一个来自内【nèi】部IP地址和端口的所有请求【qiú】,始终映射到相同的【de】外网IP地址和端口;与完【wán】全圆锥【zhuī】型NAT不同的是,当内网主【zhǔ】机【jī】向某公网【wǎng】主机发送过报文后,只有该公【gōng】网主机才能向内网主机发送报文,故【gù】得【dé】名。相比完全锥形,增加了地址【zhǐ】限制,也【yě】就是IP受【shòu】限,而端【duān】口【kǒu】不受限。
  3. 端口限制式【shì】锥形NAT(Port Restricted Cone NAT)端口限制式圆锥形NAT更加严格,在上述条件【jiàn】下,只有该【gāi】公网【wǎng】主机【jī】该端口才能向内网主机发送报文,故【gù】得【dé】名【míng】。相比地址【zhǐ】限制锥形又【yòu】增加了端口【kǒu】限【xiàn】制,也就是说IP、端口【kǒu】都受【shòu】限【xiàn】。
  4. 对称式NAT(Symmetric NAT)对【duì】称式NAT把内网IP和端【duān】口到相同目的地址和【hé】端口【kǒu】的所有【yǒu】请求,都映射【shè】到【dào】同一个公网地址【zhǐ】和端口;同一个内网主机,用相同的内【nèi】网IP和端口向【xiàng】另【lìng】外一个目的地【dì】址发送报文,则会用【yòng】不同的映射(比如【rú】映【yìng】射到不同的端口)。和端口限制式NAT不同【tóng】的【de】是,端口限制式NAT是【shì】所【suǒ】有请求映【yìng】射到相【xiàng】同的公网IP地址【zhǐ】和端口【kǒu】,而对称式NAT是为不同的【de】请【qǐng】求建【jiàn】立不同【tóng】的【de】映射。它具有端【duān】口受限【xiàn】锥型的受限特性【xìng】,内部地址每一次请求一【yī】个特定的外【wài】部地址,都可能会绑定到一个【gè】新的端【duān】口【kǒu】号。也就是请求不同的外部地址【zhǐ】映【yìng】射的【de】端口号是可能不同的。这种【zhǒng】类【lèi】型【xíng】基本上【shàng】就告别 P2P 了。

一般情况下,家用 NAT 是NAT3,也就是 端口限制式锥形NAT。我们基于这一特性可以尝试让两台主机进行内网端对端直连。

请注意,P2P通信不意味着全程【chéng】不需要【yào】服务器【qì】的介入【rù】。服务器的介入只是【shì】为了让双方节点都获取【qǔ】到【dào】各自穿透的公网【wǎng】 IP和端口,实现的具体流程请方法【fǎ】下图。

P2P 内网穿透通信与端口复用|Golang 代码示例

[Gbuy id='18608']

请注意这里使用到了端口复用技术。因【yīn】为【wéi】我们【men】的端口不【bú】仅要监听【tīng】一个服务,并且这个【gè】端口还能进行复【fù】用【yòng】发送网络请【qǐng】求【qiú】。

具体代码示例如下:

代码【mǎ】我把它托【tuō】管到了 Github 上,并且有完【wán】整说明,链【liàn】接如下

https://github.com/xhyonline/p2p-demo

server.go

代【dài】码其实很简单,server.go 只做一件事,交换【huàn】两个内网节【jiē】点【diǎn】临【lín】时生成的【de】公网 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("服务端监听失败" + err.Error())
	}
	h := &Handler{Listener: listener, ClientPool: make(map[string]*Client)}
	// 监听内网节点连接,交换彼此的公网 IP 和端口
	h.Handle()
	time.Sleep(time.Hour) // 防止主线程退出
}

client.go

客户端得到对方【fāng】的临时生成的公网IP和端口后,尝【cháng】试进【jìn】行连【lián】接,并【bìng】不停发送【sòng】数据

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 等待远【yuǎn】程【chéng】服务器【qì】发送通知告知我们另一【yī】个用户的公网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 连接对【duì】方临时的公【gōng】网地址,并且不停的发送数据【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 节点的数据
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 节点写入数据
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ú】务【wù】器【qì】注册自己的临【lín】时生成的公网 IP (请【qǐng】注意,Dial 这里拨号指定了自己临时生【shēng】成【chéng】的本地端口)
	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 生成区间范围内的随机端口
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用户/无限容量
其它【tā】服务:网站建设、企业邮箱【xiāng】、数字证【zhèng】书ssl、400电话、
联系方式:电话:13714666846 微信同号

声明:本站所有作品(图文、音【yīn】视频)均由用【yòng】户自行上传【chuán】分享【xiǎng】,或互【hù】联【lián】网相关知识整合,仅供网友学习交流,若【ruò】您的权利被侵害,请【qǐng】联系【xì】 管【guǎn】理员 删除。

本【běn】文链接【jiē】:https://www.city96.com/article_32638.html