Nodejs socket.io线上生产环境有经验的请进

Nodejs socket.io线上生产环境有经验的请进

这两天简单的用socket.io做了下封装来实现用户在线状态的统计,在实际应用中,遇到了很诡异的问题。

简单来说,就说在部分机器上会重复的给服务器发送连接请求,按照昨天出问题后回看的日志中,最多的会发送超过1000次,不知道现在各位是否有遇到同样的情况,如果有的话,求解。

transports策略为[“websocket” , “htmlfile” , “xhr-polling” , “jsonp-polling”],和express进行了整合(socket.io:0.9.14,express:3.2.5,node.js:v0.10.5,策略里没有flashsocket的原因是实际测试发现这种模式建立连接要等待好几秒,同时默认的重连策略感觉大概超过1-2秒以后就无法自动重连)。

大致的流程就是客户端浏览器在建立连接后将一些信息发送给服务端,然后服务端进行状态保持,当监听到用户断开后,再更新用户状态。

举例出问题的连接(最夸张的一个): “Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)-******”:1518 按照useragent来解析,这台用户的机器是winxp,用的是ie8,到出问题为止一共发送了1518次连接请求,但是我用同样的环境来连就不会出现这种情况(我本机就是这样)。

不知道问题这样描述是否详细,。


17 回复

针对你提到的在使用 socket.io 实现用户在线状态统计时遇到的诡异问题,即部分机器会重复发送连接请求的情况,我们可以从多个角度来分析并尝试解决这个问题。

可能的原因及解决方案

  1. 浏览器兼容性问题:

    • 某些旧版浏览器(如 IE8)可能对 WebSocket 和其他传输方式的支持不完全。可以考虑限制这些浏览器的连接,或者提供更友好的错误处理机制。
    const io = require('socket.io')(server, {
      transports: ["websocket", "htmlfile", "xhr-polling", "jsonp-polling"]
    });
    
    io.on('connection', function(socket) {
      if (socket.request.headers['user-agent'].includes("MSIE")) {
        socket.disconnect();
        console.log('IE detected, connection refused.');
      }
    });
    
  2. 网络环境问题:

    • 检查客户端与服务器之间的网络连接是否稳定。不稳定或高延迟的网络可能导致频繁的重新连接尝试。
    io.on('connect_error', function(socket) {
      console.log('Connection error:', socket.request.client.conn);
    });
    
  3. 重试策略调整:

    • 调整客户端的重试策略,避免过于频繁地尝试重新连接。
    var socket = io.connect('http://yourserver.com', {
      'reconnection': true,
      'reconnectionDelay': 5000,
      'reconnectionAttempts': 5
    });
    
  4. 服务器端日志增强:

    • 增强服务器端的日志记录,以便更好地追踪和分析连接请求的行为。
    io.on('connection', function(socket) {
      console.log(`Client connected with user agent: ${socket.request.headers['user-agent']}`);
      socket.on('disconnect', function() {
        console.log(`Client disconnected with user agent: ${socket.request.headers['user-agent']}`);
      });
    });
    
  5. 安全性检查:

    • 确保客户端连接请求的安全性,防止恶意请求导致的频繁连接。
    io.use(function(socket, next) {
      if (!socket.request.headers['x-forwarded-for']) {
        return next(new Error('Invalid request headers'));
      }
      next();
    });
    

示例代码

以下是一个完整的示例代码片段,包含了上述的一些改进措施:

const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server, {
  transports: ["websocket", "htmlfile", "xhr-polling", "jsonp-polling"]
});

io.use(function(socket, next) {
  if (!socket.request.headers['x-forwarded-for']) {
    return next(new Error('Invalid request headers'));
  }
  next();
});

io.on('connection', function(socket) {
  console.log(`Client connected with user agent: ${socket.request.headers['user-agent']}`);

  if (socket.request.headers['user-agent'].includes("MSIE")) {
    socket.disconnect();
    console.log('IE detected, connection refused.');
    return;
  }

  socket.on('disconnect', function() {
    console.log(`Client disconnected with user agent: ${socket.request.headers['user-agent']}`);
  });
});

app.get('/', function(req, res) {
  res.send('Socket.IO is running');
});

server.listen(3000, function() {
  console.log('Server listening on port 3000');
});

希望这些方案能帮助你解决在生产环境中遇到的问题。


补充下,我在代码里还用到了cluster,默认配置只启动一个子进程

  • 确定你的浏览器支持websocket?
  • 禁用htmlfile协议,如果启用了,它与gzip是不相容的,忘了具体原因,现在也没空给你看线上配置
  • 前端可以禁止socket.io重连配置,你自己监控disconnect事件,然后重连

首先,感谢您的回复

确定你的浏览器支持websocket? 这个应该不是重点,用socket.io就是想利用他现成的长连接的解决方案

禁用htmlfile协议,如果启用了,它与gzip是不相容的,忘了具体原因,现在也没空给你看线上配置 这个是否说要在transports策略中去掉htmlfile

前端可以禁止socket.io重连配置,你自己监控disconnect事件,然后重连 现在我用的是默认的自动连接方案,回头我试下。

顺便问下,请问你们现在线上的服务是要求用户使用只支持websocket的浏览器吗?

  • websocket不是长连接,是全双工
  • 线上我把websocket和htmlfile都关了,只剩jsonp-polling和xhr-polling。

哦,行吧,谢谢

问题依旧,郁闷,总会有这种不断连接的现象出现,从而导致端口不释放出问题。 (node) warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit. Trace at Socket.EventEmitter.addListener (events.js:160:15) at Socket.Readable.on (_stream_readable.js:663:33) at Socket.EventEmitter.once (events.js:179:8) at TCP.onread (net.js:527:26)

我已经按照网上的一些做法对app,io以及process都增加了设置 app.setMaxListeners(0); process.setMaxListeners(0); io.setMaxListeners(0); 但是仍然出现问题,求各种help

出问题的时候,node的进程cpu占满,请求过去一直不会返回。

你的代码应该写的有问题,方便贴出来吗?

方便,这个需要全部贴出来吗?还是只需要其中某一部分

前端页面里的代码 <input id=“chatName” type=“hidden”/><input id=“chatUserName” type=“hidden”/> <script type=“text/javascript” src=“http://XX.XX.XX.XX:443/socket.io/socket.io.js”></script> <script type=“text/javascript” src=“http://XX.XX.XX.XX:443/javascripts/browser.js”></script> <script type=“text/javascript” > //获得coolie 的值 function cookie(name){
var cookieArray=document.cookie.split("; “); //得到分割的cookie名值对
var cookie=new Object();
for (var i=0;i<cookieArray.length;i++){
var arr=cookieArray[i].split(”="); //将名和值分开
if(arr[0]==name)return unescape(arr[1]); //如果是指定的cookie,则返回它的值
} return “”; } WEBSSO_LID = cookie(“WEBSSO_LID”); $("#chatName").val(location.href); $("#chatUserName").val(“test” + parseInt(Math.random() * 1000000000000)); chat.login();

</script>

js文件browser.js var browserInfo = navigator.userAgent; var socketioHttp = “http://XX.XX.XX.XX:443”;

var chat = {}; chat.kickUser = function (){ alert(“抱歉,您已在其它地方登录!”); location.reload(); } chat.cmdObj = {“register” : 1 , “leave” : 2 , “talk” : 3 , “func” : 4}; chat.login = function (){ chat.connectServer(); }; chat.getParams = function (params){ if (params) { var arr1 = params.split("&"); var map = {}; for ( var i = 0; i < arr1.length; i++) { var arr2 = arr1[i].split("="); map[arr2[0]] = arr2.length > 1 ? arr2[1] : “”; } return map; } return {}; } chat.connectSocketIo = function(socketioHttp){ chat.socket.socket.reconnect(); } var reconnectInterval ; chat.connectServer = function(){ chat.userName = $("#chatUserName").val(); chat.moudle = $("#chatName").val(); if(!chat.userName || !chat.moudle){ return; } //chat.socket = io.connect(‘http://127.0.0.1:443’); chat.socket = io.connect(socketioHttp , {“reconnect” : false});

chat.socket.on("connect",function(){
	//var random = browserInfo + "-" + new Date().getTime() + "-" + parseInt(Math.random() * 10000);
	var random = new Date().getTime() + "-" + parseInt(Math.random() * 10000);
	chat.socket.emit("msgs" , {cmd : chat.cmdObj["register"] , msg : {uqKey : chat.userName , moudle : chat.moudle , randomKey : random , userAgent : browserInfo}});
});
chat.socket.on("disconnect",function(){
	reconnectInterval = setInterval(function(){chat.connectSocketIo();} , 5 * 1000);
});
chat.socket.on('reconnect', function(){
	//console.log("Reconnected to Server"); 
	clearInterval(reconnectInterval);
});
chat.socket.on('reconnecting', function( nextRetry ){ 
	//console.log("Reconnecting in " + nextRetry + " seconds"); 
});
chat.socket.on('reconnect_failed', function(){
	//console.log("Reconnect Failed"); 
});

chat.socket.on(‘msgs’, function (data) { var msg = data.msg; if(data.cmd == chat.cmdObj[“register”]){ var ele = $("#chat_msg"); $("#login").hide(); if(msg.uqKey != chat.userName){ var msg = “<p><a href=’#’ onclick='chat.changeTalk(”" + msg.uqKey + “”);’>" + msg.uqKey + “</a>进入直播室</p>”; ele.append(msg); } $("#chat").show(); } if(data.cmd == chat.cmdObj[“leave”]){ var ele = $("#chat_msg"); $("#login").hide(); var msg = “<p>” + msg.uqKey +“离开直播室</p>”; ele.append(msg); $("#chat").show(); } if(data.cmd == chat.cmdObj[“talk”]){ var ele = $("#chat_msg"); $("#login").hide(); var msg = “<p>” + (msg.from == chat.userName ? “我” : “<a href=’#’ onclick='chat.changeTalk(”" + msg.from + “”);’>" + msg.from + “</a>”) +" 对 " + (msg.toId == “” ? “<a href=’#’ onclick='chat.changeTalk(”");’>大家</a>" : (msg.toId == chat.userName ? “我” : “<a href=’#’ onclick='chat.changeTalk(”" + msg.toId + “”);’>" + msg.toId + “</a>”)) + “说:” + msg.info + “</p>”; ele.append(msg); $("#chat").show(); $("#chat_msg").scrollTop($("#chat_msg").scrollTop() + 100); } if(data.cmd == chat.cmdObj[“func”]){ var func = msg.func; eval(func); } });

}; chat.talk = function(){ var talk_info = $("#talk_info").val(); var to_id = $("#to_id").val(); chat.socket.emit(“msgs” , {cmd : chat.cmdObj[“talk”] , msg : {from : chat.userName ,moudle : chat.moudle ,info : talk_info , toId : to_id}}); $("#talk_info").val(""); }; chat.changeTalk = function(uqName){ $("#to_id").val(uqName); };

顺便问下,您用的是node.js的什么版本,我在这个地方用的是v0.10.5,之前在另台机器上写了一个类似的服务,但是没用express和mongodb,那个是用的v0.6.12,那个就不会出这问题。 今天看到一个老外的帖子遇到类似的问题,他是在9和10版本出现,8这个版本就不会。 如您看到,请帮我查看下对应的版本,谢谢。

我用的node0.8.22,没有用mongodb,用了express3

你的代码太多了?看得晕。你先把自动重连给禁用了:

socket = io.connect(url, {reconnect:false});

然后自己写重连,大体是这样的:

socket.on('disconnect', function() {
    console.log('连接已断开...');
    intervalID = setInterval(function(){
        socket.socket.reconnect();
        clearInterval(intervalID);   
    }, 4000);
});

然后你查看下客户端日志,验证下重连的情况。我猜测你那个短时间内重练了1000多次,是因为那个浏览器使用的通信协议不对,然后一直连不上。

请问intervalID = setInterval(function(){},这句是做什么用的啊?

你说的这个,我已经修改了,去掉了自动重连,然后在client里增加了相关的重连代码。 按照你这样的情况来看,我大概确定怎么回事了,结合我之前功能的使用版本v0.6.x,应该是node.js的版本问题,我今天看了一篇国外的帖子,他那边跟我类似也是出现这种情况,V0.8.X版本正常,但是用到9,10就出现我这种报错。 感谢您最近这段时间的解答,后面我会切换回旧版本的node.js,传送们 https://github.com/joyent/node/issues/5108#issuecomment-16897815

针对您描述的问题,用户在某些特定条件下向服务器发送了大量重复的连接请求。这种现象可能是由于多种原因引起的,包括但不限于网络不稳定、客户端配置问题或服务器配置问题等。

可能的原因及解决方案

1. 客户端配置问题

部分老旧浏览器可能不会正确处理WebSocket连接断开后的重新连接逻辑。您可以尝试优化客户端的重连逻辑,例如增加重试间隔、限制最大重试次数等。

示例代码

var io = require('socket.io-client');
var socket = io.connect('http://yourserver.com', {
    transports: ['websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling'],
    'reconnect': true,
    'reconnection delay': 1000, // 延迟1秒重连
    'max reconnection attempts': 5 // 最大重连次数为5次
});

2. 网络不稳定

某些网络环境下可能会导致频繁的连接中断和重连。可以考虑增加超时时间和错误处理机制。

示例代码

socket.on('connect_error', function () {
    console.log('连接失败,检查网络连接!');
});

3. 服务端配置问题

服务端配置也可能导致连接问题。确保您的Socket.IO版本与客户端兼容,并且服务端能够正确处理大量的并发连接。

示例代码

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);

io.on('connection', function (socket) {
    console.log('用户连接成功');
    socket.on('disconnect', function () {
        console.log('用户断开连接');
    });
});

server.listen(3000, function () {
    console.log('服务运行在 http://localhost:3000');
});

其他建议

  • 日志监控:记录客户端的连接和断开日志,有助于追踪异常。
  • 压力测试:使用工具如abwrk对系统进行压力测试,找出潜在瓶颈。
  • 性能优化:使用负载均衡技术分摊请求压力。

通过上述方法,您可以更全面地排查并解决这个问题。希望这些建议对您有所帮助!

回到顶部