1. 程式人生 > 程式設計 >Nodejs實現內網穿透服務

Nodejs實現內網穿透服務

也許你很難從網上找到一篇從程式碼層面講解內網穿透的文章,我曾搜過,未果,遂成此文。

1. 區域網內代理

我們先來回顧上篇,如何實現一個區域網內的服務代理?因為這個非常簡單,所以,直接上程式碼。

const net = require('net')

const proxy = net.createServer(socket => {
  const localServe = new net.Socket()
  localServe.connect(5502,'192.168.31.130') // 區域網內的服務埠及ip。

  socket.pipe(localServe).pipe(socket)
})

proxy.listen(80)

這就是一個非常簡單的服務端代理,程式碼簡單清晰明瞭,如果有疑問的話,估計就是管道(pipe)這裡,簡單說下。socket是一個全雙工流,也就是既可讀又可寫的資料流。程式碼中,當socket接收到客戶端資料的時http://www.cppcns.com候,它會把資料寫入localSever,當localSever有資料的時候,它會把資料寫入socket,socket再把資料傳送給客戶端。

2. 內網穿透

區域網代理簡單,內網穿透就沒這麼簡單了,但是,它卻是核心的程式碼,需要在其上做相當的邏輯處理。具體實現之前,我們先梳理一下內網穿透。

什麼是內網穿透?

簡單來說,就是公網客戶端,可以訪問區域網內的服務。比如,本地啟動的服務。公網客戶端怎麼會知道本地啟的serve呢?這裡必然要藉助公網服務端。那麼公網服務端又怎麼知道本地服務呢?這就需要本地和服務端建立socket連結了。

四個角色

通過上面的描述,我們引出四個角色。

  1. 公網客戶端,我們取名叫client。
  2. 公網服務端,因為有代理的作用,我們取名叫proxyServe。
  3. 本地服務,取名localServe。
  4. 本地與服務端的socket長連線,它是proxyServe與localServe之前的橋樑,負責資料的中轉,我們取名叫bridge。

其中,client和localServe不需要我們關心,因為client可以是瀏覽器或者其它,localServe就是一個普通的本地服務。我們只需要關心proxyServe和bridge就可以了。我們這裡介紹的依然是最簡單的實現方式,提供一種思路與思考,那我們先從最簡單的開始。

bridge

我們從四個角色一節知道, bridge是一個與proxyServe之間socket連線,且是資料的中轉,上程式碼捋捋思路。

const net = require('net')

const proxyServe = '10.253.107.245'

const bridge = new net.Socket()
bridge.connect(80,proxyServe,_ => {
  bridge.write('GET /regester?key=sq HTTP/1.1\r\n\r\n')
})

bridge.on('data',data => {
  const localServer = new net.Socket()
  localServer.connect(8088,'localhost',_ => {
    localServer.write(data)
    localServer.on('data',res => bridge.write(res))
  })
})

程式碼清晰可讀,甚至朗朗上口。引入net庫,宣告公網地址,建立bridge,使bridge連線proxyServe,成功之後,向proxyServe註冊本地服務,接著,bridge監聽資料,有請求到達時,建立與本地服務的連線,成功之後,把請求資料傳送給localServe,同時監聽響應資料,把響應流寫入到bridge。

其餘沒什麼好解釋的了,畢竟這只是示例程式碼。不過示例程式碼中有段/regester?key=sq,這個key可是有大作用的,在這裡key=sq。那麼角色client通過代理服務訪問本地服務的是,需要在路徑上加上這個key,proxyServe才能對應的上bridge,從而對應上localServe。

例如:lcoalServe是:http://localhost:8088 ,rpoxyServe是example.com,註冊的key是sq。那麼要想通過prxoyServe訪問到localServe,需要如下寫法:example.com/sq 。為什麼要這樣寫?當然只是一個定義而已,你讀懂這篇文章的程式碼之後,可以修改這樣的約定。

那麼,且看以下關鍵程式碼:

proxyServe

這裡的proxyServe雖然是一個簡化後的示例程式碼,講起來依然有些複雜,要想徹底弄懂,並結合自己的業務做成可用程式碼,是要下一番功夫的。這裡我把程式碼拆分成一塊一塊,試著把它講明白,我們給程式碼塊取個名字,方便講解。
程式碼塊一:createServe

該塊的主要功能是建立代理服務,與client和bridge建立socket連結,socket監聽資料請求,在回撥函式裡做邏輯處理,具體程式碼如下:

const net = require('net')

const brid程式設計客棧ges = {} // 當有bridge建立socket連線時,快取在這裡
const clients = {} // 當有client建立socket連線時,快取在這裡,具體資料結構看原始碼

net.createServer(socket => {
  socket.on('data',data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url
    
    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket,url)
      return
    }

    const { bridge,key } = findBridge(request,url)
    if (!bridge) return

    cacheClientRequest(bridge,key,socket,request,url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

看一下資料監聽裡的程式碼邏輯:

  1. 把請求資料轉換成字串。
  2. 從請求裡查詢URL,找不到URL直接結束本次請求。
  3. 通過URL判斷是不是bridge,如果是,註冊這個bridge,否者,認為是一個client請求。
  4. 檢視client請求有沒有已經註冊過的bridge -- 記住,這是一個代理服務,沒有已經註冊的bridge,就認為請求無效。
  5. 快取這次請求。
  6. 接著再把請求傳送給bridge。

結合程式碼及邏輯梳理,應該能看得懂,但是,對5或許有疑問,接下程式設計客棧來一一梳理。

程式碼塊二:isBridge

判斷是不是一個bridge的註冊請求,這裡寫的很簡單,不過,真實業務,或許可以定義更加確切的資料。

function isBridge (url) {
  return url.startsWith('/regester?')
}

程式碼塊三:regesterBridge
簡單,看程式碼再說明:

function regesterBridge (socket,url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}
  1. 通過URL查詢要註冊的bridge的key。
  2. 把改socket連線快取起來。
  3. 移除bridge的資料監聽 -- 程式碼塊一里每個socket都有預設的資料監聽回撥函說,如果不移除,會導致後續資料混亂。

程式碼塊四:findBridge

邏輯走到程式碼塊4的時候,說明這已經是一個client請求了,那麼,需要先找到它對應的bridge,沒有bridge,就需要先註冊bridge,然後需要使用者稍後再發起client請求。程式碼如下:

function findBridge (request,url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge,key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge,key }

  return {}
}

  • 從URL中匹配出要代理的bridge的key,找到就返回對應的bridge及key。
  • 找不到再從請求頭裡的referer裡找,找到就返回bridge及key。
  • 都找不到,我們知道在程式碼塊一里會結束掉本次請求。

程式碼塊五:cacheClientRequest

程式碼執行到這裡,說明已經是一個client請求了,我們先把這個請求快取起來,快取的時候,我們一併把請求對應的bridge、key繫結一起快取,方便後續操作。

為什麼要快取client請求?

在目前的方案裡,我們希望請求和響應都是成對有序的。我們知道網路傳輸都是分片傳輸的,目前來看,如果我們不在應用層控制請求和響應成對且有序,會導致資料包之間的混亂現象。暫且這樣,後續如果有更好方案,可以不在應用層強制控制資料的請求響應有序,可以信賴tcp/ip層。
講完原因,我們先來看快取程式碼,這裡比較簡單,複雜的在於逐個取出請求並有序返回整個響應。

function cacheClientRequest (bridge,url) {
  if (clients[key]) {
    clients[key].requests.push({bridge,url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge,url}]
  }
}

我們先判斷該bridge對應的key下是不是已經有client的請求快取了,如果有,就push進去。

如果沒有,我們就建立一個物件,把本次請求初始化進去。

接下來就是最複雜的,取出請求快取,傳送給bridge,監聽bridge的響應,直到本次響應結束,在刪除bridge的資料監聽,再試著取出下一個請求,重複上面的動作,直到處理完client的所有請求。

程式碼塊六:sendRequestToBridgeByKey

在程式碼塊五的最後,對該塊做了概括性的說明。可以先稍作理解,在看下面程式碼,因為程式碼裡會有一些響應完整性的判斷,去除這一些,程式碼就好理解一些。整個方案,我們沒有對請求完整性進行處理,原因是,一個請求的基本都在一份資料包大小內,除非是檔案上傳介面,我們暫不處理,不然,程式碼又會複雜一些。

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge,url} = requests.shift()

  const newUrl = url.replace(key,'')
  const newRequest = request.replace(url,newUrl)

  bridge.write(newRequest)
  bridge.on('data',data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*程式設計客棧\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

從clients裡取出bridge key對應的client。
判斷該client是不是有請求正在傳送,如果有,結束執行。如果沒有,繼續。
判斷該client下是否有請求,如果有,繼續,沒有,結束執行。
從佇列中取出第一個,它包含請求的socket及快取的bridge。
替換掉約定的資料,把最終的請求資料傳送給bridge。
監聽bridge的資料響應。

  • 獲取響應code
    • 如果響應是200,我們從中獲取content length,如果有,我們對本次請求做一些初始化的操作。設定請求長度,設定已經發送的請求長度。
    • 如果不是200,我們把資料傳送給client,並且結束本次請求,移除本次資料監聽,遞迴呼叫sendRequestToBridgeByKey
  • 如果沒有獲取的code,我們認為本次響應非第一次,於是,把其長度累加到已傳送欄位上。
  • 我們接著傳送該資料到client。
  • 再判斷響應的長度是否和已經發送的過的資料長度一致,如果一致,設定client的資料傳送狀態為false,移除資料監聽,遞迴呼叫遞迴呼叫sendRequestToBridgeByKey。

至此,核心程式碼邏輯已經全部結束。

總結

理解這套程式碼之後,就可以在其上做擴充套件,豐富程式碼,為你所用。理解完這套程式碼,你能想到,它還有哪些使用場景嗎?是不是這個思路也可以用在遠端控制上,如果你要控制客戶端時,從這段程式碼找找,是不是會有靈感。
這套程式碼或許會有難點,可能要對tcp/ip所有了解,也需要對http有所瞭解,並且知道一些關鍵的請求頭,知道一些關鍵的響應資訊,當然,對於http瞭解的越多越好。
如果有什麼需要交流,歡程式設計客棧迎留言。

proxyServe原始碼

const net = require('net')

const bridges = {}
const clients = {}

net.createServer(socket => {
  socket.on('data',url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

function isBridge (url) {
  return url.startsWith('/regester?')
}

function regesterBridge (socket,url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}

function findBridge (request,key }

  return {}
}

function cacheClientRequest (bridge,url}]
  }
}

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge,data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

到此這篇關於Nodejs實現內網穿透服務的文章就介紹到這了,更多相關Node 內網穿透內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!