基于 Unix Socket 的可靠 Node.js HTTP 代理实现(支持 WebSocket 协议)

Posted Nodejs技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 Unix Socket 的可靠 Node.js HTTP 代理实现(支持 WebSocket 协议)相关的知识,希望对你有一定的参考价值。


作者@欲休 | https://www.cnblogs.com/accordion/p/12457505.html

实现代理服务,最常见的便是代理服务器代理相应的协议体请求源站,并将响应从源站转发给客户端。而在本文的场景中,代理服务及源服务采用相同技术栈(Node.js),源服务是由代理服务fork出的业务服务(如下图),代理服务不仅负责请求反向代理及转发规则设定,同时也负责业务服务伸缩扩容、日志输出与相关资源监控报警。下文称源服务为业务服务。

基于 Unix Socket 的可靠 Node.js HTTP 代理实现(支持 WebSocket 协议)

最初笔者采用上图的架构,业务服务为真正的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 样例:

 
   
   
 
  1. const http = require('http');

  2. const path = require('path');

  3. const fs = require('fs');

  4. const p = path.join(__dirname,'tt.sock');


  5. fs.unlinkSync(p);

  6. let s = http.createServer((req, res)=> {

  7. req.setEncoding('utf8')

  8. req.on('data',(d)=>{

  9. console.log('server get:', d)

  10. });

  11. res.end('helloworld!!!');

  12. });


  13. s.listen(p);


  14. setTimeout(()=>{

  15. let c = http.request( {

  16. method: 'post',

  17. socketPath: p,

  18. path: '/test'

  19. }, (res) => {

  20. res.setEncoding('utf8');

  21. res.on('data', (chunk) => {

  22. console.log(`响应主体: ${chunk}`);

  23. });


  24. res.on('end', () => {

  25. });

  26. });

  27. c.write(JSON.stringify({abc: '12312312312'}));

  28. c.end();

  29. },2000)

代理服务与业务服务进程的创建

代理服务不仅仅是代理请求,同时也负责业务服务进程的创建。在更为高级的需求下,代理服务同时也担负业务服务进程的扩容与伸缩,当业务流量上来时,为了提高业务服务的吞吐量,代理服务需要创建更多的业务服务进程,流量洪峰消散后回收适当的进程资源。透过这个角度会发现这种需求与cluster和child_process模块息息相关,因此下文会介绍业务服务集群的具体实现。

本文中的代理为了实现具有粘性session功能的WebSocket服务,因此采用了childprocess模块创建业务进程。这里的粘性session主要指的是Socket.IO的握手报文需要始终与固定的进程进行协商,否则无法建立Socket.IO连接(此处Socket.IO连接特指Socket.IO成功运行之上的连接),具体可见我的文章 socket.io搭配pm2(cluster)集群解决方案 。不过,在fork业务进程的时候,会通过prehook脚本重写子进程的 http.Server.listen() 从而实现基于Unix Socket的底层可靠传输,这种方式则是参考了 cluster 模块对子进程的相关处理,关于cluster模块覆写子进程的listen,可参考我的另一篇文章 Nodejs cluster模块深入探究 的“多个子进程与端口复用”一节。

 
   
   
 
  1. // 子进程pre_hook脚本,实现基于Unix Socket可靠传输的HTTP Server

  2. function setupEnvironment() {

  3. process.title = 'ProxyNodeApp: ' + process['env']['APPNAME'];

  4. http.Server.prototype.originalListen = http.Server.prototype.listen;

  5. http.Server.prototype.listen = installServer;

  6. loadApplication();

  7. }

  8. function installServer() {

  9. var server = this;

  10. var listenTries = 0;

  11. doListen(server, listenTries, extractCallback(arguments));

  12. return server;

  13. }


  14. function doListen(server, listenTries, callback) {

  15. function errorHandler(error) {

  16. // error handle

  17. }

  18. // 生成pipe

  19. var socketPath = domainPath = generateServerSocketPath();

  20. server.once('error', errorHandler);

  21. server.originalListen(socketPath, function() {

  22. server.removeListener('error', errorHandler);

  23. doneListening(server, callback);

  24. process.nextTick(finalizeStartup);

  25. });


  26. process.send({

  27. type: 'path',

  28. path: socketPath

  29. });

  30. }

这样就完成了业务服务的底层基础设施,到了业务服务的编码阶段无需关注传输层的具体实现,仍然使用 http.Server.listen(${any_port})即可。此时业务服务侦听任何端口都可以,因为在传输层根本没有使用该端口,这样就避免了系统端口的浪费。

流量转发

流量转发包括了HTTP请求和WebSocket握手报文,虽然WebSocket握手报文仍然是基于HTTP协议实现,但需要不同的处理,因此这里分开来说。

HTTP流量转发

此节可参考 “基于Unix Socket的HTTP Server与Client”的示例,在代理服务中新创建基于Unix Socket的HTTP client请求业务服务,同时将响应pipe给客户端。

 
   
   
 
  1. class Client extends EventEmitter{

  2. constructor(options) {

  3. super();

  4. options = options || {};

  5. this.originHttpSocket = options.originHttpSocket;

  6. this.res = options.res;

  7. this.rej = options.rej;

  8. if (options.socket) {

  9. this.socket = options.socket;

  10. } else {

  11. let self = this;

  12. this.socket = http.request({

  13. method: self.originHttpSocket.method,

  14. socketPath: options.sockPath,

  15. path: self.originHttpSocket.url,

  16. headers: self.originHttpSocket.headers

  17. }, (res) => {

  18. self.originHttpSocket.set(res.headers);

  19. self.originHttpSocket.res.writeHead(res.statusCode);

  20. // 代理响应

  21. res.pipe(self.originHttpSocket.res)

  22. self.res();

  23. });

  24. }

  25. }

  26. send() {

  27. // 代理请求

  28. this.originHttpSocket.req.pipe(this.socket);

  29. }

  30. }

  31. // proxy server

  32. const app = new koa();

  33. app.use(async ctx => {

  34. await new Promise((res,rej) => {

  35. // 代理请求

  36. let client = new Client({

  37. originHttpSocket: ctx,

  38. sockPath: domainPath,

  39. res,

  40. rej

  41. });

  42. client.send();

  43. });

  44. });


  45. 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。

 
   
   
 
  1. // 初始化ws模块

  2. wsHandler = new WsHandler({

  3. target: {

  4. socketPath: domainPath

  5. }

  6. }, (err, req, socket) => {

  7. console.error(`代理wsHandler出错`, err);

  8. });


  9. // 代理ws协议握手升级

  10. server.on('upgrade',(req, socket, head) =>{

  11. wsHandler.ws(req, socket, head);

  12. });

回顾与总结

大家都知道,在Node.js范畴实现HTTP服务集群,应该使用cluster模块而不是“childprocess”模块,这是因为采用childprocess实现的HTTP服务集群会出现调度上不均匀的问题(内核为了节省上下文切换开销做出来的“优化之举”,详情可参考 Nodejs cluster模块深入探究“请求分发策略”一节)。可为何在本文的实现中仍采用child_process模块呢?

答案是:场景不同。作为代理服务,它可以使用cluster模块实现代理服务的集群;而针对业务服务,在session的场景中需要由代理服实现对应的转发策略,其他情况则采用RoundRobin策略即可,因此child_process模块更为合适。

本文并未实现代理服务的负载均衡策略,其实现仍然在 Nodejs cluster模块深入探究 中讲述,因此可参阅此文。

最终,在保持进程模型稳定的前提下,变更了底层协议可实现更高性能的代理服务。

本文代码:https://github.com/royalrover/proxy-based-unixsocket

- END -

往期精彩回顾



以上是关于基于 Unix Socket 的可靠 Node.js HTTP 代理实现(支持 WebSocket 协议)的主要内容,如果未能解决你的问题,请参考以下文章

每天看点源码 peertalk —— 一种基于 Unix Domain Socket 的 RPC 方法

Python基于unix socket的并发技巧

Python基于unix socket的并发技巧

基于tcp的socket通信

Linux上实现基于TCP和Unix domain Socket的本地IPC通信模型

Socket