Node.js는 이미 매우 강력한 서버로 인정받았고 널리 쓰이는 서버중 하나가 되었습니다. 그에 따라 Node가 하나의 플랫폼이 되가고 있고, NPM을 통해 수많은 node modules가 나오면서 생태계가 형성되고 있습니다. 그 많은 modules중에는 디버깅 툴이 없을리가 없겠죠. 하지만 디버깅 툴을 만든다는 것이 그렇게 쉬운 일만은 아니죠. 많은 node 디버깅 툴이 나오긴 했지만, 사용이 불편해서 많이 쓰이지 않고 node 자체가 자바스크립트를 쓰기 때문에 딱히 디버깅 툴을 쓰지 않아도 디버깅을 하는데는 크게 어려움이 없어서 디버깅 툴이 크게 주목받지 못했습니다. 

그러다가 굉장히 강력한 node 디버깅 툴이 나왔죠. 그게 바로 node inspector라는 툴입니다. 

node inspector: https://github.com/dannycoates/node-inspector

node inspector는 내부적으로 자체 node 서버를 돌리고 Webkit의 web inspector를 이용하여(크롬에서 F12 누르거나 오른쪽 버튼을 누르고 요소검사를 나오면 나오는 그 창 있죠? 그걸 말하는 겁니다.) node 서버를 디버깅하게 됩니다. web inpector에서 제공하는 기능들 중 서버쪽에서 사용할 만한 기능들을 대부분 지원하고 있습니다. 스크립트를 확인하여 break point를 걸어서 변수들을 확인할 수 있고, 특정 지점에서 break 되었을 때, console를 이용하여 즉흥적으로 스크립트를 작성하여 테스트해볼 수 도 있습니다. 그리고 아직 테스트중에 있는 기능은 v8-profiler를 이용하여 서버의 성능을 테스트 해 볼 수도 있습니다.

node inspector를 설치하기 위해서는 다른 node module과 마찬가지로 npm으로 설치해 줍니다.

$ npm install -g node-inspector

여기서 설치를 할때 -g 를 이용하여 글로벌로 설치를 해 줍시다. node inspector는 특정 서버 앱에 의존적이지 않으니까요.

그리고 node inspector를 사용하기 위해서는 node 서버를 실행을 할때 debug 모드로 실행을 해 주어야 합니다. debug모드로 실행을 하면 node 서버가 debug서버를 따로 돌리게 되는데, node inspector는 이를 캐치하여 web inspector에서 디버깅을 할 수 있습니다. 

$ node --debug your/node/program.js

디버깅 모드를 시작할때 한가지 옵션이 있는데, 노드를 시작하자 마자 브레이크를 걸 수 있습니다. 다음 명령어를 이용해서 말이죠.

$ node --debug-brk your/short/node/script.js

이렇게 디버깅모드에서 실행을 했으면 node inspector를 실행해 줍니다.

$ node-inspector &

뒤에 &는 백그라운드에서 실행하고 싶을때 붙이는 기호입니다. 터미널 환경에서 원래 쓰이는 기호입니다. 아무래도 node inspector는 한번 실행해 놓고 계속 디버깅을 해나가는 것이 편하기 때문에 백그라운드에서 실행하는 것을 추천합니다.

Webkit 기반의 웹 브라우저에서(크롬이 가장 안정적입니다.) 다음 URL로 접속해 줍니다.

http://127.0.0.1:8080/debug?port=5858 

여기서 8080은 node inspector가 실행되는 포트이고, 5858은 node의 debug 서버가 실행되는 포트입니다. 이렇게 실행을 하시면 크롬에서 많이 보시면 Webkit Inspector 화면을 보실 수 있습니다. 

여기서 부터는 브레이크 포인트를 걸고 콘솔창에서 스크립트를 쓰면서 놀면 되는 거죠. 자세한 사용법은 아래 동영상을 보시면 참고가 될 듯 합니다.

node inspector 사용 동영상: http://www.youtube.com/view_play_list?p=A5216AC29A41EFA8

v8-profiler같은 경우는 아직 완벽하지 않는 기능입니다. 기본적으로 node inspector에서 나타나지도 않습니다. 이는 따로 v8-profiler를 설치하여 사용하셔야 합니다. 한번 설치하여 써볼려고 시도는 해 보았는데, 잘 되지는 않더라고요. 아직 이슈가 많은 기능입니다.


출처 : 에돌이의 얕고 넓은 샘 - http://edoli.tistory.com/60

'Programming > Node.js' 카테고리의 다른 글

Socket.IO 클러스터링  (0) 2014.05.08
Socket.IO 4/4 - 채팅방 기능 추가하기  (0) 2014.05.08
Socket.IO 3/4 (1:1 귓속말 구현)  (0) 2014.05.08
Socket.IO 2/4 API 요약  (0) 2014.05.08
Socket.IO (1/4)  (0) 2014.05.08

node.js 노드가 하나가 아니라 여러개의 프로세스를 이용해서 운영할 때,socket.io를 어떻게 사용해야 할까? 이런 멀티 프로세스를 지원하기 위해서, node.js는 내부적으로 redis store를 지원한다. redis에는 publish/subscribe라는 기능이 있는데, 마치 메세지 큐처럼 메세지를 subscriber로 보낼 수 있는 기능이다.

아래 그림을 보자,하나의 node프로세스에서 메세지를 보내면, 다른 프로세스로 redis를 통해서 메세지를 전달한다. 이때 메세지를 보내는 프로세스는 redis에 메세지를 “publish”하고 나머지 프로세스들은  “subscribe”를 이용하여 메세지를 읽어드린다. 이때, 메세지를 전달하는 채널은“dispatch”라는 이름의 채널을 이용한다.



 

그러면 실제로, socket.io에서 redis store를 사용하려면 어떻게 해야 할까? 간단한 설정만으로 가능하다. 아래와 같이 redis client를 생성한 후에, socket.io에 set 명령을 이용하여 store를 redis client로만 지정해주면 된다.


var httpServer =http.createServer(app).listen(process.argv[2], function(req,res){

    console.log('Socket IO server has been started listen:'+process.argv[2]);

});

// upgrade http server to socket.io server

var io = socketio.listen(httpServer);

var pub = redis.createClient(6379,'127.0.0.1');

var sub = redis.createClient(6379,'127.0.0.1');

var store = redis.createClient(6379,'127.0.0.1');

 

io.set('store',new socketio.RedisStore({

    redis: redis

    ,redisPub : pub

    ,redisSub : sub

    ,redisClient : store

}));

 


그리고, cluster 모듈을 이용하거나, 앞단에 nginx(http:// http://nginx.org/ ) haproxy (http://haproxy.1wt.eu/)  로드밸런서를 이용하여 여러개의 node.js 프로세스에 대한 end point를 하나로 묶으면, 대규모 분산 서비스를 할 수 있는 socket.io 클러스터를 구성할 수 있다.


출처 : 조대협 (http://bcho.tistory.com)

채팅 프로그램에 (room/그룹) 기능을 추가하기

다음은 앞에서 만든 1:1 귓속말이 가능한 채팅에 “채팅방” 기능을 추가한 버전이다.

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

 

var app = express();

app.use(express.bodyParser());

app.use(express.cookieParser('your secret here'));

app.use(express.session());

app.use(express.static(path.join(__dirname, 'public')));

app.set('views', path.join(__dirname, 'views'));

app.set('view engine''ejs');

app.use(express.favicon());

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

 

var httpServer =http.createServer(app).listen(3000, function(req,res){

    console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

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

 

var count = 0;

var rooms = [];

 

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

 

 

 

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

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

 

        socket.set('room', data.room,function() {

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

                // broad cast join message

                data = {msg: nickname + 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

 

                // broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                count++;

            });

        });

 

    });

 

    socket.on('changename',function(data){

        socket.get('room',function(err,room){

            socket.get('nickname',function(err,pre_nick) {

                var nickname = data.nickname;

                // if user changes name get previous nickname from nicknames MAP

                if (pre_nick != undefined) {

                    delete rooms[room].socket_ids[pre_nick];

                }

                rooms[room].socket_ids[nickname] = socket.id

                socket.set('nickname',nickname,function() {

                    data = {msg: pre_nick + 님이 ' + nickname + '으로 대화명을 변경하셨습니다.'};

                    io.sockets.in(room).emit('broadcast_msg', data);

 

                    // send changed user nickname lists to clients

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            });

 

        });

    });

 

 

    socket.on('disconnect',function(data){

        socket.get('room',function(err,room) {

            if(err) throw err;

            if(room != undefined

                && rooms[room] != undefined){

 

                socket.get('nickname',function(err,nickname) {

                    console.log('nickname ' + nickname + ' has been disconnected');

                    // 여기에 방을 나갔다는 메세지를 broad cast 하기

                    if (nickname != undefined) {

                        if (rooms[room].socket_ids != undefined

                            && rooms[room].socket_ids[nickname] != undefined)

                            delete rooms[room].socket_ids[nickname];

                    }// if

                    data = {msg: nickname + 님이 나가셨습니다.'};

 

                    io.sockets.in(room).emit('broadcast_msg', data);

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            }

        }); //get

    });

 

    socket.on('send_msg',function(data){

        socket.get('room',function(err,room) {

            socket.get('nickname',function(err,nickname) {

                console.log('in send msg room is ' + room);

                data.msg = nickname + ' : ' + data.msg;

                if (data.to == 'ALL') socket.broadcast.to(room).emit('broadcast_msg', data); // 자신을 제외하고 다른 클라이언트에게 보냄

                else {

                    // 귓속말

                    socket_id = rooms[room].socket_ids[data.to];

                    if (socket_id != undefined) {

 

                        data.msg = '귓속말 :' + data.msg;

                        io.sockets.socket(socket_id).emit('broadcast_msg', data);

                    }// if

                }

                socket.emit('broadcast_msg', data);

            });

        });

    })

});

코드를 살펴보자

처음에 입장은 http://localhos:3000/{방이름} 으로 하게 된다.

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

그러면 URL에 있는 방이름을 받아서, index.ejs에 있는 UI로 채팅창을 띄워주고 방이름을 parameter로 index.ejs에 넘겨준다.

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

클라이언트가 서버에 접속되면  먼저 클라이언트가 join 이벤트를 보내는데 join 이벤트를 받으면이때 같이  room 이름으로 현재 소켓을room 이름의 room join한다.

        socket.set('room', data.room,function() {

다음으로해당 소켓이 어느 룸에 있는지  set 명령을 이용하여 socket 저장해놓는다.

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

윗부분이 room 데이터 객체를 생성하는 것인데앞의 예제와는 달리현재 연결된 클라이언트의 socket.id 이제는 room 단위로 관리를 해야 한다그래서 rooms라는 객체를 이용하여해당 room 대해서 rooms.room이라는 객체로 만들고그리고 room 현재 연결된 클라이언트 socket.id 저장하는 socket_ids 객체를 생성한다.

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

그리고 나서, socket_ids 귓속말 채팅방 예제와 같이 nickname to socket.id  대한 맵핑 정보를 저장한다.

                // broad cast join message

                data = {msg: nickname + 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

// broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users:

Object.keys(rooms[room].socket_ids)});

                count++;

            });

그리고 위와 같이 현재 room 들어 있는 클라이언트들에게만 , 새로운 사용자가 입장했음을 알리고사용자 리스트를 업데이트하는 이벤트를 보낸다.

disconnect 대한 부분도 크게 달라진 것이 없다. Socket_ids 객체가 rooms 아래로 들어갔고메시지를 보낼때귓속말 채팅방 예제가io.sockets.emit 대신에, room 범위를 지정하는 in() 메서드를 써서 io.sockets.in(room).emit 같이 보내게 된다.

Sendmsg 이벤트 부분도, broadcast하는 부분에서 to를 이용하여 다음과 같이socket.broadcast.to(room).emit 특정 room에 있는 클라이언트에게만 메시지를 보내는 것으로만 변경되었다

아래는 클라이언트쪽의 코드이다. 앞의 예제에서 나온 귓속말이 가능한 대화방과 코드가 거의 동일하다. 단

<script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

처음에 접속하였을 때, 서버 코드에서 방이름을 URL로부터 읽어서, 그 방이름으로 join 하는 이벤트를 보낸다.

/vies/index.ejs

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

 

<b>Welcome ROOM : <%= room%></b><p>

    Name <input type="text" id="nickname" /> <input type="button" id="changename"value="Change name"/><br>

    To

    <select id="to">

        <option value="ALL">ALL</option>

    </select>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

 

        $('#changename').click(function(){

            socket.emit('changename',{nickname:$('#nickname').val()});

        });

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('send_msg',{to:$('#to').val(),msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

 

        // 새로운 사용자가 들어오거나사용자가 이름을 바꿨을때 "To" 리스트를 변경함

        socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

 

        socket.on('broadcast_msg',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

 

다음은 실제 실행 화면이다.



출처 : 조대협 (http://bcho.tistory.com)

'Programming > Node.js' 카테고리의 다른 글

node inspector - 노드를 디버깅하자. Let's debug node  (0) 2014.05.19
Socket.IO 클러스터링  (0) 2014.05.08
Socket.IO 3/4 (1:1 귓속말 구현)  (0) 2014.05.08
Socket.IO 2/4 API 요약  (0) 2014.05.08
Socket.IO (1/4)  (0) 2014.05.08

+ Recent posts