socket.io에는 room 기능이 있습니다. 말그대로 그 방에 들어가면 그 방에 있는 사람들끼리만 메세지를 주고 받을 수 있는 기능입니다.

https://github.com/LearnBoost/socket.io/wiki/Rooms

처음에 이런 기능이 있는 줄 모르고 구현했었는데, wiki를 찾아보니 있네요.

사용법은 간단해요. 해당 socket 객체에 join, leave함수가 존재해요. join하고 방이름을 string으로 적으면 방에 입장하는 것이고, leave하면 방에서 나가는 것이에요. 

그리고 방에 있는 사용자들에게 메세지를 보내려면
io.sockets.in('room').emit('event_name', data);
이렇게 하면 돼요.

그래서 이걸 이용해서 손쉽게 랜덤채팅을 만들 수 있어요. 사용자가 접속하면 자기 id로 방을 만들고, 다른 사용자가 들어왔을 때 1명이 있는 방이 있으면 입장시키면 돼요. 그럼 두명이서만 대화를 할 수 있어요. 나중에 disconnect할 때에는 그 사용자가 어디에 접속해있는지 socketRoom객체에 저장해두었다가 그 방을 폭파시키고 로비로 보내버리면 돼요.

※여기서 좀 주의할 점이 방을 만들면 io.sockets.manager.rooms에 방 목록을 볼 수 있는데, 여기에 방키값 앞에 "/"가 붙습니다. 그래서 다른 사용자들을 그 키로 입장 시킬 때 그 값을 제거하고 넣어야 하더라구요.

app.js

var app = require('http').createServer(handler),
    io = require('socket.io').listen(app),
    fs = require('fs');
   
app.listen(process.env.PORT || 9000);

function handler(req, res){
    fs.readFile(__dirname + '/index.html',
        function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            res.writeHead(200);
            data = data.toString('utf-8').replace('<%=host%>', req.headers.host);
            res.end(data);
        }
    );
};

// socket.io 셋팅
io.configure(function(){
    io.set('transports', ['xhr-polling']);
    io.set('polling duration', 10);
    io.set('log level', 2);
});

var socketRoom = {};

io.sockets.on('connection', function(socket){
    // 접속완료를 알림.
    socket.emit('connected');
   
    // chat요청을 할 시
    socket.on('requestRandomChat', function(data){
        // 빈방이 있는지 확인
        console.log('requestRandomChat');
        var rooms = io.sockets.manager.rooms;
        for (var key in rooms){
            if (key == ''){
                continue;
            }
            // 혼자있으면 입장
            if (rooms[key].length == 1){
                var roomKey = key.replace('/', '');
                socket.join(roomKey);
                io.sockets.in(roomKey).emit('completeMatch', {});
                socketRoom[socket.id] = roomKey;
                return;
            }
        }
        // 빈방이 없으면 혼자 방만들고 기다림.
        socket.join(socket.id);
        socketRoom[socket.id] = socket.id;
    });
   
    // 요청 취소 시
    socket.on('cancelRequest', function(data){
        socket.leave(socketRoom[socket.id]);
    });
   
    // client -> server Message전송 시
    socket.on('sendMessage', function(data){
        console.log('sendMessage!');
        io.sockets.in(socketRoom[socket.id]).emit('receiveMessage', data);
    });
   
    // disconnect
    socket.on('disconnect', function(data){
        var key = socketRoom[socket.id];
        socket.leave(key);
        io.sockets.in(key).emit('disconnect');
        var clients = io.sockets.clients(key);
        for (var i = 0; i < clients.length; i++){
            clients[i].leave(key);
        }
    });
});


index.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>socketio redis store</title>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" />
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
    <script type="text/javascript" src="/socket.io/socket.io.js"></script>
    <script type="text/javascript">
        var socket = io.connect('http://<%=host%>');
       
        $(document).ready(function(){
            socket.on('connected', function(){
                console.log('connected');
            });
           
            // 매칭완료되었을 때
            socket.on('completeMatch', function(data){
                console.log('completeMatch!');
                $('.lobby').hide();
                $('.wait').hide();
                $('.chat').show();
            });
           
            // 대화를 받았을 때
            socket.on('receiveMessage', function(data){
                $('.chatResult').append('<li>' + data.message + '</li>');
            });
            // 상대방이 나갔을 때 나도 같이 로비로 나감.
            socket.on('disconnect', function(data){
                console.log('disconnect');
                $('.lobby').show();
                $('.chat').hide();
            });
           
            // 랜덤요청 시
            $('#btnRequestRandomChat').click(function(){
                $('.lobby').hide();
                $('.wait').show();
                socket.emit('requestRandomChat');
            });
           
            // 요청 취소 시
            $('#btnCancelRequest').click(function(){
                $('.lobby').show();
                $('.wait').hide();
                $('.chat').hide();
                socket.emit('cancelRequest');
            });
           
            // 엔터입력 시
            $('#inputMessage').keyup(function(e){
                if (e.keyCode == 13){
                    send();
                }
            });
            // 채팅 내용 전송 시
            $('#btnChat').click(function(){
                send();
            });
        });
       
        function send(){
            var message = $('#inputMessage').val();
            if (message.length < 1){
                alert('내용을 입력하세요.');
                return;
            }
            socket.emit('sendMessage', {message:message});
            $('#inputMessage').val('');
        }
    </script>
</head>
<body>
    <div class="lobby container">
        <button id="btnRequestRandomChat" class="btn">채팅입장</button>
    </div>
    <div class="wait container" style="display:none">
        <div>상대방을 기다리는 중...</div>
        <button id="btnCancelRequest" class="btn">취소</button>
    </div>
    <div class="chat container" style="display:none">
        <input type="text" id="inputMessage" class="input-medium search-query" />
        <button type="submit" id="btnChat" class="btn">전송</button>
        <ul class="chatResult">
        </ul>
    </div>
</body>
</html>

이상입니다.

heroku에도 올려봤어요.
http://random-chat.herokuapp.com/

소스는 github에.....
https://github.com/mudchobo/nodejs-random_chat

 
Posted by 머드초보
,
 

socket.io는 일단 기본 store가 메모리 기반이여서 프로세스를 하나 띄우면 거기서 밖에 socket.io 접속자들을 공유를 못해요. 그래서 socket.io에서는 store를 redis로 변경해서 여러 프로세스에서도 공유를 할 수 있는 옵션을 제공을 해요.

var RedisStore = require('socket.io/lib/stores/redis')
  , redis  = require('socket.io/node_modules/redis')
  , pub    = redis.createClient()
  , sub    = redis.createClient()
  , client = redis.createClient();

io.set('store', new RedisStore({
  redisPub : pub
, redisSub : sub
, redisClient : client
}));

이렇게 하면 된답니다.

그래서 한 번 해보면, redis를 설치해야해요. 저는 windows환경이라 virtualbox에 ubuntu설치하고 redis를 설치했어요. 아 그리고 나중에 redis도 1대로 못버티면 redis도 clustering해야하는데, 이건 나중에 삽질 해보고....

이게 원리가 redis에서 제공하는 pub/sub기능을 이용해서 하는건데, 하나의 서버에서 pub/sub를 만들고 연결해놓고 나중에 현재 서버에서 접속이 들어오면 다른 서버들에 publish를 해서 알려주고, 다른 서버는 구독중이기에 연결된 데이터를 받을 수 있습니다.

대략 소스는 이러합니다.

server.js

var redisInfo = {
    host: '192.168.56.1',
    port: 6379
};
var app = require('http').createServer(handler),
    io = require('socket.io').listen(app),
    fs = require('fs'),
    RedisStore = require('socket.io/lib/stores/redis'),
    redis = require('socket.io/node_modules/redis'),
    pub = redis.createClient(redisInfo.port, redisInfo.host),
    sub = redis.createClient(redisInfo.port, redisInfo.host),
    client = redis.createClient(redisInfo.port, redisInfo.host);

if (process.argv.length < 3){
    console.log('ex) node app <port>');
    process.exit(1);
}
app.listen(process.argv[2]);

function handler(req, res) {
    fs.readFile(__dirname + '/index.html',
        function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            res.writeHead(200);
            data = data.toString('utf-8').replace('<%=host%>', req.headers.host);
            res.end(data);
        });
}

io.configure(function(){
    io.set('store', new RedisStore({
        redisPub: pub,
        redisSub : sub,
        redisClient : client
    }));
});

io.sockets.on('connection', function (socket) {
    socket.on('message', function(data){
        socket.broadcast.emit('message', data);
    });
});


index.html

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>socketio redis store</title>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
    <script type="text/javascript" src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io.connect('http://<%=host%>');
       
        $(document).ready(function(){
            socket.on('message', function(data){
                $('#chat').append('<li>' + data.message + '</li>');
            });
           
            $('#btnSend').click(function(){
                send();
            });
            $('#inputText').keyup(function(e){
                if (e.keyCode == 13){
                    send();
                }
            });
        });
        function send(){
            var message = $('#inputText').val();
            if (message.length < 1){
                return;
            }
            socket.emit('message', {message:message});
            $('#chat').append('<li>' + message + '</li>');
            $('#inputText').val('');
        }
    </script>
</head>
<body>
    socketio redis store...<br />
    <input type="text" id="inputText" />
    <button id="btnSend">보내기</button>
    <ul id="chat">
    </ul>
</body>
</html>

node app.js 10001

node app.js 10002

두 개 띄워놓고 localhost:10001, localhost:10002 접속하면 두 서버에서 같은 방에 있는 것처럼 채팅을 할 수 있습니다.

redis는 기본적으로 127.0.0.1로 bind를 해서 외부에서 접속을 할 수 없습니다. 그래서 /etc/redis/redis.conf 파일을 수정해서 bind 127.0.0.1을 주석처리하거나 알맞게 수정하시면 됩니다.

socket.io는 참 잘해놓은게 다른 것은 전혀 신경쓸 것이 없이 store옵션만 바꾸어서 socket들의 정보 저장위치를 변경하게 만들어 놨습니다. 나중에 서버 확장을 할 때에도 매우 쉽게 할 수 있습니다.(하지만, redis clustering 조회해보고 있는데 힘들어보이는.....)

소스는 github에.......

https://github.com/mudchobo/nodejs-socketio_redis_store

 
Posted by 머드초보
,