在遊戲即時通訊下,兩台不同網路下的電腦要互相連接,是很常見的連線問題,透過一個 UDP 防火牆穿透技術,可以從 NAT 其中一種網路架構 Port Restriction Cone ,實作直接連接通信。
使用防火牆穿透技術,這表示同常兩端的電腦都不是直接對外公開 IP ,而是從任何有規模的網路底下建立連接的必要步驟。
UDP 穿透技術已經行之有年,整個實作的原理其實很簡單,但面對 NAT Translation 問題時,卻有很大的困難, 如上面所說的,UDP 防火牆穿透 (UDP Hole-punching) 只能在 Port Restriction Cone 的情形,大部分家用 Router 直接對連 PPPoE 的話,很有可能就是此種架構,稍後在測試時一定要注意是否自己是這種架構。
依照時序來看,真實的環境是:
- HOME A 的 PC 1 ,連接 Public IP Server : OOO.OOO.OOO.OOO:5000 等待回傳另一端
- HOME B 的 PC 1 ,連接 Public IP Server : OOO.OOO.OOO.OOO:5000 等待回傳另一端
因此,在這個情境,要把 UDP Server 程式碼放在 OOO.OOO.OOO.OOO 這台在公開網路的電腦上。
udpserver.go:
package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4zero,
Port: 5000, //對外公開 Prt 5000
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())
//定義有兩台電腦要互相連接
var a_computer, b_computer net.UDPAddr
//等待第一台電腦連接
a_computer = *waitAndRead(listener)
//等待第二台電腦連接
b_computer = *waitAndRead(listener)
//把 b 電腦資訊傳給 a 電腦
listener.WriteToUDP([]byte(b_computer.String()), &a_computer)
//把 a 電腦資訊傳給 b 電腦
listener.WriteToUDP([]byte(a_computer.String()), &b_computer)
//伺服器可以掰掰了
fmt.Println("Server Mission Clear.")
}
func waitAndRead(listener *net.UDPConn) *net.UDPAddr {
buff := make([]byte, 1024)
n, remoteAddr, err := listener.ReadFromUDP(buff)
if err != nil {
fmt.Printf("error during read %s", err)
}
fmt.Printf("<%s> %s\n", remoteAddr, buff[:n])
//port := strconv.Itoa(remoteAddr.Port)
return remoteAddr
}
Server 要做的事就是這麼簡單。
然後是兩台在不同區域的電腦下要互相連接,所要用的程式碼。
udpclient.go:
package main
import (
"fmt"
"net"
"strconv"
"strings"
"time"
)
func main() {
//選擇一個本地打洞的 port,我在這裡是固定為 5001 (注意,那表示下面連接時是固定連到對方的 5001,你不能在自己電腦開兩個 5001 的 app)
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 5001} //fixed same port for all computer, do not use 0 by dynamic
//輸入公開網路那台 server 的 ip 以及那個 port (請替換 0.0.0.0)
dstAddr := &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5000}
//先跟 server 連接
conn, err := net.DialUDP("udp", srcAddr, dstAddr)
if err != nil {
fmt.Println(err)
}
//隨便傳一個訊息給 server ,目的是讓 server 知道我的 IP 地址
//send msg to central public server(let server know my address)
conn.Write([]byte("hello"))
fmt.Println("Hole Punching, UDP Client to Public Server.")
//強制等待 server 告訴我對方的 IP
//receive another computer information...
fmt.Println("Waiting for Public Server Return Message...")
var another_computer *net.UDPAddr = waitAndReadAndParseUDP(conn)
fmt.Println("Successfuly get another computer address!")
fmt.Println("ADDRESS: ", another_computer.String())
//連接成功,跟 server 斷線,準備跟另一台電腦連接
fmt.Println("Close Original UDP Connection(Port release)")
conn.Close()
//直接連接對方電腦,使用的 port 是 5001 跟自己一樣
//send message to other computer directly
fmt.Println("Build a connection to another computer port and OPEN NAT PORT wait for receive message")
another_conn, err := net.DialUDP("udp", srcAddr, another_computer)
if err != nil {
fmt.Println("Can't Build Hole Punching...")
fmt.Println(err)
}
defer another_conn.Close()
_, err = conn.Write([]byte("Hole Punching"))
if err != nil {
fmt.Println("Failed to send punched message")
fmt.Println(err)
}
//建立連接之後,瘋狂送訊息
fmt.Println("Keep Send Message")
//keep send message...
go func() {
for {
_, err = another_conn.Write([]byte("Hello This is Another One..."))
if err != nil {
fmt.Println("Failed to send message")
fmt.Println(err)
}
time.Sleep(1 * time.Second)
fmt.Println("Sent...")
}
}()
//瘋狂接收對方訊息
//keep receiving message
for {
buff := make([]byte, 1024)
n, remoteAddr, err := another_conn.ReadFromUDP(buff)
if err != nil {
fmt.Printf("error during read %s", err)
}
fmt.Printf("<%s> %s\n", remoteAddr, buff[:n])
}
}
func waitAndReadAndParseUDP(listener *net.UDPConn) *net.UDPAddr {
buff := make([]byte, 1024)
n, _, err := listener.ReadFromUDP(buff)
if err != nil {
fmt.Printf("error during read %s", err)
}
another_computer_address := strings.Split(string(buff[:n]), ":")
port, _ := strconv.Atoi(another_computer_address[1])
return &net.UDPAddr{
IP: net.ParseIP(another_computer_address[0]),
Port: port,
}
}
在這段程式中的缺點是 port 是固定的,如果要浮動各自電腦的 port,其實只要在跟 server 建立連接時,也把自己開的 port 傳出去給 server 幫忙交換資訊,也可以達到效果,只是因為我的電腦網路環境無法任意開 port,防火牆有擋住 Inbound rule,所以只能用 5001。
詳細其他穿透手段,可以參考 Reference。
Reference:
https://blog.csdn.net/chen3888015/article/details/7474922
https://www.zhihu.com/question/20436734
https://blog.csdn.net/jq0123/article/details/840302
https://portforward.com/nat-types/
沒有留言:
張貼留言