Rev. 2.73

이전 학습에서 Socket.IO퍼블릭/브로드캐스트/프라이빗 전송 개념에 대해 알아보았습니다. 프라이빗 전송을 이용하면 특정 대상에게만 패킷을 소비할 수 있다는 사실을 알 수 있었죠. 이를 응용하여 이 번 시간에는 효율적으로 패킷을 소비할 수 있는 방법에 대하여 알아보겠습니다.

근대 웹 사용 행태에서 두드러진 변화중 하나는 탭 브라우징입니다. 이로 인해 사용자들이 사이트에 머물러있는 시간도 급격히 늘어나게 되었습니다. 이는 곧 웹사이트에 접속되어 있는 상태이긴 하지만 사용자가 웹사이트를 이용중이지 않을 수도 있다는 것을 의미합니다. 혹은 페이지가 열여있는 상태지만 다른 창에서 작업중인 경우도 마찬가지죠. 무조건 온라인 상태의 사용자를 통신 대상으로 삼는 Socket.IO는 무의미한 패킷을 소비하는 상태일수도 있다는 것입니다. 아쉽게도 이를 우회하는 장치를 제공하고 있지 않기 때문에 직접 구현해야 합니다.

아래는 이전 학습에 사용했던 코드에 추가적으로 idle(유휴) 상태를 구분할수 있도록 추가하고 유효한 상태의 클라이언트들에게만 패킷을 소비하도록 하는 내용을 작성한 것입니다.

 var io = require('socket.io').listen(8080);

io.sockets.on('connection', function(socket) {

  var timeout = 10000 // idle timeout
    , sockets = io.sockets.sockets // sockets store
    , clients = {} // clients store
    , timer = null; // timeout store

  idle();

  socket.on('join', function (data) {
    clients[socket.id] = {
      idle: false
    };
    socket.broadcast.emit('join', socket.id, data);
  });

  socket.on('message', function (data) {
    for (var id in clients) {
      if (socket.id != id && !clients[id].idle)
        sockets[id].emit('message', socket.id, data);
    }
    idle();
  });

  socket.on('whisper', function (id, data, fn) {
    if (id && sockets[id]) {
      sockets[id].emit('whisper', socket.id, data);
      fn(true);
    } else {
      fn(false);
    }
    idle();
  });

  socket.on('idle', function (data) {
    clients[socket.id].idle = data; // boolean
    socket.broadcast.emit('idle', socket.id, data);   
  });

  socket.on('disconnect', function () {
    clear();
    delete clients[socket.id];
    socket.broadcast.emit('close', socket.id);
  });

  // set idle timer
  function idle() {
    clear();
    timer = setTimeout(function() {
      if (clients[socket.id]) {
        clients[socket.id].idle = true;
        socket.broadcast.emit('idle', socket.id, true);
      }
      timer = null;
    }, timeout);
  }

  // clear idle timer
  function clear() {
    clients[socket.id].idle = false;
    timer && clearTimeout(timer);
  }

});

이것으로 대상이 자리를 비웠는지를 서버가 감시할 수 있게 되었으며, 자리를 비운 사용자에게는 패킷을 절약할 수 있습니다. 이와 유사한 로직을 클라이언트-사이드에 추가하여 사용자들의 상태를 공유하는 기능을 추가하고 사용자에게 알릴수 있습니다. 마우스 움직임이나 클릭을 감시하여 유휴상태를 측정하는 코드를 작성하면 더욱 신용할 수 있는 유휴상태 구분할 수 있게됩니다. 하지만, 채팅과 같은 기록성 패킷이나 알림성 패킷인 'join'이나 'disconnect'에는 적용하지 않는것이 좋겠죠?

Comments

퍼블릭/브로드캐스트/프라이빗 전송 개념은 Socket.IO를 사용하면서 익혀야할 중요한 서버-사이드 개념입니다. 퍼블릭(Public)은 발송자를 포함한 모든 클라이언트들에게, 브로드캐스트는 발송자를 제외한 다른 모든 클라이언트들에게, 그리고 프라이빗은 지정된 개인에게 패킷을 전송하는 것을 말합니다. 특히, 프라이빗 전송은 보안을 필요로하는 귓속말과 같은 기능을 구현할 때 아주 요긴하게 사용될 수 있습니다. 만약, 이를 구분하지 않는다면 사실상 모든 사용자들에게 귓속말에 해당하는 패킷을 발송하고 클라이언트-사이드에서 귓속말 대상 사용자 외에 다른 사용자들은 보여주지 않도록 예외처리 작업으로 구현해야겠지요. 겉으로는 완전해 보이겠지만, 이건 눈가리고 아웅입니다. 트래픽 낭비에 보안문제도 안고있죠. 다음 예제는 각기 다른 상황에 대한 응답 형태를 Node.JS 서버에서 구분한 것입니다.

var io = require('socket.io').listen(8080);

io.sockets.on('connection', function(socket) {

  // on public
  socket.on('join', function (data) {
    io.sockets.emit('join', socket.id, data);
  });

  // on broadcast
  socket.on('message', function (data) {
    socket.broadcast.emit('message', socket.id, data);
  });

  // on privat
  socket.on('whisper', function (id, data, fn) {
    if (id && io.sockets.sockets[id]) {
      io.sockets.sockets[id].emit('whisper', socket.id, data);
      fn(true);
    } else {
      fn(false);
    }
  });

  socket.on('disconnect', function () {
    socket.broadcast.emit('close', socket.id);
  });

});

위 예제에서 브라이빗 패킷 송/수신 부분을 살펴 보면 클라이언트로 부터 수신할 대상의 id를 받는 부분에 집중해야 합니다. Socket.IO는 자체적으로 sessionId를 발급하고 이것으로 사용자를 구분합니다. 위 예제는 이 sessionId를 클라이언트에서도 동일하게 취급한다고 가정한 것입니다. 그리고 또 한 가지 주의해야 할 점은 발송 대상 추출시 룸(room) 또는 채널(channel) 범주에 구속되어야 한다는 것입니다. io.sockets.sockets 개체에는 모든 클라이언트들에 대한 정보가 들어있기 때문입니다. 물론, 룸이나 채널을 사용하지 않으면 상관 없습니다.

Comments