基于Unix Socket的可靠Node.js HTTP代理实现(支持WebSocket协议)
实现代理服务,最常见的便是代理服务器代理相应的协议体请求源站,并将响应从源站转发给客户端。而在本文的场景中,代理服务及源服务采用相同技术栈(Node.js),源服务是由代理服务fork出的业务服务(如下图),代理服务不仅负责请求反向代理及转发规则设定,同时也负责业务服务伸缩扩容、日志输出与相关资源监控报警。下文称源服务为业务服务。
最初笔者采用上图的架构,业务服务为真正的HTTP服务或WebSocket服务,其侦听服务器的某个端口并处理代理服务的转发请求。可这有一些问题会困扰我们:
- 业务服务需要侦听端口,而端口是有上限的且有可能冲突(尽管可以避免冲突)
- 代理服务转发请求时,又在内核走了一次TCP/IP协议栈解析,且存在性能损耗(TCP的慢启动、ack机制等可靠性保证导致传输性能降低)
- 转发策略需要与端口耦合,业务移植时存在风险
因此,笔者尝试寻找更优的解决方案。
基于Unix Socket协议的HTTP Server
老实说,之前学习linux网络编程的时候从没有尝试基于域套接字的HTTP Server,不过从协议上说,HTTP协议并没有严格要求传输层协议必须为TCP,因此如果底层采用基于字节流的Unix Socket传输,应该也是可以实现要求的。
同时相比较TCP协议实现的可靠传输,Unix Socket作为IPC有些优点:
- Unix Socket仅仅复制数据,并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不产生顺序号,也不需要发送确认报文
- 仅依赖命名管道,不占用端口
Unix Socket并不是一种协议,它是进程间通信(IPC)的一种方式,解决本机的两个进程通信
在Node.js的http模块和net模块,都提供了相关接口 “listen(path, cb)”,不同的是http模块在Unix Socket之上封装了HTTP的协议解析及相关规范,因此这是可以无缝兼容基于TCP实现的HTTP服务的。
下为基于Unix Socket的HTTP Server与Client 样例:
const http = require('http');
const path = require('path');
const fs = require('fs');
const p = path.join(__dirname,'tt.sock');
fs.unlinkSync(p);
let s = http.createServer((req, res)=> {
req.setEncoding('utf8')
req.on('data',(d)=>{
console.log('server get:', d)
});
res.end('helloworld!!!');
});
s.listen(p);
setTimeout(()=>{
let c = http.request( {
method: 'post',
socketPath: p,
path: '/test'
}, (res) => {
res.setEncoding('utf8');
res.on('data', (chunk) => {
console.log(`响应主体: ${chunk}`);
});
res.on('end', () => {
});
});
c.write(JSON.stringify({abc: '12312312312'}));
c.end();
},2000)
代理服务与业务服务进程的创建
代理服务不仅仅是代理请求,同时也负责业务服务进程的创建。在更为高级的需求下,代理服务同时也担负业务服务进程的扩容与伸缩,当业务流量上来时,为了提高业务服务的吞吐量,代理服务需要创建更多的业务服务进程,流量洪峰消散后回收适当的进程资源。透过这个角度会发现这种需求与cluster和child_process模块息息相关,因此下文会介绍业务服务集群的具体实现。
本文中的代理为了实现具有粘性session功能的WebSocket服务,因此采用了child_process模块创建业务进程。这里的粘性session主要指的是Socket.IO的握手报文需要始终与固定的进程进行协商,否则无法建立Socket.IO连接(此处Socket.IO连接特指Socket.IO成功运行之上的连接),具体可见我的文章 socket.io搭配pm2(cluster)集群解决方案 。不过,在fork业务进程的时候,会通过pre_hook脚本重写子进程的 http.Server.listen() 从而实现基于Unix Socket的底层可靠传输,这种方式则是参考了 cluster 模块对子进程的相关处理,关于cluster模块覆写子进程的listen,可参考我的另一篇文章 Nodejs cluster模块深入探究 的“多个子进程与端口复用”一节。
// 子进程pre_hook脚本,实现基于Unix Socket可靠传输的HTTP Server
function setupEnvironment() {
process.title = 'ProxyNodeApp: ' + process['env']['APPNAME'];
http.Server.prototype.originalListen = http.Server.prototype.listen;
http.Server.prototype.listen = installServer;
loadApplication();
}
function installServer() {
var server = this;
var listenTries = 0;
doListen(server, listenTries, extractCallback(arguments));
return server;
}
function doListen(server, listenTries, callback) {
function errorHandler(error) {
// error handle
}
// 生成pipe
var socketPath = domainPath = generateServerSocketPath();
server.once('error', errorHandler);
server.originalListen(socketPath, function() {
server.removeListener('error', errorHandler);
doneListening(server, callback);
process.nextTick(finalizeStartup);
});
process.send({
type: 'path',
path: socketPath
});
}
这样就完成了业务服务的底层基础设施,到了业务服务的编码阶段无需关注传输层的具体实现,仍然使用 http.Server.listen(${any_port})即可。此时业务服务侦听任何端口都可以,因为在传输层根本没有使用该端口,这样就避免了系统端口的浪费。
流量转发
流量转发包括了HTTP请求和WebSocket握手报文,虽然WebSocket握手报文仍然是基于HTTP协议实现,但需要不同的处理,因此这里分开来说。
HTTP流量转发
此节可参考 “基于Unix Socket的HTTP Server与Client”的示例,在代理服务中新创建基于Unix Socket的HTTP client请求业务服务,同时将响应pipe给客户端。
class Client extends EventEmitter{
constructor(options) {
super();
options = options || {};
this.originHttpSocket = options.originHttpSocket;
this.res = options.res;
this.rej = options.rej;
if (options.socket) {
this.socket = options.socket;
} else {
let self = this;
this.socket = http.request({
method: self.originHttpSocket.method,
socketPath: options.sockPath,
path: self.originHttpSocket.url,
headers: self.originHttpSocket.headers
}, (res) => {
self.originHttpSocket.set(res.headers);
self.originHttpSocket.res.writeHead(res.statusCode);
// 代理响应
res.pipe(self.originHttpSocket.res)
self.res();
});
}
}
send() {
// 代理请求
this.originHttpSocket.req.pipe(this.socket);
}
}
// proxy server
const app = new koa();
app.use(async ctx => {
await new Promise((res,rej) => {
// 代理请求
let client = new Client({
originHttpSocket: ctx,
sockPath: domainPath,
res,
rej
});
client.send();
});
});
let server = app.listen(8000);
WebSocket报文处理
如果不做WebSocket报文处理,到此为止采用Socket.IO仅仅可以使用 “polling” 模式,即通过XHR轮询的形式实现假的长连接,WebSocket连接无法建立。因此,如果为了更好性能体验,需要处理WebSocket报文。这里主要参考了“http-proxy”的实现,针对报文做了一些操作:
- 头部协议升级字段检查
- 基于Unix Socket的协议升级代理请求
报文处理的核心在于第2点:创建一个代理服务与业务服务进程之间的“长连接”(该连接时基于Unix Socket管道的,而非TCP长连接),并使用此连接overlay的HTTP升级请求进行协议升级。
此处实现较为复杂,因此只呈现代理服务的处理,关于WebSocket报文处理的详细过程,可参考 proxy-based-unixsocket。
// 初始化ws模块
wsHandler = new WsHandler({
target: {
socketPath: domainPath
}
}, (err, req, socket) => {
console.error(`代理wsHandler出错`, err);
});
// 代理ws协议握手升级
server.on('upgrade',(req, socket, head) =>{
wsHandler.ws(req, socket, head);
});
回顾与总结
大家都知道,在Node.js范畴实现HTTP服务集群,应该使用cluster模块而不是“child_process”模块,这是因为采用child_process实现的HTTP服务集群会出现调度上不均匀的问题(内核为了节省上下文切换开销做出来的“优化之举”,详情可参考 Nodejs cluster模块深入探究“请求分发策略”一节)。可为何在本文的实现中仍采用child_process模块呢?
答案是:场景不同。作为代理服务,它可以使用cluster模块实现代理服务的集群;而针对业务服务,在session的场景中需要由代理服实现对应的转发策略,其他情况则采用RoundRobin策略即可,因此child_process模块更为合适。
本文并未实现代理服务的负载均衡策略,其实现仍然在 Nodejs cluster模块深入探究 中讲述,因此可参阅此文。
最终,在保持进程模型稳定的前提下,变更了底层协议可实现更高性能的代理服务。
本文代码proxy-based-unixsocket。
基于Unix Socket的可靠Node.js HTTP代理实现(支持WebSocket协议)的更多相关文章
- TCP Socket Programming in Node.js
TCP Socket Programming in Node.js Posted on October 26th, 2011 under Node.jsTags: Client, node.js, S ...
- 基于promise用于浏览器和node.js的http客户端的axios
axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它本身具有以下特征: 从浏览器中创建 XMLHttpRequest 从 node.js 发出 http 请求 支 ...
- 基于 Markdown 的开源的 Node.js 知识库平台
Raneto 是一个免费,开源的 Node.js 知识库平台,基于静态 Markdown 文件实现. Raneto 可以被称为静态网站生成器,因为它并不需要数据库支持.所有的内容都存储在 Markdo ...
- 分布式消息总线,基于.NET Socket Tcp的发布-订阅框架之离线支持,附代码下载
一.分布式消息总线以及基于Socket的实现 在前面的分享一个分布式消息总线,基于.NET Socket Tcp的发布-订阅框架,附代码下载一文之中给大家分享和介绍了一个极其简单也非常容易上的基于.N ...
- node.js 使用 net 模块模拟 websocket 握手,进行数据传递。
websocket 是一种让浏览器与服务器之间建立持久的连接,并能进行双向数据传输的一种协议. websocket 属性应用层协议,基于tcp传输协议,并复用http的握手通道. 一.如何进行webs ...
- 一个基于React整套技术栈+Node.js的前端页面制作工具
pagemaker是一个前端页面制作工具,方便产品,运营和视觉的同学迅速开发简单的前端页面,从而可以解放前端同学的工作量.此项目创意来自网易乐得内部项目nfop中的pagemaker项目.原来项目的前 ...
- Unix Socket 代理服务 unix域套接字
基于Unix Socket的可靠Node.js HTTP代理实现(支持WebSocket协议) - royalrover - 博客园 https://www.cnblogs.com/accordion ...
- 使用node.js 进行服务器端JavaScript编程
node.js 入门 node.js 可以运行在 Linux.Windows 和 Macintosh 等主流的操作系统上.在 Windows 平台上运行 node.js ...
- node.js入门(二) 模块 事件驱动
模块化结构 node.js 使用了 CommonJS 定义的模块系统.不同的功能组件被划分成不同的模块.应用可以根据自己的需要来选择使用合适的模块.每个模块都会暴露一些公共的方法或属性.模块使用者直接 ...
随机推荐
- 复杂的Polygon
- 如何回收VCSA 6自带的vPostgres数据库空间
最近有学生连续反应由于VCSA磁盘空间满了,导致服务无法正常启动,寻求压缩数据库空间的问题.首先说下,VCSA的数据库是没办法图形界面管理的, 它的内置vPostgres数据库的管理只能通过命令行来完 ...
- [LC] 117. Populating Next Right Pointers in Each Node II
Given a binary tree struct Node { int val; Node *left; Node *right; Node *next; } Populate each next ...
- 收集到的技术相关网址——python
1.Python中常用数据库访问接口模块 专用数据库连接模块——MySQL.SQLite.PostgreSQL.Oracle.IBM DB2.Infomix.Interbase.Sybase.SQL ...
- [LC] 746. Min Cost Climbing Stairs
On a staircase, the i-th step has some non-negative cost cost[i] assigned (0 indexed). Once you pay ...
- Markdown 内嵌 HTML 语法
Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式.Markdown内嵌HTML,本文总结了一些常用的HTML标记用于扩展Markdow ...
- 修改 commit message
本文为原创文章,转载请标明出处 目录 修改上一条提交的 commit message 修改之前提交的 commit message 1. 修改上一条提交的 commit message git com ...
- SQL语言基础及数据库的创建
一.数据类型:1.二进制数据二进制数据以十六进制形式存储.二进制数据最多能存8000个英文字符,4000个汉字字符. 2.字符数据char:存100,不足100补足.varcha:存多少占多少. 3. ...
- Python---10小结
因一边上班一边自学python,一旦忙起来,python就会放两天,可是2天后之前学的内容就会有点忘记. 今天把python的各种启动方法总结一下; 我的文档路径: ------- 1打开文件所在的c ...
- 递归、尾递归和使用Stream延迟计算优化尾递归
我们在学数据结构的时候必然会接触栈(Stack),而栈有一个重要的应用是在程序设计语言中实现递归.递归用途十分广泛,比如我们常见的阶乘,如下代码: 1234 public static int (in ...