介绍一下我是如何构建一个基于WebSocket的HTTP Tunnel工具的。
每天分享最新软件开发,Devops,敏捷,测试以及项目管理最新,最热门的文章,每天花3分钟学习何乐而不为,希望大家点赞,评论,加关注,你的支持是我最大的动力。下方抖音有我介绍自动化测试,以及google cloud 相关视频课程,欢迎观看。
当我们开发一些与第三方服务集成的应用程序时,我们需要让我们的本地开发服务器暴露在互联网上。为此,我们需要为本地服务器建立一个 HTTP 。HTTP Tunnel是如何工作的?在本文中,我将向您展示如何构建 HTTP Tunnel工具。
为什么我们需要部署自己的 HTTP Tunnel服务?
HTTP Tunnel有很多很棒的在线服务。例如,我们可以使用 获得付费的固定公共域来连接您的本地服务器。它也有一个免费的包。但是对于免费套餐,您无法获得固定域。重新启动客户端后,您将获得一个新的随机域。当您需要将域保存在第三方服务中时,这很不方便。 ngrok
为了得到一个固定的域,我们可以在我们自己的服务器上部署一个 HTTP Tunnel。 还提供了 用于服务器端部署的开源版本 。但它是一个旧的 1.x 版本,不建议在生产中部署,存在一些严重的可靠性问题。 ngrok
使用我们自己的服务器,它还可以保证数据的安全。
Lite HTTP Tunnel简介
Lite HTTP Tunnel 是我最近为自托管 HTTP Tunnel服务构建的。您可以在 Github 存储库中使用按钮部署它, 以快速获得免费的固定 Heroku 域。 Heroku
它是基于 并 仅使用一些代码构建的。它使用 WebSocket 将来自公共服务器的 HTTP/HTTPS 请求流式传输到本地服务器。 Express.js Socket.io
我如何实现它
步骤1:建立一个WebSocket服务器和客户端之间的连接
在服务器端支持WebSocket连接 socket . io :
JavaScript
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连接客户端:
JavaScript
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 令牌保护 Websocket 连接
在服务器端,我们使用套接字。io 中间件拒绝无效连接:
JavaScript
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:流请求从服务器到客户端
我们实现一个可写流Tunnel客户端发送请求数据:
JavaScript
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);
});
// ...
});
实现一个流可读的get请求数据在客户端:
JavaScript
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:流响应从客户端到服务器
实现一个可写流将响应数据发送到Tunnel服务器:
JavaScript
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);
});
实现一个可读的流在Tunnel响应数据服务器:
JavaScript
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 请求流式传输到本地计算机,并将本地服务器的响应发送到原始请求。它是一个精简的解决方案,但在任何环境 下都稳定且易于部署 。node . js
更多的
如果你只是想找一个免费的固定域名的 HTTP Tunnel服务,可以尝试将 Lite HTTP Tunnel 项目部署
https://github.com/web-tunnel/lite-http-tunnel
到 与 在 Github README 中。希望你能从这篇文章中有所收获。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请通知我们,一经查实,本站将立刻删除。