websocket 요청 하나 만들려고 서버를 무엇을 쓸려고 하나 고민이 된다..
netty, jetty, glassfish, grizzly, node.js 중에. 그냥 node.js로 선택해야 겠다는 생각이 든다..

자바보다는 js~ ㅎㅎ
이번 기회에 node.js도 배울 수 있겠다.


리눅스 OS(ubuntu) 에서 http://nodejs.org/#download 에 접속하고, 가장 최신인 0.4.12를 다운받는다. 


 Download

git repo

2011.09.15 v0.4.12 (stable)

node-v0.4.12.tar.gz Source Code
Documentation




 ./configure 명령을 내린다. 그러나 에러가 난다. 

 ./configure  
Checking for program g++ or c++          : not found 
Checking for program icpc                : not found 
Checking for program c++                 : not found 
/home/google/node-v0.4.12/wscript:232: error: could not configure a cxx compiler!



build-essential 패키지를 설치하니 문제가 사라진다.

sudo aptitude install build-essential 




./configure 명령을 내리면 잘 되는지 확인할 수 있다. 

$ ./configure
Checking for program g++ or c++          : /usr/bin/g++ 
Checking for program cpp                 : /usr/bin/cpp 
Checking for program ar                  : /usr/bin/ar 
Checking for program ranlib              : /usr/bin/ranlib 
Checking for g++                         : ok  
Checking for program gcc or cc           : /usr/bin/gcc 
Checking for program ar                  : /usr/bin/ar 
Checking for program ranlib              : /usr/bin/ranlib 

....
 Checking for header ['sys/types.h', 'sys/event.h'] : not found 
Checking for header sys/queue.h                    : yes 
Checking for function kqueue                       : not found 
Checking for header sys/select.h                   : yes 
Checking for function select                       : yes 
Checking for header sys/eventfd.h                  : yes 
Checking for function eventfd                      : yes 
Checking for SYS_clock_gettime                     : yes 
Checking for library rt                            : yes 
Checking for function clock_gettime                : yes 
Checking for function nanosleep                    : yes 
Checking for function ceil                         : yes 
Checking for fdatasync(2) with c++                 : yes 
'configure' finished successfully (2.194s)



컴파일을 하고, 설치를 완료한다. 

$ make
$ sudo make install



정상적으로 설치 되었는지 확인한다.  node.js 소스 예제(example)을 하나 만든다. 
hello를 표준 출력으로 나오게 하고, 2초 뒤에 world 를 출력한다.  


$ gedit helloworld.js

var sys = require("sys")
sys.puts("hello");
setTimeout(function() {
    sys.puts("world");
}, 2000);


$ node helloword.js 
hello
(2초 뒤) 
world
(종료)

$  



이번엔 tcp 서버 테스트를 진행한다.


터미널 #1
$ gedit helloworld-tcp.js

var tcp = require("net");
 
 tcp.createServer(function(c) {
     c.write("hello world!\n");
    c.end();
 }).listen(8000);


$ node helloworld-tcp.js
(서버)



터미널 #2
$ node helloworld-tcp.js
telnet localhost 8000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello world!
Connection closed by foreign host.



http 서버를 만들어 본다.

node helloworld-http.js

var http = require("http");
     
http.createServer(function(req, res) {
    res.writeHead(200, {"Content-Type": "text/plain"});
    res.write("Hello World!!\r\n");
    res.end();
}).listen(8000);

 $ node helloworld-http.js
(서버)



브라우져로 http://localhost:8000/ 로 접속하면
Hello World!! 가 출력하는 것을 볼 수 있다.


 





마지막으로 시스템 명령어도 내릴 수 있다.


$ gedit system.js

var sys = require("sys"),

spawn = require("child_process").spawn;
 
var ls = spawn("ls", ["-hls", "/"]);
ls.stdout.addListener("data", function(data) {
    sys.print(data);
});


ls -hls  / 의 결과가 출력된다.

$ node system.js 
합계 104K
4.0K drwxr-xr-x   2 root   root   4.0K 2011-10-11 10:30 bin
4.0K drwxr-xr-x   3 root   root   4.0K 2011-10-11 10:31 boot
4.0K drwxr-xr-x   3 root   root   4.0K 2011-10-13 11:35 build
4.0K drwxr-xr-x   2 root   root   4.0K 2011-10-11 10:04 cdrom
4.0K drwxrwxrwx   2 cubrid cubrid 4.0K 2011-10-13 10:35 cubridmanager
   0 drwxr-xr-x  17 root   root   3.6K 2011-10-27 19:17 dev
 12K drwxr-xr-x 138 root   root    12K 2011-10-28 18:20 etc
4.0K drwxr-xr-x   4 root   root   4.0K 2011-10-25 11:05 home
   0 lrwxrwxrwx   1 root   root     33 2011-10-11 10:31 initrd.img -> boot/initrd.img-2.6.32-34-generic
   0 lrwxrwxrwx   1 root   root     33 2011-10-11 10:06 initrd.img.old -> boot/initrd.img-2.6.32-28-generic
 12K drwxr-xr-x  20 root   root    12K 2011-10-11 10:30 lib
 16K drwx------   2 root   root    16K 2011-10-11 10:01 lost+found
4.0K drwxr-xr-x   4 root   root   4.0K 2011-10-27 10:08 media
4.0K drwxr-xr-x   2 root   root   4.0K 2010-04-23 19:11 mnt
4.0K drwxr-xr-x   5 root   root   4.0K 2011-10-13 10:35 opt
   0 dr-xr-xr-x 182 root   root      0 2011-10-27 10:07 proc
4.0K drwx------  12 root   root   4.0K 2011-10-28 18:48 root
4.0K drwxr-xr-x   2 root   root   4.0K 2011-10-11 10:37 sbin
4.0K drwxr-xr-x   2 root   root   4.0K 2009-12-06 06:55 selinux
4.0K drwxr-xr-x   2 root   root   4.0K 2011-02-11 22:04 srv
   0 drwxr-xr-x  12 root   root      0 2011-10-27 10:07 sys
4.0K drwxrwxrwt  15 root   root   4.0K 2011-10-28 19:04 tmp
4.0K drwxr-xr-x  11 root   root   4.0K 2011-10-11 15:05 usr
4.0K drwxr-xr-x  16 root   root   4.0K 2011-10-11 12:05 var
   0 lrwxrwxrwx   1 root   root     30 2011-10-11 10:31 vmlinuz -> boot/vmlinuz-2.6.32-34-generic
   0 lrwxrwxrwx   1 root   root     30 2011-10-11 10:06 vmlinuz.old -> boot/vmlinuz-2.6.32-28-generic




websocket을 테스트해본다.


서버 코드
간단하게 테스트할려고. socket.io를 설치 없이 node.js 단에서 한 테스트.. 


websocket.js

var sys = require('sys'), ws = require('./ws');

ws.createServer(function (socket) {
socket.addListener("connect", function (resource) {
      sys.debug("connect: " + resource);
//socket.write("welcome WebSocket Server\r\n");
setTimeout(socket.end, 100 * 1000); 

}).addListener("data", function (data) {
console.info("read data: " + data);
//socket.write(data);

}).addListener("close", function () {
sys.puts("client left");
})

}).listen(8000);


 
ws.js  (https://github.com/ncr/node.ws.js 에서 조금 수정한 버전) 
이 내용을 잘 살피면서 websocket handshake에 대한 감을 잡을 수 있었음..

 // Github: http://github.com/ncr/node.ws.js
// Compatible with node v0.1.91
// Author: Jacek Becela
// Contributors:
//   Michael Stillwell  http://github.com/ithinkihaveacat
//   Nick Chapman       http://github.com/nchapman
//   Dmitriy Shalashov  http://github.com/skaurus
//   Johan Dahlberg
//   Andreas Kompanez
//   Samuel Cyprian http://github.com/samcyp
// License: MIT
// Based on: http://github.com/Guille/node.websocket.js

function nano(template, data) {
  return template.replace(/\{([\w\.]*)}/g, function (str, key) {
    var keys = key.split("."), value = data[keys.shift()];
    keys.forEach(function (key) { value = value[key];});
    return value;
  });
}

function pack(num) {
  var result = '';
  result += String.fromCharCode(num >> 24 & 0xFF);
  result += String.fromCharCode(num >> 16 & 0xFF);
  result += String.fromCharCode(num >> 8 & 0xFF);
  result += String.fromCharCode(num & 0xFF);
  return result;
}

var sys  = require("sys"),
  net    = require("net"),
  crypto = require("crypto"),
  requiredHeaders = {
    'get': /^GET (\/[^\s]*)/,
    'upgrade': /^websocket$/,
    'connection': /^Upgrade$/,
    'host': /^(.+)$/,
    'origin': /^(.+)$/
  },
  handshakeTemplate75 = [
    'HTTP/1.1 101 Web Socket Protocol Handshake', 
    'Upgrade: websocket', 
    'Connection: Upgrade',
    'WebSocket-Origin: {origin}',
    'WebSocket-Location: {protocol}://{host}{resource}',
    '',
    ''
  ].join("\r\n"),
  handshakeTemplate76 = [
    'HTTP/1.1 101 WebSocket Protocol Handshake', // note a diff here
    'Upgrade: websocket',
    'Connection: Upgrade',
    'Sec-WebSocket-Origin: {origin}',
    'Sec-WebSocket-Location: {protocol}://{host}{resource}',
    '',
    '{data}'
  ].join("\r\n"),
  flashPolicy = '<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>';



exports.createSecureServer = function (websocketListener, credentials, options) {
if (!options) options = {};
options.secure = credentials;
return this.createServer(websocketListener, options);
};

exports.createServer = function (websocketListener, options) {
  if (!options) options = {};
  if (!options.flashPolicy) options.flashPolicy = flashPolicy;
  // The value should be a crypto credentials
  if (!options.secure) options.secure = null;

  return net.createServer(function (socket) {
//Secure WebSockets
var wsProtocol = 'ws';
if(options.secure) {
 wsProtocol = 'wss';
 socket.setSecure(options.secure);
}

console.info("1");

    socket.setTimeout(0);
    socket.setNoDelay(true);
    socket.setKeepAlive(true, 0);

console.info("2");

    var emitter = new process.EventEmitter(),
      handshaked = false,
      buffer = "";
      
console.info("3");
    function handle(data) {
      buffer += data;
      
console.info("4");
      var chunks = buffer.split("\ufffd"),
        count = chunks.length - 1; // last is "" or a partial packet
        
console.info("5");
      for(var i = 0; i < count; i++) {
        var chunk = chunks[i];
        if(chunk[0] == "\u0000") {
console.info("6");
          emitter.emit("data", chunk.slice(1));
        } else {
console.info("7");
          socket.end();
          return;
        }
      }
      

      buffer = chunks[count];
console.info("6");
    }

    function handshake(data) {
console.info("handshake");
      var _headers = data.split("\r\n");

      if ( /<policy-file-request.*>/.exec(_headers[0]) ) {
        socket.write( options.flashPolicy );
        socket.end();
console.info("handshake-1");
        return;
      }

      // go to more convenient hash form
      var headers = {}, upgradeHead, len = _headers.length;
      if ( _headers[0].match(/^GET /) ) {
        headers["get"] = _headers[0];
      } else {
console.info("handshake-2");
        socket.end();
        return;
      }

console.info("handshake-3");
      if ( _headers[ _headers.length - 1 ] ) {
        upgradeHead = _headers[ _headers.length - 1 ];
        len--;
      }
      while (--len) { // _headers[0] will be skipped
        var header = _headers[len];
        if (!header) continue;

        var split = header.split(": ", 2); // second parameter actually seems to not work in node
        headers[ split[0].toLowerCase() ] = split[1];
      }
console.info("handshake-4");

      // check if we have all needed headers and fetch data from them
      var data = {}, match;
      for (var header in requiredHeaders) {



        //           regexp                          actual header value
// modified
console.info("handshake-5 header param - [" + header + "] : " + headers[header] );

        if ( match = requiredHeaders[ header ].exec( headers[header] ) ) {
          data[header] = match;
        } else {
console.info("handshake-5 end.. requiredHeader");
          socket.end();
          return;
        }
      }

console.info("handshake-6");

      // draft auto-sensing
      if ( headers["sec-websocket-key1"] && headers["sec-websocket-key2"] && upgradeHead ) { // 76
console.info("handshake-7");
        var strkey1 = headers["sec-websocket-key1"]
          , strkey2 = headers["sec-websocket-key2"]

          , numkey1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10)
          , numkey2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10)

          , spaces1 = strkey1.replace(/[^\ ]/g, "").length
          , spaces2 = strkey2.replace(/[^\ ]/g, "").length;

        if (spaces1 == 0 || spaces2 == 0 || numkey1 % spaces1 != 0 || numkey2 % spaces2 != 0) {
console.info("handshake-7 end");
          socket.end();
          return;
        }

        var hash = crypto.createHash("md5")
        , key1 = pack(parseInt(numkey1/spaces1))
        , key2 = pack(parseInt(numkey2/spaces2));
        
        hash.update(key1);
        hash.update(key2);
        hash.update(upgradeHead);

        socket.write(nano(handshakeTemplate76, {
          protocol: wsProtocol,
// modified
          resource: data.get[1],
          host:     data.host[1],
          origin:   data.origin[1],
          data:     hash.digest("binary")
        }), "binary");

      } else { // 75
console.info("handshake-8");
        socket.write(nano(handshakeTemplate75, {
          protocol: wsProtocol,
//modified
          resource: data.get[1],
          host:     data.host[1],
          origin:   data.origin[1]
        }));

      }

      handshaked = true;
      emitter.emit("connect", data.get[1]);
console.info("handshake-9");
    }

    socket.addListener("data", function (data) {
      if(handshaked) {
        handle(data.toString("utf8"));
      } else {
        handshake(data.toString("binary")); // because of draft76 handshakes
                }
    }).addListener("end", function () {
console.info("handshake-9 end..");
      socket.end();
    }).addListener("close", function () {
      if (handshaked) { // don't emit close from policy-requests
        emitter.emit("close");
      }
    }).addListener("error", function (exception) {
      if (emitter.listeners("error").length > 0) {
        emitter.emit("error", exception);
      } else {
console.info("exception");
        throw exception;
                }
    });


   console.info("9");
    emitter.remoteAddress = socket.remoteAddress;
    
    emitter.write = function (data) {
      try {
        socket.write('\u00', 'binary');
        socket.write(data, 'utf8');
        socket.write('\uff', 'binary');
      } catch(e) { 
   console.info("socket error -9");
        // Socket not open for writing, 
        // should get "close" event just before.
        socket.end();
      }
    };
    
   console.info("10");
    emitter.end = function () {
 console.info("emitter.end");
      socket.end();
    };
    
    websocketListener(emitter); // emits: "connect", "data", "close", provides: write(data), end()
  });
};





8000번 포트로 접속하고  아래 http 명령어를 날려봄

 GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com:8080
Origin: http://www.example.com 

정상적으로 잘 나온다. 

$ telnet localhost 8000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com:8080
Origin: http://www.example.comHTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: websocket
Connection: Upgrade
WebSocket-Origin: undefined
WebSocket-Location: ws://example.com:8080/

(커넥션 연결된 상태)




클라이언트 (브라주져. ws.html) 코드


<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">

function WebSocketTest() {

  if ("WebSocket" in window) {

     alert("WebSocket is supported by your Browser!");
     
     var socket = new WebSocket("ws://localhost:8000");

     socket.onopen = function() {
        //ws.send("hello world");
        alert("Message is sent...");
            };



     socket.onmessage = function (evt) { 
        var received_msg = evt.data;
        alert("Message is received..." + received_msg);
            };

socket.onerror = function (error) {
  alert("error.." + error);
};


     socket.onclose = function() { 
        alert("Connection is closed..."); 
           };



 } else {
     alert("WebSocket NOT supported by your Browser!");
   }
}


</script>
</head>
<body>
<div id="sse">
   <a href="javascript:WebSocketTest()">Run WebSocket1</a>
</div>

</body>
</html>



내가 가지고 있는 크롬 웹 브라우져에서는 connection을 맺자마자 바로 fin 패킷을 보내면서 connection을 종료한다. 어딘가 약간 잘못된 것 같기는 하지만, 데모용으로만 쓸 거라서, 문제 원인을 자세히 파악하는 것은 패쓰.. 맛만 봤다는 점에서 통과해야겠음.

와이어샤크 정보





다음에는 websocket 원리를 봐야지..








<부록>

node.js를 사용한 이유는 머 간단하 websocket 테스트하려고 한것 이었는데. 쓰면서 문제점들이 좀 발견되었다. 

* node.js 의 문제점 

#1. 버그 하나로 시스템에 영향을 줄 수 있음

아래 코드에서 response 객체에 대한 end() function이 호출되지 않았을 때, 치명적으로 문제가 생긴다.
하나의 요청에서 end가 안되었기 때문에 모든 요청은 blocking된 요청이 끝나기를 기다려야 한다. 
버그 하나에 치명적임..

var http = require("http");
     
http.createServer(function(req, res) {
    res.writeHead(200, {"Content-Type": "text/plain"});
    res.write("Hello World\r\n");

    //res.end();
}).listen(8000);




#2 피보나치 이슈 (node.js에 재귀적 메소드에 문제가 있는 것 같다. )

 
재귀적 함수(피보나치)에 대해서 속도 이슈가 있다. 

var http = require("http");

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end(fibonacci(30).toString());
}).listen(8000);

function fibonacci(n) {
  if (n < 2)
    return 1;
  else
    return fibonacci(n-2) + fibonacci(n-1);
}



결과값은 아래과 같이 확인한다. 

$ time curl http://localhost:8000



10에 대한 피보나치 수열의 값을 구했을때는 0.007초가 걸린다. 
  res.end(fibonacci(10).toString());

$ time curl http://localhost:8000
10946
real 0m0.007s
user 0m0.000s
sys 0m0.004s




30에 대한 피보나치 수열의 값을 구했을때는 0.053초가 걸린다. 
  res.end(fibonacci(30).toString());

 $ time curl http://localhost:8000
1346269
real 0m0.053s
user 0m0.004s
sys 0m0.000s



아래와 같이 40의 피보나치 수열의 값을 구했을때는 4초 이상이 걸린다. 

  res.end(fibonacci(40).toString());
 

$ time curl http://localhost:8000
165580141
real 0m4.312s
user 0m0.008s
sys 0m0.000s



100의 피보나치 수열의 값을 구할때는 상상을 초월한다. 

  res.end(fibonacci(100).toString());
 

$ time curl http://localhost:8000

30분동안 아무런 응답이 없어서, 그냥 종료시켰다. 



cpu는 최대한 활용하고 있다. 엔진의 이슈일까?? 



 #3. 코드 관리
자바스크립트 특성상, 코드 관리를 잘 해야 한다. BO가 들어가는 순간부터 복잡해질 것 같다. ...
















'scribbling' 카테고리의 다른 글

websocket #4 (웹소켓의 한계)  (1) 2011.11.02
websocket #3  (0) 2011.10.31
websocket #1  (0) 2011.10.28
Comet에 대한 일반론  (0) 2011.10.28
글쓰기 전략  (0) 2011.10.27
Posted by '김용환'
,