当前位置: 首页 > 后端技术 > Node.js

基于UnixSocket的可靠Node.jsHTTP代理实现(支持WebSocket协议)

时间:2023-04-04 01:13:53 Node.js

实现代理服务。最常见的是代理服务器向源站点请求相应的协议体,并将源站点的响应转发给客户端。本文场景中,代理服务和源服务使用相同的技术栈(Node.js),源服务是代理服务fork出来的业务服务(如下图)。代理服务不仅负责请求反向代理和转发规则设置,还负责业务服务的伸缩和扩展、日志输出以及相关资源的监控和告警。以下将源服务称为业务服务。最初笔者采用上图的架构,业务服务是一个真正的HTTP服务或者WebSocket服务,监听服务器的某个端口,处理代理服务的转发请求。但是有一些问题会困扰我们:业务服务需要监听端口,端口有上限,可能会冲突(虽然可以避免冲突)。代理服务在转发请求时,在内核中经过了一次TCP/IP协议栈解析,存在性能损失(TCP慢启动、ack机制等可靠性保证导致传输性能下降)。转发策略需要和端口耦合,业务迁移存在风险。因此,作者试图寻找更好的解决方案。基于UnixSocket协议的HTTPServer说实话,之前学习linux网络编程的时候并没有尝试过基于domainsocket的HTTPServer,但是从协议上看,HTTP协议并没有严格要求传输层协议必须是TCP,所以如果底层采用基于字节流的UnixSocket传输,应该也能满足要求。同时,与TCP协议实现的可靠传输相比,UnixSocket作为IPC具有一些优势:UnixSocket只复制数据,不进行协议处理,不需要增删网络头,不需要修改。计算校验和,不生成序号,不进行协议处理。需要发送确认消息只依赖命名管道,不占用端口UnixSocket不是一个协议,它是一种进程间通信(IPC)的方式,在http模块中解决本机两个进程间的通信Node.js和net模块,都提供了相关接口“listen(path,cb)”,不同的是http模块在UnixSocket之上封装了HTTP协议解析和相关规范,所以无缝兼容基于TCP的的HTTP服务。以下是基于UnixSocket的HTTPServer和Client示例:consthttp=require('http');constpath=require('路径');constfs=require('fs');constp=path.join(__dirname,'tt.sock');fs.unlinkSync(p);lets=http.createServer((req,res)=>{req.setEncoding('utf8')req.on('data',(d)=>{console.log('serverget:',d)});res.end('helloworld!!!');});s.listen(p);setTimeout(()=>{letc=http.request({method:'post',socketPath:p,path:'/test'},(res)=>{res.setEncoding('utf8');res.on('data',(chunk)=>{console.log(`Responsebody:${chunk}`);});res.on('end',()=>{});});c.write(JSON.stringify({abc:'12312312312'}));c.end();},2000)代理服务和业务服务流程的创建代理服务不仅是代理请求,还负责业务服务流程的创建.在更高级的需求下,代理服务还负责业务服务流程的伸缩。当业务流量上来时,为了提高业务服务的吞吐量,代理服务需要创建更多的业务服务进程。在流量高峰消散后回收适当的进程资源。从这个角度来看,会发现这个需求与cluster和child_process模块??密切相关,所以下面介绍业务服务集群的具体实现。为了实现具有粘性会话功能的WebSocket服务,本文中的代理使用child_process模块??创建业务流程。这里的stickysession主要是指Socket.IO的握手报文需要一直和固定的进程协商,否则无法建立Socket.IO连接(这里的Socket.IO连接是指成功上面的连接)Socket.IO的操作)具体可以看我的文章socket.iowithpm2(cluster)集群解决方案。但是在fork业务进程时,会通过pre_hook脚本重写子进程的http.Server.listen(),实现基于UnixSocket的底层可靠传输。该方法参考了cluster模块对子进程的相关处理,cluster模块覆盖子进程的listen请参考我的另一篇文章Nodejs集群模块中的“多子进程与端口复用”部分-深度探索。//子进程pre_hook脚本,实现基于UnixSocket可靠传输的HTTPServerfunctionsetupEnvironment(){process.title='ProxyNodeApp:'+process['env']['APPNAME'];http.Server.prototype.originalListen=http.Server.prototype.listen;http.Server.prototype.listen=installServer;loadApplication();}functioninstallServer(){varserver=this;varlistenTries=0;doListen(服务器,listenTries,extractCallback(参数));returnserver;}functiondoListen(server,listenTries,callback){functionerrorHandler(error){//错误句柄}//生成管道varsocketPath=domainPath=generateServerSocketPath();server.once('错误',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流量转发请参考《UnixSocket-basedHTTPServerandClient》示例,在代理服务中新建一个基于UnixSocket的HTTP客户端请求业务服务,并将响应通过管道传递给客户端。类客户端扩展EventEmitter{构造函数(选项){super();选项=选项||{};this.originHttpSocket=options.originHttpSocket;this.res=options.res;this.rej=options.rej;如果(options.socket){this.socket=options.socket;}else{让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.sock等);}}//代理服务器constapp=newkoa();app.use(asyncctx=>{awaitnewPromise((res,rej)=>{//代理请求letclient=newClient({originHttpSocket:ctx,sockPath:domainPath,res,rej});client.send();});});letserver=app.listen(8000);WebSocket消息处理如果不做WebSocket消息处理,目前使用Socket.IO只能使用“轮询”方式,即通过XHR轮询实现伪长连接,无法建立WebSocket连接。因此,为了更好的性能体验,需要对WebSocket报文进行处理。这里主要参考“http-proxy”的实现,对报文做一些操作:检查header中的protocolupgrade字段。基于UnixSocket的协议升级代理请求消息处理的核心在于第二点:创建代理服务和业务服务进程之间的“长连接”(连接是基于UnixSocket管道的,不是TCP长连接连接),并使用此连接覆盖的HTTP升级请求来升级协议。这里的实现比较复杂,所以只介绍代理服务的处理。WebSocket报文处理的详细过程请参考proxy-based-unixsocket。//初始化ws模块wsHandler=newWsHandler({target:{socketPath:domainPath}},(err,req,socket)=>{console.error(`ProxywsHandlererror`,err);});//代理ws协议握手升级server.on('upgrade',(req,socket,head)=>{wsHandler.ws(req,socket,head);});回顾总结大家都知道HTTP服务是在Node.js类Cluster中实现的,应该使用cluster模块而不是“child_process”模块,因为child_process实现的HTTP服务集群会出现调度不均的问题(“优化举措””内核为节省上下文切换开销而做的,详情请参考Nodejs集群模块深入“请求分发策略”部分)。但是为什么本文的实现中还是使用了child_process模块??呢?答案是:场景不同。作为代理服务,可以使用cluster模块实现代理服务的集群;对于业务服务,在session场景下,代理服务器需要执行相应的转发策略,其他情况下可以使用RoundRobin策略,所以child_process模块??比较合适。本文没有实现代理服务的负载均衡策略,其实现还是在Nodejs集群模块深入探索中描述,可以参考本文。最后,在保持进程模型稳定的前提下,改变底层协议可以实现更高性能的代理服务。]~~~~