WebSocket Node構(gòu)建HTTP隧道實(shí)現(xiàn)實(shí)例
前言
當(dāng)我們開發(fā)一些與第三方服務(wù)集成的應(yīng)用程序時(shí),我們需要使我們的本地開發(fā)服務(wù)器暴露在網(wǎng)上。為此,我們需要為本地服務(wù)器提供一個(gè) HTTP 隧道。HTTP 隧道如何工作?嘎嘎嘎,看下面!????
為什么我們需要部署自己的 HTTP 隧道服務(wù)?
HTTP隧道有很多很?的在線服務(wù)。??,我們可以用來獲得付費(fèi)的固定公共域來連接你的本地服務(wù)器。它還有一個(gè)免費(fèi)包。但是對于免費(fèi)套餐??,你無法獲得固定域。重新啟動(dòng)客戶端后,你將獲得一個(gè)新的隨機(jī)域。當(dāng)你需要在第三方服務(wù)中保存域時(shí),這很不方便。
要獲得固定域,我們可以在自己的服務(wù)器中部署HTTP隧道。 還提供用于服務(wù)器端部署的開源版本。但它是舊的 1.x 版本,不建議在生產(chǎn)環(huán)境中部署,存在一些嚴(yán)重的可靠性問題。
使用我們自己的服務(wù)器,它還可以確保數(shù)據(jù)安全。?
關(guān)于精簡版 HTTP 隧道簡介
Lite HTTP 隧道是我最近為自托管 HTTP 隧道服務(wù)構(gòu)建的。你可以使用 Github 存儲庫中的按鈕部署它,以快速獲得免費(fèi)的固定 Heroku 域。Heroku
它是基于并且只有幾個(gè)代碼構(gòu)建的。它使用 WebSocket 將 HTTP/HTTPS 請求從公共服務(wù)器流式傳輸?shù)奖镜胤?wù)器。Express.js
`Socket.io`
如何實(shí)現(xiàn)它
步驟 1:在服務(wù)器和客戶端之間建立 WebSocket 連接
支持服務(wù)器端的WebSocket連接:socket.io
const http = require('http'); const express = require('express'); const { Server } = require('socket.io'); const app = express(); const httpServer = http.createServer(app); const io = new Server(httpServer); let connectedSocket = null; io.on('connection', (socket) => { console.log('client connected'); connectedSocket = socket; const onMessage = (message) => { if (message === 'ping') { socket.send('pong'); } } const onDisconnect = (reason) => { console.log('client disconnected: ', reason); connectedSocket = null; socket.off('message', onMessage); socket.off('error', onError); }; const onError = (e) => { connectedSocket = null; socket.off('message', onMessage); socket.off('disconnect', onDisconnect); }; socket.on('message', onMessage); socket.once('disconnect', onDisconnect); socket.once('error', onError); }); httpServer.listen(process.env.PORT);
?在客戶端連接 WebSocket:
const { io } = require('socket.io-client'); let socket = null; function initClient(options) { socket = io(options.server, { transports: ["websocket"], auth: { token: options.jwtToken, }, }); socket.on('connect', () => { if (socket.connected) { console.log('client connect to server successfully'); } }); socket.on('connect_error', (e) => { console.log('connect error', e && e.message); }); socket.on('disconnect', () => { console.log('client disconnected'); }); }
步驟 2:使用 JWT 令牌保護(hù) Websocket 連接
?在服務(wù)器端,我們使用 socket.io 中間件來拒絕無效連接:
const jwt = require('jsonwebtoken'); io.use((socket, next) => { if (connectedSocket) { return next(new Error('Connected error')); } if (!socket.handshake.auth || !socket.handshake.auth.token){ next(new Error('Authentication error')); } jwt.verify(socket.handshake.auth.token, process.env.SECRET_KEY, function(err, decoded) { if (err) { return next(new Error('Authentication error')); } if (decoded.token !== process.env.VERIFY_TOKEN) { return next(new Error('Authentication error')); } next(); }); });
步驟 3:將請求從服務(wù)器流式傳輸?shù)娇蛻舳?/h3>
?我們實(shí)現(xiàn)一個(gè)可寫流來將請求數(shù)據(jù)發(fā)送到隧道客戶端:
const { Writable } = require('stream'); class SocketRequest extends Writable { constructor({ socket, requestId, request }) { super(); this._socket = socket; this._requestId = requestId; this._socket.emit('request', requestId, request); } _write(chunk, encoding, callback) { this._socket.emit('request-pipe', this._requestId, chunk); this._socket.conn.once('drain', () => { callback(); }); } _writev(chunks, callback) { this._socket.emit('request-pipes', this._requestId, chunks); this._socket.conn.once('drain', () => { callback(); }); } _final(callback) { this._socket.emit('request-pipe-end', this._requestId); this._socket.conn.once('drain', () => { callback(); }); } _destroy(e, callback) { if (e) { this._socket.emit('request-pipe-error', this._requestId, e && e.message); this._socket.conn.once('drain', () => { callback(); }); return; } callback(); } } app.use('/', (req, res) => { if (!connectedSocket) { res.status(404); res.send('Not Found'); return; } const requestId = uuidV4(); const socketRequest = new SocketRequest({ socket: connectedSocket, requestId, request: { method: req.method, headers: { ...req.headers }, path: req.url, }, }); const onReqError = (e) => { socketRequest.destroy(new Error(e || 'Aborted')); } req.once('aborted', onReqError); req.once('error', onReqError); req.pipe(socketRequest); req.once('finish', () => { req.off('aborted', onReqError); req.off('error', onReqError); }); // ... });
?實(shí)現(xiàn)可讀的流以在客戶端獲取請求數(shù)據(jù):
const stream = require('stream'); class SocketRequest extends stream.Readable { constructor({ socket, requestId }) { super(); this._socket = socket; this._requestId = requestId; const onRequestPipe = (requestId, data) => { if (this._requestId === requestId) { this.push(data); } }; const onRequestPipes = (requestId, data) => { if (this._requestId === requestId) { data.forEach((chunk) => { this.push(chunk); }); } }; const onRequestPipeError = (requestId, error) => { if (this._requestId === requestId) { this._socket.off('request-pipe', onRequestPipe); this._socket.off('request-pipes', onRequestPipes); this._socket.off('request-pipe-error', onRequestPipeError); this._socket.off('request-pipe-end', onRequestPipeEnd); this.destroy(new Error(error)); } }; const onRequestPipeEnd = (requestId, data) => { if (this._requestId === requestId) { this._socket.off('request-pipe', onRequestPipe); this._socket.off('request-pipes', onRequestPipes); this._socket.off('request-pipe-error', onRequestPipeError); this._socket.off('request-pipe-end', onRequestPipeEnd); if (data) { this.push(data); } this.push(null); } }; this._socket.on('request-pipe', onRequestPipe); this._socket.on('request-pipes', onRequestPipes); this._socket.on('request-pipe-error', onRequestPipeError); this._socket.on('request-pipe-end', onRequestPipeEnd); } _read() {} } socket.on('request', (requestId, request) => { console.log(`${request.method}: `, request.path); request.port = options.port; request.hostname = options.host; const socketRequest = new SocketRequest({ requestId, socket: socket, }); const localReq = http.request(request); socketRequest.pipe(localReq); const onSocketRequestError = (e) => { socketRequest.off('end', onSocketRequestEnd); localReq.destroy(e); }; const onSocketRequestEnd = () => { socketRequest.off('error', onSocketRequestError); }; socketRequest.once('error', onSocketRequestError); socketRequest.once('end', onSocketRequestEnd); // ... });
步驟 4:將響應(yīng)從客戶端流式傳輸?shù)椒?wù)器
?實(shí)現(xiàn)可寫流以將響應(yīng)數(shù)據(jù)發(fā)送到隧道服務(wù)器:
const stream = require('stream'); class SocketResponse extends stream.Writable { constructor({ socket, responseId }) { super(); this._socket = socket; this._responseId = responseId; } _write(chunk, encoding, callback) { this._socket.emit('response-pipe', this._responseId, chunk); this._socket.io.engine.once('drain', () => { callback(); }); } _writev(chunks, callback) { this._socket.emit('response-pipes', this._responseId, chunks); this._socket.io.engine.once('drain', () => { callback(); }); } _final(callback) { this._socket.emit('response-pipe-end', this._responseId); this._socket.io.engine.once('drain', () => { callback(); }); } _destroy(e, callback) { if (e) { this._socket.emit('response-pipe-error', this._responseId, e && e.message); this._socket.io.engine.once('drain', () => { callback(); }); return; } callback(); } writeHead(statusCode, statusMessage, headers) { this._socket.emit('response', this._responseId, { statusCode, statusMessage, headers, }); } } socket.on('request', (requestId, request) => { // ...stream request and send request to local server... const onLocalResponse = (localRes) => { localReq.off('error', onLocalError); const socketResponse = new SocketResponse({ responseId: requestId, socket: socket, }); socketResponse.writeHead( localRes.statusCode, localRes.statusMessage, localRes.headers ); localRes.pipe(socketResponse); }; const onLocalError = (error) => { console.log(error); localReq.off('response', onLocalResponse); socket.emit('request-error', requestId, error && error.message); socketRequest.destroy(error); }; localReq.once('error', onLocalError); localReq.once('response', onLocalResponse); });
?實(shí)現(xiàn)可讀流以在隧道服務(wù)器中獲取響應(yīng)數(shù)據(jù):
class SocketResponse extends Readable { constructor({ socket, responseId }) { super(); this._socket = socket; this._responseId = responseId; const onResponse = (responseId, data) => { if (this._responseId === responseId) { this._socket.off('response', onResponse); this._socket.off('request-error', onRequestError); this.emit('response', data.statusCode, data.statusMessage, data.headers); } } const onResponsePipe = (responseId, data) => { if (this._responseId === responseId) { this.push(data); } }; const onResponsePipes = (responseId, data) => { if (this._responseId === responseId) { data.forEach((chunk) => { this.push(chunk); }); } }; const onResponsePipeError = (responseId, error) => { if (this._responseId !== responseId) { return; } this._socket.off('response-pipe', onResponsePipe); this._socket.off('response-pipes', onResponsePipes); this._socket.off('response-pipe-error', onResponsePipeError); this._socket.off('response-pipe-end', onResponsePipeEnd); this.destroy(new Error(error)); }; const onResponsePipeEnd = (responseId, data) => { if (this._responseId !== responseId) { return; } if (data) { this.push(data); } this._socket.off('response-pipe', onResponsePipe); this._socket.off('response-pipes', onResponsePipes); this._socket.off('response-pipe-error', onResponsePipeError); this._socket.off('response-pipe-end', onResponsePipeEnd); this.push(null); }; const onRequestError = (requestId, error) => { if (requestId === this._responseId) { this._socket.off('request-error', onRequestError); this._socket.off('response', onResponse); this._socket.off('response-pipe', onResponsePipe); this._socket.off('response-pipes', onResponsePipes); this._socket.off('response-pipe-error', onResponsePipeError); this._socket.off('response-pipe-end', onResponsePipeEnd); this.emit('requestError', error); } }; this._socket.on('response', onResponse); this._socket.on('response-pipe', onResponsePipe); this._socket.on('response-pipes', onResponsePipes); this._socket.on('response-pipe-error', onResponsePipeError); this._socket.on('response-pipe-end', onResponsePipeEnd); this._socket.on('request-error', onRequestError); } _read(size) {} } app.use('/', (req, res) => { // ... stream request to tunnel client const onResponse = (statusCode, statusMessage, headers) => { socketRequest.off('requestError', onRequestError) res.writeHead(statusCode, statusMessage, headers); }; socketResponse.once('requestError', onRequestError) socketResponse.once('response', onResponse); socketResponse.pipe(res); const onSocketError = () => { res.end(500); }; socketResponse.once('error', onSocketError); connectedSocket.once('close', onSocketError) res.once('close', () => { connectedSocket.off('close', onSocketError); socketResponse.off('error', onSocketError); }); });
??完成所有步驟后,我們支持將 HTTP 請求流式傳輸?shù)奖镜赜?jì)算機(jī),并將響應(yīng)從本地服務(wù)器發(fā)送到原始請求。它是一個(gè)精簡的解決方案,但它穩(wěn)定且易于在任何環(huán)境中部署。Node.js
更多
如果你只想查找具有免費(fèi)固定域的HTTP隧道服務(wù),則可以嘗試在Github自述文件中將Lite HTTP Tunnel項(xiàng)目部署到with中。希望小伙伴們能從這篇文章中學(xué)到一些東西??。Heroku
`Heroku deploy button`
以上就是WebSocket Node構(gòu)建HTTP隧道實(shí)現(xiàn)實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于WebSocket Node構(gòu)建HTTP的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.js 使用 cors 中間件解決跨域問題小結(jié)
cors 是 Express 的一個(gè)第三方中間件,通過安裝和配置 cors 中間件,可以很方便地解決跨域問題,本文介紹Node.js 使用 cors 中間件解決跨域問題,感興趣的朋友一起看看吧2024-01-01使用pkg打包nodejs項(xiàng)目并解決本地文件讀取的問題
這篇文章主要介紹了使用pkg打包nodejs項(xiàng)目并解決本地文件讀取的問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10node中Express 動(dòng)態(tài)設(shè)置端口的方法
本篇文章主要介紹了node中Express 動(dòng)態(tài)設(shè)置端口的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08node.js Promise對象的使用方法實(shí)例分析
這篇文章主要介紹了node.js Promise對象的使用方法,結(jié)合實(shí)例形式分析了node.js中Promise對象的功能、定義、調(diào)用方法及相關(guān)使用技巧,需要的朋友可以參考下2019-12-12Node.js環(huán)境下編寫爬蟲爬取維基百科內(nèi)容的實(shí)例分享
WikiPedia平時(shí)在國內(nèi)不大好訪問-- 所以用爬蟲一次性把要看的東西都爬下來保存慢慢看還是比較好的XD 這里我們就來看一下Node.js環(huán)境下編寫爬蟲爬取維基百科內(nèi)容的實(shí)例分享2016-06-06如何解決安裝websocket還是報(bào)錯(cuò)Cannot find module'ws&apos
這篇文章主要介紹了如何解決安裝websocket還是報(bào)Cannot find module'ws'問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02nodejs實(shí)現(xiàn)的http、https 請求封裝操作示例
這篇文章主要介紹了nodejs實(shí)現(xiàn)的http、https 請求封裝操作,結(jié)合實(shí)例形式分析了node.js針對http、https 請求的封裝與使用相關(guān)操作技巧,需要的朋友可以參考下2020-02-02