最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Nodejs實現(xiàn)內(nèi)網(wǎng)穿透服務(wù)

 更新時間:2021年05月20日 11:46:25   作者:我們來掘金吧  
很多人都不知道什么是內(nèi)網(wǎng)穿透,就是公網(wǎng)客戶端,可以訪問局域網(wǎng)內(nèi)的服務(wù),本文詳細(xì)的介紹了原理以及實現(xiàn),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

也許你很難從網(wǎng)上找到一篇從代碼層面講解內(nèi)網(wǎng)穿透的文章,我曾搜過,未果,遂成此文。

1. 局域網(wǎng)內(nèi)代理

我們先來回顧上篇,如何實現(xiàn)一個局域網(wǎng)內(nèi)的服務(wù)代理?因為這個非常簡單,所以,直接上代碼。

const net = require('net')

const proxy = net.createServer(socket => {
  const localServe = new net.Socket()
  localServe.connect(5502, '192.168.31.130') // 局域網(wǎng)內(nèi)的服務(wù)端口及ip。

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

proxy.listen(80)

這就是一個非常簡單的服務(wù)端代理,代碼簡單清晰明了,如果有疑問的話,估計就是管道(pipe)這里,簡單說下。socket是一個全雙工流,也就是既可讀又可寫的數(shù)據(jù)流。代碼中,當(dāng)socket接收到客戶端數(shù)據(jù)的時候,它會把數(shù)據(jù)寫入localSever,當(dāng)localSever有數(shù)據(jù)的時候,它會把數(shù)據(jù)寫入socket,socket再把數(shù)據(jù)發(fā)送給客戶端。

2. 內(nèi)網(wǎng)穿透

局域網(wǎng)代理簡單,內(nèi)網(wǎng)穿透就沒這么簡單了,但是,它卻是核心的代碼,需要在其上做相當(dāng)?shù)倪壿嬏幚?。具體實現(xiàn)之前,我們先梳理一下內(nèi)網(wǎng)穿透。

什么是內(nèi)網(wǎng)穿透?

簡單來說,就是公網(wǎng)客戶端,可以訪問局域網(wǎng)內(nèi)的服務(wù)。比如,本地啟動的服務(wù)。公網(wǎng)客戶端怎么會知道本地啟的serve呢?這里必然要借助公網(wǎng)服務(wù)端。那么公網(wǎng)服務(wù)端又怎么知道本地服務(wù)呢?這就需要本地和服務(wù)端建立socket鏈接了。

四個角色

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

  1. 公網(wǎng)客戶端,我們?nèi)∶衏lient。
  2. 公網(wǎng)服務(wù)端,因為有代理的作用,我們?nèi)∶衟roxyServe。
  3. 本地服務(wù),取名localServe。
  4. 本地與服務(wù)端的socket長連接,它是proxyServe與localServe之前的橋梁,負(fù)責(zé)數(shù)據(jù)的中轉(zhuǎn),我們?nèi)∶衎ridge。

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

bridge

我們從四個角色一節(jié)知道, bridge是一個與proxyServe之間socket連接,且是數(shù)據(jù)的中轉(zhuǎn),上代碼捋捋思路。

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庫,聲明公網(wǎng)地址,創(chuàng)建bridge,使bridge連接proxyServe,成功之后,向proxyServe注冊本地服務(wù),接著,bridge監(jiān)聽數(shù)據(jù),有請求到達(dá)時,創(chuàng)建與本地服務(wù)的連接,成功之后,把請求數(shù)據(jù)發(fā)送給localServe,同時監(jiān)聽響應(yīng)數(shù)據(jù),把響應(yīng)流寫入到bridge。

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

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

那么,且看以下關(guān)鍵代碼:

proxyServe

這里的proxyServe雖然是一個簡化后的示例代碼,講起來依然有些復(fù)雜,要想徹底弄懂,并結(jié)合自己的業(yè)務(wù)做成可用代碼,是要下一番功夫的。這里我把代碼拆分成一塊一塊,試著把它講明白,我們給代碼塊取個名字,方便講解。
代碼塊一:createServe

該塊的主要功能是創(chuàng)建代理服務(wù),與client和bridge建立socket鏈接,socket監(jiān)聽數(shù)據(jù)請求,在回調(diào)函數(shù)里做邏輯處理,具體代碼如下:

const net = require('net')

const bridges = {} // 當(dāng)有bridge建立socket連接時,緩存在這里
const clients = {} // 當(dāng)有client建立socket連接時,緩存在這里,具體數(shù)據(jù)結(jié)構(gòu)看源代碼

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)

看一下數(shù)據(jù)監(jiān)聽里的代碼邏輯:

  1. 把請求數(shù)據(jù)轉(zhuǎn)換成字符串。
  2. 從請求里查找URL,找不到URL直接結(jié)束本次請求。
  3. 通過URL判斷是不是bridge,如果是,注冊這個bridge,否者,認(rèn)為是一個client請求。
  4. 查看client請求有沒有已經(jīng)注冊過的bridge -- 記住,這是一個代理服務(wù),沒有已經(jīng)注冊的bridge,就認(rèn)為請求無效。
  5. 緩存這次請求。
  6. 接著再把請求發(fā)送給bridge。

結(jié)合代碼及邏輯梳理,應(yīng)該能看得懂,但是,對5或許有疑問,接下來一一梳理。

代碼塊二:isBridge

判斷是不是一個bridge的注冊請求,這里寫的很簡單,不過,真實業(yè)務(wù),或許可以定義更加確切的數(shù)據(jù)。

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的數(shù)據(jù)監(jiān)聽 -- 代碼塊一里每個socket都有默認(rèn)的數(shù)據(jù)監(jiān)聽回調(diào)函說,如果不移除,會導(dǎo)致后續(xù)數(shù)據(jù)混亂。

代碼塊四:findBridge

邏輯走到代碼塊4的時候,說明這已經(jīng)是一個client請求了,那么,需要先找到它對應(yīng)的bridge,沒有bridge,就需要先注冊bridge,然后需要用戶稍后再發(fā)起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,找到就返回對應(yīng)的bridge及key。
  • 找不到再從請求頭里的referer里找,找到就返回bridge及key。
  • 都找不到,我們知道在代碼塊一里會結(jié)束掉本次請求。

代碼塊五:cacheClientRequest

代碼執(zhí)行到這里,說明已經(jīng)是一個client請求了,我們先把這個請求緩存起來,緩存的時候,我們一并把請求對應(yīng)的bridge、key綁定一起緩存,方便后續(xù)操作。

為什么要緩存client請求?

在目前的方案里,我們希望請求和響應(yīng)都是成對有序的。我們知道網(wǎng)絡(luò)傳輸都是分片傳輸?shù)?,目前來看,如果我們不在?yīng)用層控制請求和響應(yīng)成對且有序,會導(dǎo)致數(shù)據(jù)包之間的混亂現(xiàn)象。暫且這樣,后續(xù)如果有更好方案,可以不在應(yīng)用層強制控制數(shù)據(jù)的請求響應(yīng)有序,可以信賴tcp/ip層。
講完原因,我們先來看緩存代碼,這里比較簡單,復(fù)雜的在于逐個取出請求并有序返回整個響應(yīng)。

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

我們先判斷該bridge對應(yīng)的key下是不是已經(jīng)有client的請求緩存了,如果有,就push進(jìn)去。

如果沒有,我們就創(chuàng)建一個對象,把本次請求初始化進(jìn)去。

接下來就是最復(fù)雜的,取出請求緩存,發(fā)送給bridge,監(jiān)聽bridge的響應(yīng),直到本次響應(yīng)結(jié)束,在刪除bridge的數(shù)據(jù)監(jiān)聽,再試著取出下一個請求,重復(fù)上面的動作,直到處理完client的所有請求。

代碼塊六:sendRequestToBridgeByKey

在代碼塊五的最后,對該塊做了概括性的說明??梢韵壬宰骼斫?,在看下面代碼,因為代碼里會有一些響應(yīng)完整性的判斷,去除這一些,代碼就好理解一些。整個方案,我們沒有對請求完整性進(jìn)行處理,原因是,一個請求的基本都在一份數(shù)據(jù)包大小內(nèi),除非是文件上傳接口,我們暫不處理,不然,代碼又會復(fù)雜一些。

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, socket, request, 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對應(yīng)的client。
判斷該client是不是有請求正在發(fā)送,如果有,結(jié)束執(zhí)行。如果沒有,繼續(xù)。
判斷該client下是否有請求,如果有,繼續(xù),沒有,結(jié)束執(zhí)行。
從隊列中取出第一個,它包含請求的socket及緩存的bridge。
替換掉約定的數(shù)據(jù),把最終的請求數(shù)據(jù)發(fā)送給bridge。
監(jiān)聽bridge的數(shù)據(jù)響應(yīng)。

  • 獲取響應(yīng)code
    • 如果響應(yīng)是200,我們從中獲取content length,如果有,我們對本次請求做一些初始化的操作。設(shè)置請求長度,設(shè)置已經(jīng)發(fā)送的請求長度。
    • 如果不是200,我們把數(shù)據(jù)發(fā)送給client,并且結(jié)束本次請求,移除本次數(shù)據(jù)監(jiān)聽,遞歸調(diào)用sendRequestToBridgeByKey
  • 如果沒有獲取的code,我們認(rèn)為本次響應(yīng)非第一次,于是,把其長度累加到已發(fā)送字段上。
  • 我們接著發(fā)送該數(shù)據(jù)到client。
  • 再判斷響應(yīng)的長度是否和已經(jīng)發(fā)送的過的數(shù)據(jù)長度一致,如果一致,設(shè)置client的數(shù)據(jù)發(fā)送狀態(tài)為false,移除數(shù)據(jù)監(jiān)聽,遞歸調(diào)用遞歸調(diào)用sendRequestToBridgeByKey。

至此,核心代碼邏輯已經(jīng)全部結(jié)束。

總結(jié)

理解這套代碼之后,就可以在其上做擴展,豐富代碼,為你所用。理解完這套代碼,你能想到,它還有哪些使用場景嗎?是不是這個思路也可以用在遠(yuǎn)程控制上,如果你要控制客戶端時,從這段代碼找找,是不是會有靈感。
這套代碼或許會有難點,可能要對tcp/ip所有了解,也需要對http有所了解,并且知道一些關(guān)鍵的請求頭,知道一些關(guān)鍵的響應(yīng)信息,當(dāng)然,對于http了解的越多越好。
如果有什么需要交流,歡迎留言。

proxyServe源碼

const net = require('net')

const bridges = {}
const clients = {}

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)

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, 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 {}
}

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, 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, socket, request, 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)
    }
  })
}

到此這篇關(guān)于Nodejs實現(xiàn)內(nèi)網(wǎng)穿透服務(wù)的文章就介紹到這了,更多相關(guān)Node 內(nèi)網(wǎng)穿透內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Node模塊化開發(fā)實例解析

    Node模塊化開發(fā)實例解析

    這篇文章主要為大家介紹了Node模塊化開發(fā)實例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • Node.js腳本提取OPML文件信息實現(xiàn)示例詳解

    Node.js腳本提取OPML文件信息實現(xiàn)示例詳解

    這篇文章主要為大家介紹了Node.js腳本提取OPML文件信息,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • node.js使用require()函數(shù)加載模塊

    node.js使用require()函數(shù)加載模塊

    本文通過幾個具體實例來分析node.js中使用require()函數(shù)來加載模塊的方法。非常的詳盡,給需要的小伙伴們參考下吧
    2014-11-11
  • node+js搭建時間服務(wù)器的思路詳解

    node+js搭建時間服務(wù)器的思路詳解

    這篇文章主要介紹了node+js搭建時間服務(wù)器,通過本文的學(xué)習(xí)可以了解node的fs模塊怎么讀取數(shù)據(jù)及express怎么搭建服務(wù)器,設(shè)置數(shù)據(jù)接口的,需要的朋友可以參考下
    2022-07-07
  • nodejs遞歸文件夾獲取所有文件路徑實例

    nodejs遞歸文件夾獲取所有文件路徑實例

    這篇文章主要為大家介紹了nodejs遞歸文件夾獲取所有文件路徑實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • 深入理解Puppeteer的入門教程和實踐

    深入理解Puppeteer的入門教程和實踐

    這篇文章主要介紹了深入理解Puppeteer的入門教程和實踐,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • 整理幾個關(guān)鍵節(jié)點深入理解nodejs

    整理幾個關(guān)鍵節(jié)點深入理解nodejs

    這篇文章主要介紹了整理幾個關(guān)鍵節(jié)點深入理解nodejs,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,需要的小伙伴可以參考一下,需要的小伙伴可以參考一下
    2022-07-07
  • Node.js中的package.json與cnpm命令行工具介紹

    Node.js中的package.json與cnpm命令行工具介紹

    這篇文章介紹了Node.js中的package.json與cnpm命令行工具,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-06-06
  • nodejs動態(tài)創(chuàng)建二維碼的方法

    nodejs動態(tài)創(chuàng)建二維碼的方法

    這篇文章主要為大家詳細(xì)介紹了nodejs動態(tài)創(chuàng)建二維碼的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-08-08
  • nodejs socket實現(xiàn)的服務(wù)端和客戶端功能示例

    nodejs socket實現(xiàn)的服務(wù)端和客戶端功能示例

    這篇文章主要介紹了nodejs socket實現(xiàn)的服務(wù)端和客戶端功能,結(jié)合具體實例形式分析了nodejs基于socket通信實現(xiàn)的服務(wù)端與客戶端功能相關(guān)操作技巧,需要的朋友可以參考下
    2017-06-06

最新評論

金沙县| 武宣县| 昭觉县| 灵丘县| 昂仁县| 明溪县| 当雄县| 叶城县| 松原市| 临夏县| 澎湖县| 丰都县| 松江区| 南郑县| 酉阳| 五峰| 金昌市| 涞源县| 丰镇市| 忻州市| 贡嘎县| 长治县| 宝丰县| 深圳市| 九台市| 木兰县| 临泉县| 宣武区| 明溪县| 蚌埠市| 武穴市| 青海省| 涡阳县| 临海市| 汪清县| 隆子县| 札达县| 禄丰县| 大邑县| 沽源县| 广河县|