WebSocket Node構(gòu)建HTTP隧道實(shí)現(xiàn)實(shí)例
前言
當(dāng)我們開發(fā)一些與第三方服務(wù)集成的應(yīng)用程序時,我們需要使我們的本地開發(fā)服務(wù)器暴露在網(wǎng)上。為此,我們需要為本地服務(wù)器提供一個 HTTP 隧道。HTTP 隧道如何工作?嘎嘎嘎,看下面!????
為什么我們需要部署自己的 HTTP 隧道服務(wù)?
HTTP隧道有很多很?的在線服務(wù)。??,我們可以用來獲得付費(fèi)的固定公共域來連接你的本地服務(wù)器。它還有一個免費(fèi)包。但是對于免費(fèi)套餐??,你無法獲得固定域。重新啟動客戶端后,你將獲得一個新的隨機(jī)域。當(dāng)你需要在第三方服務(wù)中保存域時,這很不方便。
要獲得固定域,我們可以在自己的服務(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ò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)一個可寫流來將請求數(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ù)奖镜赜嬎銠C(jī),并將響應(yīng)從本地服務(wù)器發(fā)送到原始請求。它是一個精簡的解決方案,但它穩(wěn)定且易于在任何環(huán)境中部署。Node.js
更多
如果你只想查找具有免費(fèi)固定域的HTTP隧道服務(wù),則可以嘗試在Github自述文件中將Lite HTTP Tunnel項目部署到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 的一個第三方中間件,通過安裝和配置 cors 中間件,可以很方便地解決跨域問題,本文介紹Node.js 使用 cors 中間件解決跨域問題,感興趣的朋友一起看看吧2024-01-01
node中Express 動態(tài)設(shè)置端口的方法
本篇文章主要介紹了node中Express 動態(tài)設(shè)置端口的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
node.js Promise對象的使用方法實(shí)例分析
這篇文章主要介紹了node.js Promise對象的使用方法,結(jié)合實(shí)例形式分析了node.js中Promise對象的功能、定義、調(diào)用方法及相關(guān)使用技巧,需要的朋友可以參考下2019-12-12
Node.js環(huán)境下編寫爬蟲爬取維基百科內(nèi)容的實(shí)例分享
WikiPedia平時在國內(nèi)不大好訪問-- 所以用爬蟲一次性把要看的東西都爬下來保存慢慢看還是比較好的XD 這里我們就來看一下Node.js環(huán)境下編寫爬蟲爬取維基百科內(nèi)容的實(shí)例分享2016-06-06
如何解決安裝websocket還是報錯Cannot find module'ws&apos
這篇文章主要介紹了如何解決安裝websocket還是報Cannot find module'ws'問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-02-02
nodejs實(shí)現(xiàn)的http、https 請求封裝操作示例
這篇文章主要介紹了nodejs實(shí)現(xiàn)的http、https 請求封裝操作,結(jié)合實(shí)例形式分析了node.js針對http、https 請求的封裝與使用相關(guān)操作技巧,需要的朋友可以參考下2020-02-02

