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초 뒤)
(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
$ 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()
});
};
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된 요청이 끝나기를 기다려야 한다.
버그 하나에 치명적임..
#2 피보나치 이슈 (node.js에 재귀적 메소드에 문제가 있는 것 같다. )
결과값은 아래과 같이 확인한다.
10에 대한 피보나치 수열의 값을 구했을때는 0.007초가 걸린다.
30에 대한 피보나치 수열의 값을 구했을때는 0.053초가 걸린다.
res.end(fibonacci(30).toString());
아래와 같이 40의 피보나치 수열의 값을 구했을때는 4초 이상이 걸린다.
res.end(fibonacci(40).toString());
* 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());
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 |