欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang實(shí)現(xiàn)內(nèi)網(wǎng)穿透詳解

 更新時(shí)間:2024年11月13日 08:34:11   作者:bbuu  
這篇文章主要為大家詳細(xì)介紹了Golang實(shí)現(xiàn)內(nèi)網(wǎng)穿透的相關(guān)知識(shí),包括原理和代碼實(shí)現(xiàn),文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下

我們經(jīng)常會(huì)遇到一個(gè)問(wèn)題,如何將本機(jī)的服務(wù)暴露到公網(wǎng)上,讓別人也可以訪問(wèn)。我們知道,在家上網(wǎng)的時(shí)候我們有一個(gè) IP 地址,但是這個(gè) IP 地址并不是一個(gè)公網(wǎng)的 IP 地址,別人無(wú)法通過(guò)一個(gè) IP 地址訪問(wèn)到你的服務(wù),所以在例如:微信接口調(diào)試、三方對(duì)接的時(shí)候,你必須將你的服務(wù)部署到一個(gè)公網(wǎng)的系統(tǒng)中去,這樣太累了。 這個(gè)時(shí)候,內(nèi)網(wǎng)穿透就出現(xiàn)了,它的作用就是即使你在家的服務(wù),也能被其人訪問(wèn)到。 今天讓我們來(lái)用一個(gè)最簡(jiǎn)單的案例學(xué)習(xí)一下如何用 go 來(lái)做一個(gè)最簡(jiǎn)單的內(nèi)網(wǎng)穿透工具。

整體結(jié)構(gòu)

首先我們用幾張圖來(lái)說(shuō)明一下我們是如何實(shí)現(xiàn)的,說(shuō)清楚之后再來(lái)用代碼實(shí)現(xiàn)一下。

當(dāng)前網(wǎng)絡(luò)情況

我們可以看到,畫實(shí)線的是我們當(dāng)前可以訪問(wèn)的,畫虛線的是我們當(dāng)前無(wú)法進(jìn)行直接訪問(wèn)的。

我們現(xiàn)在有的路是:

  • 用戶主動(dòng)訪問(wèn)公網(wǎng)服務(wù)器是可以的
  • 內(nèi)網(wǎng)主動(dòng)訪問(wèn)公網(wǎng)服務(wù)也是可以的

當(dāng)前我們要做的是想辦法能讓用戶訪問(wèn)到內(nèi)網(wǎng)服務(wù),所以如果能做到公網(wǎng)服務(wù)訪問(wèn)到內(nèi)網(wǎng)服務(wù),那么用戶就能間接訪問(wèn)到內(nèi)網(wǎng)服務(wù)了。

想是這么想的,但是實(shí)際怎么做呢?用戶訪問(wèn)不到內(nèi)網(wǎng)服務(wù),那我公網(wǎng)服務(wù)器同樣訪問(wèn)不到吧。所以我們就需要利用現(xiàn)有的鏈路來(lái)完成這件事。

基本架構(gòu)

  • 內(nèi)網(wǎng),客戶端(我們要搞一個(gè))
  • 外網(wǎng),服務(wù)端(我們也要搞一個(gè))
  • 訪問(wèn)者,用戶
  • 首先我們需要一個(gè)控制通道來(lái)傳遞消息,因?yàn)橹挥袃?nèi)網(wǎng)可以訪問(wèn)公網(wǎng),公網(wǎng)不知道內(nèi)網(wǎng)在哪里,所以第一次肯定需要客戶端主動(dòng)告訴服務(wù)端我在哪
  • 服務(wù)端通過(guò) 8007 端口監(jiān)聽用戶來(lái)的請(qǐng)求
  • 當(dāng)用戶發(fā)來(lái)請(qǐng)求時(shí),服務(wù)端需要通過(guò)控制信道告訴客戶端,有用戶來(lái)了
  • 客戶端收到消息之后建立隧道通道,主動(dòng)訪問(wèn)服務(wù)端的 8008 來(lái)建立 TCP 連接
  • 此時(shí)客戶端需要同時(shí)與本地需要暴露的服務(wù) 127.0.0.1:8080 建立連接
  • 連接完成后,服務(wù)端需要將 8007 的請(qǐng)求轉(zhuǎn)發(fā)到隧道端口 8008 中
  • 客戶端從隧道中獲得用戶請(qǐng)求,轉(zhuǎn)發(fā)給內(nèi)網(wǎng)服務(wù),同時(shí)將內(nèi)網(wǎng)服務(wù)的返回信息放入隧道

最終請(qǐng)求流向是,如圖中的紫色箭頭走向,請(qǐng)求返回是如圖中紅色箭頭走向。

需要理解的是,TCP 一旦建立了連接,雙方就都可以向?qū)Ψ桨l(fā)送信息了,所以其實(shí)原理很簡(jiǎn)單,就是利用已有的單向路建立 TCP 連接,從而知道對(duì)方的位置信息,然后將請(qǐng)求進(jìn)行轉(zhuǎn)發(fā)即可。

代碼實(shí)現(xiàn)

工具方法

首先我們先定義三個(gè)需要使用的工具方法,還需要定義兩個(gè)消息編碼常量,后面會(huì)用到

  • 監(jiān)聽一個(gè)地址對(duì)應(yīng)的 TCP 請(qǐng)求 CreateTCPListener
  • 連接一個(gè) TCP 地址 CreateTCPConn
  • 將一個(gè) TCP-A 連接的數(shù)據(jù)寫入另一個(gè) TCP-B 連接,將 TCP-B 連接返回的數(shù)據(jù)寫入 TCP-A 的連接中 Join2Conn (別看這短短 10 幾行代碼,這就是核心了)
package network

import (
   "io"
   "log"
   "net"
)

const (
   KeepAlive     = "KEEP_ALIVE"
   NewConnection = "NEW_CONNECTION"
)

func CreateTCPListener(addr string) (*net.TCPListener, error) {
   tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
   if err != nil {
       return nil, err
   }
   tcpListener, err := net.ListenTCP("tcp", tcpAddr)
   if err != nil {
       return nil, err
   }
   return tcpListener, nil
}

func CreateTCPConn(addr string) (*net.TCPConn, error) {
   tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
   if err != nil {
      return nil, err
   }
   tcpListener, err := net.DialTCP("tcp",nil, tcpAddr)
   if err != nil {
      return nil, err
   }
   return tcpListener, nil
}

func Join2Conn(local *net.TCPConn, remote *net.TCPConn) {
   go joinConn(local, remote)
   go joinConn(remote, local)
}

func joinConn(local *net.TCPConn, remote *net.TCPConn) {
   defer local.Close()
   defer remote.Close()
   _, err := io.Copy(local, remote)
   if err != nil {
      log.Println("copy failed ", err.Error())
      return
   }
}

客戶端

我們先來(lái)實(shí)現(xiàn)相對(duì)簡(jiǎn)單的客戶端,客戶端主要做的事情是 3 件:

  • 連接服務(wù)端的控制通道
  • 等待服務(wù)端從控制通道中發(fā)來(lái)建立連接的消息
  • 收到建立連接的消息時(shí),將本地服務(wù)和遠(yuǎn)端隧道建立連接(這里就要用到我們的工具方法了)
package main

import (
   "bufio"
   "io"
   "log"
   "net"

   "nat-proxy/cmd/network"
)

var (
   // 本地需要暴露的服務(wù)端口
   localServerAddr = "127.0.0.1:32768"

   remoteIP = "111.111.111.111"
   // 遠(yuǎn)端的服務(wù)控制通道,用來(lái)傳遞控制信息,如出現(xiàn)新連接和心跳
   remoteControlAddr = remoteIP + ":8009"
   // 遠(yuǎn)端服務(wù)端口,用來(lái)建立隧道
   remoteServerAddr  = remoteIP + ":8008"
)

func main() {
   tcpConn, err := network.CreateTCPConn(remoteControlAddr)
   if err != nil {
      log.Println("[連接失敗]" + remoteControlAddr + err.Error())
      return
   }
   log.Println("[已連接]" + remoteControlAddr)

   reader := bufio.NewReader(tcpConn)
   for {
      s, err := reader.ReadString('\n')
      if err != nil || err == io.EOF {
         break
      }

      // 當(dāng)有新連接信號(hào)出現(xiàn)時(shí),新建一個(gè)tcp連接
      if s == network.NewConnection+"\n" {
         go connectLocalAndRemote()
      }
   }

   log.Println("[已斷開]" + remoteControlAddr)
}

func connectLocalAndRemote() {
   local := connectLocal()
   remote := connectRemote()

   if local != nil && remote != nil {
      network.Join2Conn(local, remote)
   } else {
      if local != nil {
         _ = local.Close()
      }
      if remote != nil {
         _ = remote.Close()
      }
   }
}

func connectLocal() *net.TCPConn {
   conn, err := network.CreateTCPConn(localServerAddr)
   if err != nil {
      log.Println("[連接本地服務(wù)失敗]" + err.Error())
   }
   return conn
}

func connectRemote() *net.TCPConn {
   conn, err := network.CreateTCPConn(remoteServerAddr)
   if err != nil {
      log.Println("[連接遠(yuǎn)端服務(wù)失敗]" + err.Error())
   }
   return conn
}

服務(wù)端

服務(wù)端的實(shí)現(xiàn)就相對(duì)復(fù)雜一些了:

  • 監(jiān)聽控制通道,接收客戶端的連接請(qǐng)求
  • 監(jiān)聽訪問(wèn)端口,接收來(lái)自用戶的 http 請(qǐng)求
  • 第二步接收到請(qǐng)求之后需要存放一下這個(gè)連接并同時(shí)發(fā)消息給客戶端,告訴客戶端有用戶訪問(wèn)了,趕緊建立隧道進(jìn)行通信
  • 監(jiān)聽隧道通道,接收來(lái)自客戶端的連接請(qǐng)求,將客戶端的連接與用戶的連接建立起來(lái)(也是用工具方法)
package main

import (
   "log"
   "net"
   "strconv"
   "sync"
   "time"

   "nat-proxy/cmd/network"
)

const (
   controlAddr = "0.0.0.0:8009"
   tunnelAddr  = "0.0.0.0:8008"
   visitAddr   = "0.0.0.0:8007"
)

var (
   clientConn         *net.TCPConn
   connectionPool     map[string]*ConnMatch
   connectionPoolLock sync.Mutex
)

type ConnMatch struct {
   addTime time.Time
   accept  *net.TCPConn
}

func main() {
   connectionPool = make(map[string]*ConnMatch, 32)
   go createControlChannel()
   go acceptUserRequest()
   go acceptClientRequest()
   cleanConnectionPool()
}

// 創(chuàng)建一個(gè)控制通道,用于傳遞控制消息,如:心跳,創(chuàng)建新連接
func createControlChannel() {
   tcpListener, err := network.CreateTCPListener(controlAddr)
   if err != nil {
      panic(err)
   }

   log.Println("[已監(jiān)聽]" + controlAddr)
   for {
      tcpConn, err := tcpListener.AcceptTCP()
      if err != nil {
         log.Println(err)
         continue
      }

      log.Println("[新連接]" + tcpConn.RemoteAddr().String())
      // 如果當(dāng)前已經(jīng)有一個(gè)客戶端存在,則丟棄這個(gè)鏈接
      if clientConn != nil {
         _ = tcpConn.Close()
      } else {
         clientConn = tcpConn
         go keepAlive()
      }
   }
}

// 和客戶端保持一個(gè)心跳鏈接
func keepAlive() {
   go func() {
      for {
         if clientConn == nil {
            return
         }
         _, err := clientConn.Write(([]byte)(network.KeepAlive + "\n"))
         if err != nil {
            log.Println("[已斷開客戶端連接]", clientConn.RemoteAddr())
            clientConn = nil
            return
         }
         time.Sleep(time.Second * 3)
      }
   }()
}

// 監(jiān)聽來(lái)自用戶的請(qǐng)求
func acceptUserRequest() {
   tcpListener, err := network.CreateTCPListener(visitAddr)
   if err != nil {
      panic(err)
   }
   defer tcpListener.Close()
   for {
      tcpConn, err := tcpListener.AcceptTCP()
      if err != nil {
         continue
      }
      addConn2Pool(tcpConn)
      sendMessage(network.NewConnection + "\n")
   }
}

// 將用戶來(lái)的連接放入連接池中
func addConn2Pool(accept *net.TCPConn) {
   connectionPoolLock.Lock()
   defer connectionPoolLock.Unlock()

   now := time.Now()
   connectionPool[strconv.FormatInt(now.UnixNano(), 10)] = &ConnMatch{now, accept,}
}

// 發(fā)送給客戶端新消息
func sendMessage(message string) {
   if clientConn == nil {
      log.Println("[無(wú)已連接的客戶端]")
      return
   }
   _, err := clientConn.Write([]byte(message))
   if err != nil {
      log.Println("[發(fā)送消息異常]: message: ", message)
   }
}

// 接收客戶端來(lái)的請(qǐng)求并建立隧道
func acceptClientRequest() {
   tcpListener, err := network.CreateTCPListener(tunnelAddr)
   if err != nil {
      panic(err)
   }
   defer tcpListener.Close()

   for {
      tcpConn, err := tcpListener.AcceptTCP()
      if err != nil {
         continue
      }
      go establishTunnel(tcpConn)
   }
}

func establishTunnel(tunnel *net.TCPConn) {
   connectionPoolLock.Lock()
   defer connectionPoolLock.Unlock()

   for key, connMatch := range connectionPool {
      if connMatch.accept != nil {
         go network.Join2Conn(connMatch.accept, tunnel)
         delete(connectionPool, key)
         return
      }
   }

   _ = tunnel.Close()
}

func cleanConnectionPool() {
   for {
      connectionPoolLock.Lock()
      for key, connMatch := range connectionPool {
         if time.Now().Sub(connMatch.addTime) > time.Second*10 {
            _ = connMatch.accept.Close()
            delete(connectionPool, key)
         }
      }
      connectionPoolLock.Unlock()
      time.Sleep(5 * time.Second)
   }
}

其他

  • 其中我加入了 keepalive 的消息,用于保持客戶端與服務(wù)端的一直正常連接
  • 我們還需要定期清理一下服務(wù)端 map 中沒(méi)有建立成功的連接

實(shí)驗(yàn)一下

首先在本機(jī)用 dokcer 部署一個(gè) nginx 服務(wù)(你可以啟動(dòng)一個(gè) tomcat 都可以的),并修改客戶監(jiān)聽端口localServerAddr為127.0.0.1:32768,并修改remoteIP 為服務(wù)端 IP 地址。然后訪問(wèn)以下,看到是可以正常訪問(wèn)的。

然后編譯打包服務(wù)端扔到服務(wù)器上啟動(dòng)、客戶端本地啟動(dòng),如果控制臺(tái)輸出連接成功,就完成準(zhǔn)備了

現(xiàn)在通過(guò)訪問(wèn)服務(wù)端的 8007 端口就可以訪問(wèn)我們內(nèi)網(wǎng)的服務(wù)了。

遺留問(wèn)題

上述的實(shí)現(xiàn)是一個(gè)最小的實(shí)現(xiàn),也只是為了完成基本功能,還有一些遺留的問(wèn)題等待你的處理:

  • 現(xiàn)在一個(gè)客戶端連接上了就不能連接第二個(gè)了,那怎么做多個(gè)客戶端的連接呢?
  • 當(dāng)前這個(gè) map 的使用其實(shí)是有風(fēng)險(xiǎn)的,如何做好連接池的管理?
  • TCP 連接的開銷是很大的,如何做好連接的復(fù)用?
  • 當(dāng)前是 TCP 的連接,那么如果是 UDP 如何實(shí)現(xiàn)呢?
  • 當(dāng)前連接都是不加密的,如何進(jìn)行加密呢?
  • 當(dāng)前的 keepalive 實(shí)現(xiàn)很簡(jiǎn)單,有沒(méi)有更優(yōu)雅的實(shí)現(xiàn)方式呢?

這些就交給聰明的你來(lái)完成了

總結(jié)

其實(shí)最后回頭看看實(shí)現(xiàn)起來(lái)并不復(fù)雜,用 go 來(lái)實(shí)現(xiàn)已經(jīng)是非常簡(jiǎn)單了,所以 github 上面有很多利用 go 來(lái)實(shí)現(xiàn)代理或者穿透的工具,我也是參考它們抽離了其中的核心,最重要的就是工具方法中的第三個(gè) copy 了,不過(guò)其實(shí)還有很多細(xì)節(jié)點(diǎn)需要考慮的。

以上就是Golang實(shí)現(xiàn)內(nèi)網(wǎng)穿透詳解的詳細(xì)內(nèi)容,更多關(guān)于Go內(nèi)網(wǎng)穿透的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Golang新提案:panic?能不能加個(gè)?PanicError?

    Golang新提案:panic?能不能加個(gè)?PanicError?

    這篇文章主要為大家介紹了Golang的新提案關(guān)于panic能不能加個(gè)PanicError的問(wèn)題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的全過(guò)程

    Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的全過(guò)程

    Go語(yǔ)言是Google的開源編程語(yǔ)言,廣泛應(yīng)用于云計(jì)算、分布式系統(tǒng)開發(fā)等領(lǐng)域,在Linux上也有大量的應(yīng)用場(chǎng)景,這篇文章主要給大家介紹了關(guān)于Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的相關(guān)資料,需要的朋友可以參考下
    2023-11-11
  • golang判斷文本文件是否是BOM格式的方法詳解

    golang判斷文本文件是否是BOM格式的方法詳解

    在Go語(yǔ)言中,我們可以通過(guò)讀取文本文件的前幾個(gè)字節(jié)來(lái)識(shí)別它是否是BOM格式的文件,BOM(Byte Order Mark)是UTF編碼標(biāo)準(zhǔn)中的一部分,用于標(biāo)示文本文件的編碼順序,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-10-10
  • Go語(yǔ)言獲取系統(tǒng)性能數(shù)據(jù)gopsutil庫(kù)的操作

    Go語(yǔ)言獲取系統(tǒng)性能數(shù)據(jù)gopsutil庫(kù)的操作

    這篇文章主要介紹了Go語(yǔ)言獲取系統(tǒng)性能數(shù)據(jù)gopsutil庫(kù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-12-12
  • Go高級(jí)特性探究之優(yōu)先級(jí)隊(duì)列詳解

    Go高級(jí)特性探究之優(yōu)先級(jí)隊(duì)列詳解

    Heap?是一種數(shù)據(jù)結(jié)構(gòu),這種數(shù)據(jù)結(jié)構(gòu)常用于實(shí)現(xiàn)優(yōu)先隊(duì)列,這篇文章主要就是來(lái)和大家深入探討一下GO語(yǔ)言中的優(yōu)先級(jí)隊(duì)列,感興趣的可以了解一下
    2023-06-06
  • 我為什么喜歡Go語(yǔ)言(簡(jiǎn)潔的Go語(yǔ)言)

    我為什么喜歡Go語(yǔ)言(簡(jiǎn)潔的Go語(yǔ)言)

    從2000年至今,也寫了11年代碼了,期間用過(guò)VB、Delphi、C#、C++、Ruby、Python,一直在尋找一門符合自己心意和理念的語(yǔ)言。我很在意寫代碼時(shí)的手感和執(zhí)行的效率,所以在Go出現(xiàn)之前一直沒(méi)有找到
    2014-10-10
  • 一文吃透Go的內(nèi)置RPC原理

    一文吃透Go的內(nèi)置RPC原理

    這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中內(nèi)置RPC的原理。說(shuō)起?RPC?大家想到的一般是框架,Go?作為編程語(yǔ)言竟然還內(nèi)置了?RPC,著實(shí)讓我有些吃鯨,本文就來(lái)一起聊聊吧
    2023-03-03
  • Golang使用Gin框架實(shí)現(xiàn)HTTP響應(yīng)格式統(tǒng)一處理

    Golang使用Gin框架實(shí)現(xiàn)HTTP響應(yīng)格式統(tǒng)一處理

    在gin框架中,我們可以定義一個(gè)中間件來(lái)處理統(tǒng)一的HTTP響應(yīng)格式,本文主要為大家介紹了具體是怎么定義實(shí)現(xiàn)這樣的中間件的,感興趣的小伙伴可以了解一下
    2023-07-07
  • 對(duì)Golang import 導(dǎo)入包語(yǔ)法詳解

    對(duì)Golang import 導(dǎo)入包語(yǔ)法詳解

    今天小編就為大家分享一篇對(duì)Golang import 導(dǎo)入包語(yǔ)法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2019-06-06
  • 一文弄懂用Go實(shí)現(xiàn)MCP服務(wù)的示例代碼

    一文弄懂用Go實(shí)現(xiàn)MCP服務(wù)的示例代碼

    本文主要介紹了一文弄懂用Go實(shí)現(xiàn)MCP服務(wù)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2025-04-04

最新評(píng)論