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接收到客戶端數據的時候,它會把數據寫入localSever,當localSever有數據的時候,它會把數據寫入socket,socket再把數據發送給客戶端。
2. 內網穿透
局域網代理簡單,內網穿透就沒這麼簡單瞭,但是,它卻是核心的代碼,需要在其上做相當的邏輯處理。具體實現之前,我們先梳理一下內網穿透。
什麼是內網穿透?
簡單來說,就是公網客戶端,可以訪問局域網內的服務。比如,本地啟動的服務。公網客戶端怎麼會知道本地啟的serve呢?這裡必然要借助公網服務端。那麼公網服務端又怎麼知道本地服務呢?這就需要本地和服務端建立socket鏈接瞭。
四個角色
通過上面的描述,我們引出四個角色。
- 公網客戶端,我們取名叫client。
- 公網服務端,因為有代理的作用,我們取名叫proxyServe。
- 本地服務,取名localServe。
- 本地與服務端的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 bridges = {} // 當有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)
看一下數據監聽裡的代碼邏輯:
- 把請求數據轉換成字符串。
- 從請求裡查找URL,找不到URL直接結束本次請求。
- 通過URL判斷是不是bridge,如果是,註冊這個bridge,否者,認為是一個client請求。
- 查看client請求有沒有已經註冊過的bridge — 記住,這是一個代理服務,沒有已經註冊的bridge,就認為請求無效。
- 緩存這次請求。
- 接著再把請求發送給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') }
- 通過URL查找要註冊的bridge的key。
- 把改socket連接緩存起來。
- 移除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, 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對應的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, 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對應的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', 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) } }) }
到此這篇關於Nodejs實現內網穿透服務的文章就介紹到這瞭,更多相關Node 內網穿透內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!