您好,登錄后才能下訂單哦!
這篇文章給大家介紹使用Nodejs怎么實(shí)現(xiàn)內(nèi)網(wǎng)穿透服務(wù),內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
我們先來(lái)回顧上篇,如何實(shí)現(xiàn)一個(gè)局域網(wǎng)內(nèi)的服務(wù)代理?因?yàn)檫@個(gè)非常簡(jiǎn)單,所以,直接上代碼。
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)
這就是一個(gè)非常簡(jiǎn)單的服務(wù)端代理,代碼簡(jiǎn)單清晰明了,如果有疑問(wèn)的話,估計(jì)就是管道(pipe)這里,簡(jiǎn)單說(shuō)下。socket是一個(gè)全雙工流,也就是既可讀又可寫的數(shù)據(jù)流。代碼中,當(dāng)socket接收到客戶端數(shù)據(jù)的時(shí)候,它會(huì)把數(shù)據(jù)寫入localSever,當(dāng)localSever有數(shù)據(jù)的時(shí)候,它會(huì)把數(shù)據(jù)寫入socket,socket再把數(shù)據(jù)發(fā)送給客戶端。
局域網(wǎng)代理簡(jiǎn)單,內(nèi)網(wǎng)穿透就沒(méi)這么簡(jiǎn)單了,但是,它卻是核心的代碼,需要在其上做相當(dāng)?shù)倪壿嬏幚?。具體實(shí)現(xiàn)之前,我們先梳理一下內(nèi)網(wǎng)穿透。
簡(jiǎn)單來(lái)說(shuō),就是公網(wǎng)客戶端,可以訪問(wèn)局域網(wǎng)內(nèi)的服務(wù)。比如,本地啟動(dòng)的服務(wù)。公網(wǎng)客戶端怎么會(huì)知道本地啟的serve呢?這里必然要借助公網(wǎng)服務(wù)端。那么公網(wǎng)服務(wù)端又怎么知道本地服務(wù)呢?這就需要本地和服務(wù)端建立socket鏈接了。
四個(gè)角色
通過(guò)上面的描述,我們引出四個(gè)角色。
公網(wǎng)客戶端,我們?nèi)∶衏lient。
公網(wǎng)服務(wù)端,因?yàn)橛写淼淖饔?,我們?nèi)∶衟roxyServe。
本地服務(wù),取名localServe。
本地與服務(wù)端的socket長(zhǎng)連接,它是proxyServe與localServe之前的橋梁,負(fù)責(zé)數(shù)據(jù)的中轉(zhuǎn),我們?nèi)∶衎ridge。
其中,client和localServe不需要我們關(guān)心,因?yàn)閏lient可以是瀏覽器或者其它,localServe就是一個(gè)普通的本地服務(wù)。我們只需要關(guān)心proxyServe和bridge就可以了。我們這里介紹的依然是最簡(jiǎn)單的實(shí)現(xiàn)方式,提供一種思路與思考,那我們先從最簡(jiǎn)單的開(kāi)始。
我們從四個(gè)角色一節(jié)知道, bridge是一個(gè)與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庫(kù),聲明公網(wǎng)地址,創(chuàng)建bridge,使bridge連接proxyServe,成功之后,向proxyServe注冊(cè)本地服務(wù),接著,bridge監(jiān)聽(tīng)數(shù)據(jù),有請(qǐng)求到達(dá)時(shí),創(chuàng)建與本地服務(wù)的連接,成功之后,把請(qǐng)求數(shù)據(jù)發(fā)送給localServe,同時(shí)監(jiān)聽(tīng)響應(yīng)數(shù)據(jù),把響應(yīng)流寫入到bridge。
其余沒(méi)什么好解釋的了,畢竟這只是示例代碼。不過(guò)示例代碼中有段/regester?key=sq,這個(gè)key可是有大作用的,在這里key=sq。那么角色client通過(guò)代理服務(wù)訪問(wèn)本地服務(wù)的是,需要在路徑上加上這個(gè)key,proxyServe才能對(duì)應(yīng)的上bridge,從而對(duì)應(yīng)上localServe。
例如:lcoalServe是:http://localhost:8088 ,rpoxyServe是example.com ,注冊(cè)的key是sq。那么要想通過(guò)prxoyServe訪問(wèn)到localServe,需要如下寫法:example.com/sq 。為什么要這樣寫?當(dāng)然只是一個(gè)定義而已,你讀懂這篇文章的代碼之后,可以修改這樣的約定。
那么,且看以下關(guān)鍵代碼:
這里的proxyServe雖然是一個(gè)簡(jiǎn)化后的示例代碼,講起來(lái)依然有些復(fù)雜,要想徹底弄懂,并結(jié)合自己的業(yè)務(wù)做成可用代碼,是要下一番功夫的。這里我把代碼拆分成一塊一塊,試著把它講明白,我們給代碼塊取個(gè)名字,方便講解。
代碼塊一:createServe
該塊的主要功能是創(chuàng)建代理服務(wù),與client和bridge建立socket鏈接,socket監(jiān)聽(tīng)數(shù)據(jù)請(qǐng)求,在回調(diào)函數(shù)里做邏輯處理,具體代碼如下:
const net = require('net') const bridges = {} // 當(dāng)有bridge建立socket連接時(shí),緩存在這里 const clients = {} // 當(dāng)有client建立socket連接時(shí),緩存在這里,具體數(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)聽(tīng)里的代碼邏輯:
把請(qǐng)求數(shù)據(jù)轉(zhuǎn)換成字符串。
從請(qǐng)求里查找URL,找不到URL直接結(jié)束本次請(qǐng)求。
通過(guò)URL判斷是不是bridge,如果是,注冊(cè)這個(gè)bridge,否者,認(rèn)為是一個(gè)client請(qǐng)求。
查看client請(qǐng)求有沒(méi)有已經(jīng)注冊(cè)過(guò)的bridge -- 記住,這是一個(gè)代理服務(wù),沒(méi)有已經(jīng)注冊(cè)的bridge,就認(rèn)為請(qǐng)求無(wú)效。
緩存這次請(qǐng)求。
接著再把請(qǐng)求發(fā)送給bridge。
結(jié)合代碼及邏輯梳理,應(yīng)該能看得懂,但是,對(duì)5或許有疑問(wèn),接下來(lái)一一梳理。
代碼塊二:isBridge
判斷是不是一個(gè)bridge的注冊(cè)請(qǐng)求,這里寫的很簡(jiǎn)單,不過(guò),真實(shí)業(yè)務(wù),或許可以定義更加確切的數(shù)據(jù)。
function isBridge (url) { return url.startsWith('/regester?') }
代碼塊三:regesterBridge
簡(jiǎn)單,看代碼再說(shuō)明:
function regesterBridge (socket, url) { const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key bridges[key] = socket socket.removeAllListeners('data') }
通過(guò)URL查找要注冊(cè)的bridge的key。
把改socket連接緩存起來(lái)。
移除bridge的數(shù)據(jù)監(jiān)聽(tīng) -- 代碼塊一里每個(gè)socket都有默認(rèn)的數(shù)據(jù)監(jiān)聽(tīng)回調(diào)函說(shuō),如果不移除,會(huì)導(dǎo)致后續(xù)數(shù)據(jù)混亂。
代碼塊四:findBridge
邏輯走到代碼塊4的時(shí)候,說(shuō)明這已經(jīng)是一個(gè)client請(qǐng)求了,那么,需要先找到它對(duì)應(yīng)的bridge,沒(méi)有bridge,就需要先注冊(cè)bridge,然后需要用戶稍后再發(fā)起client請(qǐng)求。代碼如下:
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,找到就返回對(duì)應(yīng)的bridge及key。
找不到再?gòu)恼?qǐng)求頭里的referer里找,找到就返回bridge及key。
都找不到,我們知道在代碼塊一里會(huì)結(jié)束掉本次請(qǐng)求。
代碼塊五:cacheClientRequest
代碼執(zhí)行到這里,說(shuō)明已經(jīng)是一個(gè)client請(qǐng)求了,我們先把這個(gè)請(qǐng)求緩存起來(lái),緩存的時(shí)候,我們一并把請(qǐng)求對(duì)應(yīng)的bridge、key綁定一起緩存,方便后續(xù)操作。
為什么要緩存client請(qǐng)求?
在目前的方案里,我們希望請(qǐng)求和響應(yīng)都是成對(duì)有序的。我們知道網(wǎng)絡(luò)傳輸都是分片傳輸?shù)?,目前?lái)看,如果我們不在應(yīng)用層控制請(qǐng)求和響應(yīng)成對(duì)且有序,會(huì)導(dǎo)致數(shù)據(jù)包之間的混亂現(xiàn)象。暫且這樣,后續(xù)如果有更好方案,可以不在應(yīng)用層強(qiáng)制控制數(shù)據(jù)的請(qǐng)求響應(yīng)有序,可以信賴tcp/ip層。
講完原因,我們先來(lái)看緩存代碼,這里比較簡(jiǎn)單,復(fù)雜的在于逐個(gè)取出請(qǐng)求并有序返回整個(gè)響應(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對(duì)應(yīng)的key下是不是已經(jīng)有client的請(qǐng)求緩存了,如果有,就push進(jìn)去。
如果沒(méi)有,我們就創(chuàng)建一個(gè)對(duì)象,把本次請(qǐng)求初始化進(jìn)去。
接下來(lái)就是最復(fù)雜的,取出請(qǐng)求緩存,發(fā)送給bridge,監(jiān)聽(tīng)bridge的響應(yīng),直到本次響應(yīng)結(jié)束,在刪除bridge的數(shù)據(jù)監(jiān)聽(tīng),再試著取出下一個(gè)請(qǐng)求,重復(fù)上面的動(dòng)作,直到處理完client的所有請(qǐng)求。
代碼塊六:sendRequestToBridgeByKey
在代碼塊五的最后,對(duì)該塊做了概括性的說(shuō)明。可以先稍作理解,在看下面代碼,因?yàn)榇a里會(huì)有一些響應(yīng)完整性的判斷,去除這一些,代碼就好理解一些。整個(gè)方案,我們沒(méi)有對(duì)請(qǐng)求完整性進(jìn)行處理,原因是,一個(gè)請(qǐng)求的基本都在一份數(shù)據(jù)包大小內(nèi),除非是文件上傳接口,我們暫不處理,不然,代碼又會(huì)復(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對(duì)應(yīng)的client。
判斷該client是不是有請(qǐng)求正在發(fā)送,如果有,結(jié)束執(zhí)行。如果沒(méi)有,繼續(xù)。
判斷該client下是否有請(qǐng)求,如果有,繼續(xù),沒(méi)有,結(jié)束執(zhí)行。
從隊(duì)列中取出第一個(gè),它包含請(qǐng)求的socket及緩存的bridge。
替換掉約定的數(shù)據(jù),把最終的請(qǐng)求數(shù)據(jù)發(fā)送給bridge。
監(jiān)聽(tīng)bridge的數(shù)據(jù)響應(yīng)。
獲取響應(yīng)code
如果響應(yīng)是200,我們從中獲取content length,如果有,我們對(duì)本次請(qǐng)求做一些初始化的操作。設(shè)置請(qǐng)求長(zhǎng)度,設(shè)置已經(jīng)發(fā)送的請(qǐng)求長(zhǎng)度。
如果不是200,我們把數(shù)據(jù)發(fā)送給client,并且結(jié)束本次請(qǐng)求,移除本次數(shù)據(jù)監(jiān)聽(tīng),遞歸調(diào)用sendRequestToBridgeByKey
如果沒(méi)有獲取的code,我們認(rèn)為本次響應(yīng)非第一次,于是,把其長(zhǎng)度累加到已發(fā)送字段上。
我們接著發(fā)送該數(shù)據(jù)到client。
再判斷響應(yīng)的長(zhǎng)度是否和已經(jīng)發(fā)送的過(guò)的數(shù)據(jù)長(zhǎng)度一致,如果一致,設(shè)置client的數(shù)據(jù)發(fā)送狀態(tài)為false,移除數(shù)據(jù)監(jiān)聽(tīng),遞歸調(diào)用遞歸調(diào)用sendRequestToBridgeByKey。
至此,核心代碼邏輯已經(jīng)全部結(jié)束。
理解這套代碼之后,就可以在其上做擴(kuò)展,豐富代碼,為你所用。理解完這套代碼,你能想到,它還有哪些使用場(chǎng)景嗎?是不是這個(gè)思路也可以用在遠(yuǎn)程控制上,如果你要控制客戶端時(shí),從這段代碼找找,是不是會(huì)有靈感。
這套代碼或許會(huì)有難點(diǎn),可能要對(duì)tcp/ip所有了解,也需要對(duì)http有所了解,并且知道一些關(guān)鍵的請(qǐng)求頭,知道一些關(guān)鍵的響應(yīng)信息,當(dāng)然,對(duì)于http了解的越多越好。
如果有什么需要交流,歡迎留言。
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怎么實(shí)現(xiàn)內(nèi)網(wǎng)穿透服務(wù)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。