在线上系统中,需要使用node的多进程模型,我们可以自己实现简易的基于cluster模式的socket分发模型,也可以使用比较稳定的pm2这样进程管理工具。在常规的http服务中,这套模式一切正常,可是一旦server中集成了socket.io服务就会导致ws通道建立失败,即使通过backup的polling方式仍会出现时断时连的现象,因此我们需要解决这种问题,让socket.io充分利用多核。
在这里之所以提到socket.io而未说websocket服务,是因为socket.io在封装websocket基础上又保证了可用性。在客户端未提供websocket功能的基础上使用xhr polling、jsonp或forever iframe的方式进行兼容,同时在建立ws连接前往往通过几次http轮训确保ws服务可用,因此socket.io并不等于websocket。再往底层深入研究,socket.io其实并没有做真正的websocket兼容,而是提供了上层的接口以及namespace服务,真正的逻辑则是在“engine.io”模块。该模块实现握手的http代理、连接升级、心跳、传输方式等,因此研究engine.io模块才能清楚的了解socket.io实现机制。
场景重现
服务端采用express+socket.io的组合方案,搭配pm2的cluster模式,实现一个简易的b/s通信demo:
app.js
var path = require('path'); var app = require('express')(), server = require('http').createServer(app), io = require('socket.io')(server); io .on('connection', function(socket) { socket.on('disconnect', function() { console.log('/: disconnect-------->') }); socket.on('b:message', function() { socket.emit('s:message', '/: '+port); console.log('/: '+port) }); }); io.of('/ws') .on('connection', function(socket) { socket.on('disconnect', function() { console.log('/ws: disconnect-------->') }); socket.on('b:message', function() { socket.emit('/ws: message', port); }); }); app.get('/page',function(req,res){ res.sendFile(path.join(process.cwd(),'./index.html')); }); server.listen(8080);
index.html
<script> var btn = document.getElementById('btn1'); btn.addEventListener('click',function(){ var socket = io.connect('http://127.0.0.1:8080/ws',{ reconnection: false }); socket.on('connect',function(){ // 发起“脚手架安装”请求 socket.emit('b:message',{}); socket.on('s:message',function(d){ console.log(d); }); }); socket.on('error',function(err){ console.log(err); }) }); </script>
pm2.json
{ "apps": [ { "name": "ws", "script": "./app.js", "env": { "NODE_ENV": "development" }, "env_production": { "NODE_ENV": "production" }, "instances": 4, "exec_mode": "cluster", "max_restarts" : 3, "restart_delay" : 5000, "log_date_format" : "YYYY-MM-DD HH:mm Z", "combine_logs" : true } ] }
这样,执行命令pm2 start pm2.json即可开启服务,访问127.0.0.1:8080/page,点击按钮发起ws连接,观察控制台即可。
下图清晰显示了socket.io握手的错误:
可见在websocket连接建立之前多出了3个xhr请求,而websocket连接建立失败后又多出了几个xhr请求,同时最后两个xhr请求失败了。
socket.io没有采用直接建立websocket连接的粗暴方式,而是首先通过http请求(xhr)访问服务端的相关轮训配置信息以及sid。此处sid类似sessionID,但是它唯一标识连接,可理解为socketId,以后每次http请求cookie中都必须携带sid(httponly);
第二、三个请求用于确认连接,在socket.io中,post请求是客户端发送消息给服务端的唯一形式,而且post响应一定是“ok”,它的“content-length”一定为2;而get请求主要用于轮训,同时获取服务端的相关消息,这会在下文中有体现;
第四个websocket连接请求失败,这主要是由于与后端http握手失败造成的;
第五个请求为xhr方式的post请求,它是作为websocket通道建立失败后的一种兼容性处理,上文讲述了socket.io的post请求只在客户端需要发送消息给服务端时才会使用,因此,为了证实我们查看消息体:
可见,它携带了客户端发出的消息类型b:message,同时包含消息体{}空对象。对应的,服务端返回“OK”;
第六个请求为xhr方式的get请求,用来获取服务端对第五个请求的响应。
至此,大致分析了socket.io建立连接的大致过程以及连接建立失败后如何兜底的方案,下面分析为何出现握手失败的问题。
原因何在