이전 포스트에서 올린 서버에서 WebSocket 요청을 처리하는 부분에 이어서 이번에는 "패러디 웨이브" 서비스를 제공하는 부분이다. 정리가 제대로 안되서 코드가 보기에 좀 너접하지만, 이것 역시 참고 자료 삼아서 올린다.

ParodyWave.js
var ParodyWave = {
    totalcount: 0,
    clients: [],
    status: function () {
        var response = {type: 'status',
            connections: ParodyWave.clients.length};

        return JSON.stringify(response);
    },
    list: function () {
        var response = {type: 'list'}
        response.threads = ParodyWave.threads.list();

        return JSON.stringify(response);
    },
    newThread: function (title, conn) {
        var response = {type: 'join'};
        if (conn.activity.thread) {
            response = {type: 'error', text: 'already joined.'}
        } else {
            response.thread = conn.activity.thread = ParodyWave.threads.add(title);
        }
        return JSON.stringify(response);
    },
    joinThread: function (id, conn) {
        var response = {type: 'join'};
        if (conn.activity.thread) {
            response = {type: 'error', text: 'already joined.'}
        } else if (ParodyWave.threads.join(id)) {
            response.thread = conn.activity.thread = id;
            response.title = ParodyWave.threads._list[id].title;
            response.data = ParodyWave.threads._list[id]._data;
        } else {
            response = {type: 'warning', text: 'cannot joined'};
        }
        return JSON.stringify(response);
    },
    exit: function (msg, conn) {
        ParodyWave.threads.exit(msg.id);
        conn.activity.thread = null;
    },
    newLine: function (conn) {
        var l = ParodyWave.threads._list[conn.activity.thread].append(conn.activity.id, conn.activity.user);
        conn.activity.line = l;
        return JSON.stringify({type: "update", id: conn.activity.id, line: l, user: conn.activity.user});
    },
    update: function (v, conn) {
        ParodyWave.threads._list[conn.activity.thread].update(conn.activity.line, v);
        return JSON.stringify({type: "update", id: conn.activity.id, line: conn.activity.line, value: v});
    },
    submit: function (conn) {
        conn.activity.line = null;
    },
    notify: function (msg, conn, area) {
        for (var i = 0 ; i < ParodyWave.clients.length ; i ++) {
            var cond;
            switch (area) {
                case -1: // all, exclude same thread
                    cond = (conn.activity.thread !== ParodyWave.clients[i].activity.thread);
                    break;
                case 1: // same thread
                    cond = (conn.activity.thread === ParodyWave.clients[i].activity.thread);
                    break;
                case -2: // all, not in a thread
                    cond = (ParodyWave.clients[i].activity.thread === null);
                    break;
                case 2: // all in a thread
                    cond = (ParodyWave.clients[i].activity.thread !== null);
                    break;
                default:    // all, exclude me
                    cond = (conn !== ParodyWave.clients[i]);
                    break;
            }
            if (cond) ParodyWave.clients[i].write(msg);
        }
    }
};
ParodyWave.threads = {
    _list:[],
    count: 0,
    add: function (title) {
        var th = {}
        th.title = title;
        th.count = 1;
        th._data = [];
        th.append = function (id, user) {
            return th._data.push([id, user, ""]) - 1;
        }
        th.update = function (line, value) {
            th._data[line][2] = value;
        }
        th.get = function (line) {
            return th._data[line][2];
        }
        ParodyWave.threads.count ++;
        return ParodyWave.threads._list.push(th) - 1;
    },
    join: function (id) {
        if (ParodyWave.threads._list[id]) {
            ParodyWave.threads._list[id].count ++;
            return true;
        } else {
            return false;
        }
    },
    exit: function (id) {
        if (ParodyWave.threads._list[id] && -- ParodyWave.threads._list[id].count <= 0) {
            ParodyWave.threads._list[id] = null;
            ParodyWave.threads.count --;
        }
        ParodyWave.notify(ParodyWave.list());
    },
    list: function () {
        var l = [];
        for (var i = 0 ; i < ParodyWave.threads._list.length ; i++) {
            if (ParodyWave.threads._list[i] == null) continue;
            l.push([i, ParodyWave.threads._list[i].title, ParodyWave.threads._list[i].count]);
        }
        return l;
    }
};

exports.service = function(socket) {
    socket.on("open", onOpen);
    socket.on("message", onMessage);
    socket.on("close", onClose);
    socket.on("error", onError);

    function onOpen() {
        socket.activity = {id: null, user: null, thread: null, line: null};
        ParodyWave.clients.push(socket);
        socket.activity.id = ++ParodyWave.totalcount;
        ParodyWave.notify(ParodyWave.status(), socket);
    }

    function onMessage(data) {
        var msg;
        try {
            msg = JSON.parse(data);
        } catch (e) {
            if (data != 'HB') { // HeartBeat
                console.log(e);
                console.log("DATA: " + data);
            }
            return;
        }
        console.log(msg);   // Debug
        switch (msg.cmd) {
            case "user":
                break;
            case "status":
                socket.write(ParodyWave.status(msg));
                socket.write(ParodyWave.list());
                break;
            case "open":
                socket.activity.user = msg.user;
                socket.write(ParodyWave.newThread(msg.title, socket));
                ParodyWave.notify(ParodyWave.list(), socket, -2);
                break;
            case "join":
                socket.activity.user = msg.user;
                socket.write(ParodyWave.joinThread(msg.id, socket));
                ParodyWave.notify(ParodyWave.list(), socket, -2);
                break;
            case "exit":
                ParodyWave.exit(msg, socket);
                break;
            case "new":
                ParodyWave.notify(ParodyWave.newLine(socket), socket, 1);
                if (msg.value) ParodyWave.notify(ParodyWave.update(msg.value, socket), socket, 1);
                break;
            case "update":
                ParodyWave.notify(ParodyWave.update(msg.value, socket), socket, 1);
                break;
            case "submit":
                if (msg.value) ParodyWave.notify(ParodyWave.update(msg.value, socket), socket, 1);
                ParodyWave.submit(socket);
                break;
        }
    }

    function onClose() {
        for (var i = 0 ; i < ParodyWave.clients.length ; i ++) {
            if (socket === ParodyWave.clients[i]) {
                ParodyWave.clients.splice(i, 1);
            }
        }
        var idx = socket.activity.thread;
        ParodyWave.threads.exit(idx);
        ParodyWave.notify(ParodyWave.status(), socket);
    }

    function onError(exception) {
        console.log("ERROR: " + exception);
        for (var i = 0 ; i < ParodyWave.clients.length ; i ++) {
            if (socket === ParodyWave.clients[i]) {
                ParodyWave.clients.splice(i, 1);
            }
        }
    }
};

124~127줄의 부분을 보면 open, message, close, error라는 네가지의 이벤트에 대한 핸들러를 지정하는 부분이 있다. 원래 Node.js에서 WebSocket 접속이 이루어진다고 해도 HTML5 표준에 있는 WebSocket API에 정의된 이런 이벤트가 발생하지는 않지만, 서비스 구현시의 편리를 위해서 클라이언트와 비슷하게 임의로 저 이벤트를 발생하도록 WebSocket 요청 처리부분에서 구현했다. 따라서, 이전 포스트의 프로그램을 사용한다면, 서비스 부분에서는 HTML5에서 사용하는 것처럼 저 네가지 이벤트와 write(), end() 등의 메소드를 사용하면 된다.

지난 주에 'WebSocket & Node.js' 라는 주제로 발표하면서 예제로 준비했던 WebSocket을 사용한 간단한 채팅 프로그램을 여기에 올려보려고한다. 이 프로그램은 Google의 Wave서비스에서 아이디어를 얻어서, 훨씬 적은 부하로 Wave와 비슷한 기능을 구현할 수 있다는 것을 보여주려고 만들었기에 "패러디 웨이브"라고 이름을 지었는데, 어쩌다보니 Wave와는 전혀 다른 모습이 되었다.



사용해보신 분들의 요구 사항도 좀 적용하고 코드를 좀 더 정리해서, 실제로 사용할만한 모양을 좀 만든 뒤에 공개할 생각이었는데, 웹브라우저의 WebSocket 지원에 대한 이야기도 있고해서 정리한다고 시간 보내는 것보다는 기록 삼아서라도 되도록 빨리 공개하는 것이 좋다는 판단에서 그냥 공개하게 됐다.(필요한 사람이 있을까 싶긴 하지만...)

발표에서 이야기했지만, "패러디 웨이브"는 다음과 같이 구성되어 있다.

  • 클라이언트 : ParodyWave.html
  • 서버
    • 웹 소켓 요청 처리 : WebSocket.js
    • "패러디 웨이브" 서비스 : ParodyWave.js

위에서 볼수 있는 것처럼, 프로그램은 서버와 클라이언트로 되어 있고, 서버는 웹소켓 요청을 처리하는 부분과 실제 채팅 서비스를 제공하기 위한 부분으로 나뉘어있다. 각각 파일로 나뉘어 총 3개 파일로 구성되어 있는데, 한 부분씩 세차례에 걸쳐서 여기에 포스트할 예정이다. 오늘은 그 첫번째 부분으로 Node.js로 구현한 웹 소켓 요청을 처리하는 서버 프로그램을 올린다.

웹 소켓 요청 처리 부분은 간단하게나마 HTTP요청도 처리할 수 있도록 되어있고, 웹 소켓 요청의 경우에는 조금만 수정하면 요청 URL에 따라서 여러 서비스도 제공할 수도 있다. 어디까지나 예제 수준의 코드이므로 Node.js를 이용하면 현재의 웹브라우저에서 지원하는 WebSocket을 이런식으로 구현할 수 있다는 참고자료 정도로 보기 바란다.

WebSocketServer.js
var WebSocketServer = function (options) {
    require('http').Server.call(this);
    
    this.on('connection', onConnect);
    this.on('request', onRequest);  // HTTP Request Handler
    this.on('upgrade', onUpgrade);  // WebSocket Request Handler
    
    this.WebSocketResponse75 = [
        'HTTP/1.1 101 Web Socket Protocol Handshake', 
        'Upgrade: WebSocket', 
        'Connection: Upgrade',
        'WebSocket-Origin: {origin}',
        'WebSocket-Location: {protocol}://{host}{resource}',
        '',
        ''
    ]
    this.WebSocketResponse76 = [
        'HTTP/1.1 101 Web Socket Protocol Handshake', 
        'Upgrade: WebSocket', 
        'Connection: Upgrade',
        'Sec-WebSocket-Origin: {origin}',
        'Sec-WebSocket-Location: {protocol}://{host}{resource}',
        '',
        '{data}'
    ]
    
    function onConnect() {
    }
    
    function onRequest(req, res) {
        var fileName = '.'
        if (req.url == '/') fileName += '/ParodyWave.html';
        else fileName += req.url;
        require('fs').readFile(fileName, function (err, data) {
            if (err) {
                res.writeHead(404);
                res.end();
                console.log(fileName);
                return;
            } else {
                res.writeHead(200, {'Content-Length': data.length});
                res.write(data);
                res.end();
            }
        });
    }

    function onUpgrade(req, socket, head) {
        if ("sec-websocket-key1", "sec-websocket-key2" in req.headers) { // 76
            var key = calcResponseKey(req.headers["sec-websocket-key1"],
                                    req.headers["sec-websocket-key2"], head);
            if (key) {
                var res = socket.server.WebSocketResponse76.join('rn')
                            .replace(/{origin}/,req.headers.origin || '')
                            .replace(/{protocol}/,'ws')
                            .replace(/{host}/,req.headers.host || '')
                            .replace(/{resource}/,req.url || '')
                            .replace(/{data}/,key);
            } else {
                socket.end();
                return;
            }
        } else {    // 75
            var res = socket.server.WebSocketResponse75.join('rn')
                        .replace(/{origin}/,req.headers.origin || '')
                        .replace(/{protocol}/,'ws')
                        .replace(/{host}/,req.headers.host || '')
                        .replace(/{resource}/,req.url || '')
        }
        
        try {
            socket.write(res, "binary");
            var client = initiateWebSocket(socket);
            
            var action = require('./ParodyWave');
            action.service(client);
            client.ready(); // "open" event for action
            setTimeout(socket.end, 300 * 1000);
        } catch (e) {
            socket.destroy();
        }
    }

    function calcResponseKey(key1, key2, key) {
        var md5 = require('crypto').createHash('md5');
        [key1, key2].forEach(function (k) {
            var n = parseInt(k.replace(/[^d]/g,'')),
                space = k.replace(/[^ ]/g,'').length;
            
            if (space === 0 || n % space !== 0) {
                return null;
            }
            
            n = parseInt(n/space);
            var result = '';
            result += String.fromCharCode(n >> 24 & 0xFF);
            result += String.fromCharCode(n >> 16 & 0xFF);
            result += String.fromCharCode(n >> 8 & 0xFF);
            result += String.fromCharCode(n & 0xFF);
            
            md5.update(result);
        });
        md5.update(key);
        return md5.digest('binary');
    }
    
    function initiateWebSocket(socket) {
        var c = new process.EventEmitter();
        socket.on("data", onData);
        socket.on("end", function () { socket.end(); });
        socket.on("close", onClose);
        socket.on("error", onError);
        socket.wsBuffer = "";
        
        // WebSocket readyState status
        // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3:CLOSED
        c.readyState = 0;
        c.ready = function() {
            if (c.readyState === 0) {
                c.readyState = 1;
                c.emit("open");
            }
        }
        c.write = function (data) {
          try {
            socket.write("u0000", "binary");
            socket.write(data, "utf8");
            socket.write("uffff", "binary");
          } catch(e) {
            socket.end();
          }
        };
        
        c.end = function () {
            socket.end();
            c.readyState = 2;
        };
        
        return c;
        
        function onData(data) {
            socket.wsBuffer += data;
            
            var chunks = socket.wsBuffer.split("ufffd"),
                count = chunks.length - 1;
            
            for (var i = 0 ; i < count ; i ++) {
                var chunk = chunks[i];
                if (chunk[0] == "u0000") {
                    c.emit("message", chunk.slice(1));
                } else {
                    socket.end();
                    return;
                }
            }
            
            socket.wsBuffer = chunks[count];
        }
        
        function onClose() {
            c.emit("close");
            c.readyState = 2;
        }
        
        function onError(exception) {
            if (c.listeners("error").length > 0) {
                c.emit("error", exception);
            } else {
                throw exception;
            }
        }
    }
};
require("sys").inherits(WebSocketServer, require("http").Server);

var server = new WebSocketServer();
server.listen(8888);

간단한 설명을 덧붙여보자면, 5~6줄에서 HTTP 요청과 WebSocket 요청시에 발생하는 Request 이벤트와 Upgrade 이벤트에 대한 핸들러를 등록한다. 엄연히 구분하자면, Upgrade 이벤트 발생시에 이것이 웹소켓 접속으로 프로토콜을 변경할 것인지 구분하는 과정이 있어야겠지만, 생략했다.

30줄부터 시작되는 onRequest 메소드가 HTTP 요청을 처리하는 부분인데, 웹서버 기능을 보강하려면 여길 고치면 되겠다. 현재는 요청한 url에 아무런 처리도 하지않고 그대로 현재 디렉토리에서 해당 파일을 찾아서 보내준다. 파일이 없을 경우에만 404 에러로 처리한다.

45줄부터 나오는 onUpgrade 메소드는 WebSocket 요청을 처리하는 부분으로 HandShake를 옛 방식과 새 방식을 모두 지원하게 해놓았고, 이 접속을 "패러디 웨이브" 서비스로 연결해주는 기능을 한다. 75줄을 적당히 고치면 여러가지 서비스를 요청 경로에 따라서 제공하는 것도 가능할 것이다.

PHP에서 MYSQL을 연결할때 사용하는 것이 mysql_connect() 함수인데, 하나의 세션/파일에서 여러개의 연결이 필요할때는 이 함수의 네번째 매개변수인 $new_link를 True로 설정해줘야한다. 그렇지 않으면 기존의 연결을 계속 재사용하게 되므로 의도와는 다르게 동작한다.

참고 : php.net


/*
ID: ****
LANG: C
TASK: dualpal
*/

/*#define __DEBUG__*/
#define TRUE 1
#define FALSE 0
#define MAX 100

#include 

int isDualPalindrome(int num);
int checkPalindrome(int num[], int pos);

int main()
{
	int N;
	int S;
	int i;
	int count = 0;

#ifndef __DEBUG__
	FILE *fin;
	FILE *fout;

	fin = fopen("dualpal.in","r");
	fout = fopen("dualpal.out","w");
	fscanf(fin, "%d", &N);
	fscanf(fin, "%d", &S);
#else
	printf("how many do you want to get palindromes : ");
	scanf("%d", &N);
	printf("what number do you want to palindromes that greater than? : ");
	scanf("%d", &S);
#endif

	for (i = S + 1 ; ; i++)
	{
		if (isDualPalindrome(i) == TRUE)
		{
#ifndef __DEBUG__
			fprintf(fout, "%dn", i);
#else
			printf("%dn", i);
#endif
			count++;
		}
		if (count == N) break;
	}

#ifndef __DEBUG__
	fclose(fin);
	fclose(fout);
#endif

	return 0;
}

int isDualPalindrome(int num)
{
	int i;
	int changedNum[MAX];
	int pos;
	int temp;
	int palCount = 0;
	
	for (i = 2 ; i < = 10 ; i++)
	{
		pos = 0;
		temp = num;

		do
		{
			changedNum[pos++] = temp % i;
		} while(temp = temp / i);

		if (checkPalindrome(changedNum, pos) == TRUE)
		{
			palCount++;
			if (palCount >= 2)
			{
				return TRUE;
			}
		}
	}
	
	return FALSE;
}

int checkPalindrome(int num[], int pos)
{
	int i;

	for (i = 0 ; i < = pos / 2 ; i++)
	{
		if (num[i] != num[pos-i-1])
		{
			return FALSE;
		}
	}

	return TRUE;
}

주어진 수보다 큰 수 중에서 2~10진수로 표현했을때 두번 이상 palindrome(회문) 이 되는 수를 구하는 문제였다. 상당히 어렵게 생각하고 있었는데 실제로 해보니 의외로 쉽게 풀린 문제다.

'Developing' 카테고리의 다른 글

PHP에서 MySQL 다중 연결시 조심할 것  (0) 2008.12.30
USACO Training - Milking Cows  (0) 2007.11.24
파이썬 문자열 뒤집기  (0) 2007.11.17
/*
ID: ****
LANG: C
TASK: milk2
*/

/*#define __DEBUG__*/
#define MAX 1000000

#include 

int main()
{
	int N;
	int start, end;
	int **time;
	int count = 0;
	int modified = 0;
	int i,j;
	int milkTime = 0;
	int restTime = 0;
	int temp;

#ifndef __DEBUG__
	FILE *fin;
	FILE *fout;

	fin = fopen("milk2.in","r");
	fout = fopen("milk2.out","w");
	fscanf(fin, "%d", &N);
#else
	printf("input the number of farmers : ");
	scanf("%d", &N);
#endif

	time = (int**)malloc(sizeof(int*)*N);

	for(i=0 ; i < N ; i++)
	{
		time[i] = (int*)malloc(sizeof(int)*2);
#ifndef __DEBUG__
		fscanf(fin, "%d %d", &start, &end);
#else
		printf("input start time and end time.n[start time] [end time]n");
		scanf("%d %d", &start, &end);
#endif
		for (j=0 ; j < count ; j++)
		{
			if (start <= time[j][1] && end >= time[j][1])
			{
				time[j][1] = end;
				modified = 1;
			}
			if (end >= time[j][0] && start < = time[j][0])
			{
				time[j][0] = start;
				modified = 1;
			}
		}

		if (modified == 0)
		{
			time[j][0] = start;
			time[j][1] = end;
			count ++;
		}
		modified = 0;
	}

	for (i=0 ; i < count ; i++)
	{
		for (j=0 ; j < count ; j++)
		{
			if (time[i][0] <= time[j][1] && time[i][1] >= time[j][1])
			{
				time[j][1] = time[i][1];
			}
			if (time[i][1] >= time[j][0] && time[i][0] < = time[j][0])
			{
				time[j][0] = time[i][0];
			}
		}
	}

	for (i=0 ; i < count ; i++)
	{
		temp = time[i][1] - time[i][0];
		if (temp > milkTime)
		{
			milkTime = temp;
		}
	}

	for (i=0 ; i < count ; i++)
	{
		temp = MAX;
		for (j=0 ; j < count ; j++)
		{
			if (time[j][0] > time[i][1] && temp > time[j][0])
			{
				temp = time[j][0];
			}
		}

		if (temp == MAX)
		{
			continue;
		}

		temp = temp - time[i][1];

		if (temp > restTime)
		{
			restTime = temp;
		}
	}

#ifndef __DEBUG__
	fprintf(fout, "%d %dn", milkTime, restTime);
	fclose(fin);
	fclose(fout);
#else
	printf("the longest continuous time of milking : %dnthe longest idle time : %dn", milkTime, restTime);
#endif
	return 0;
}

매번 뻔한 코딩만하다보니 문제해결능력이 많이 떨어졌다는 것을 느낀다. 이번 문제는 여러 젖소가 우유를 짜는 시각이 주어지고, 최소한 한 젖소에서 우유를 짜는 가장 긴 시간과 전혀 우유를 짜지 않는 가장 긴 시간을 구하는 문제였다.

최소한 한 젖소에서 우유를 짜는 가장 긴 시간은 금새 구할 수 있었지만, 우유를 짜지 않는 가장 긴 시간을 구하는 것은 쉽게 나오지 않았다.

'Developing' 카테고리의 다른 글

USACO Training - Dual Plindromes  (0) 2007.12.15
파이썬 문자열 뒤집기  (0) 2007.11.17
이클립스에서 EUC-KR 문서 이용  (0) 2007.11.08
s[::-1]

저걸로 끝!

'Developing' 카테고리의 다른 글

USACO Training - Milking Cows  (0) 2007.11.24
이클립스에서 EUC-KR 문서 이용  (0) 2007.11.08
Visual C++ 프로젝트 정리하기  (0) 2007.05.15

이클립스에서 EUC-KR 문서를 사용하기 위해서는 다음과 같이 하면된다.

  1. 파일을 오른쪽 클릭
  2. Properties 선택
  3. Resource - Text file encoding 에서 Other 선택
  4. EUC_KR 을 써넣는다.

'Developing' 카테고리의 다른 글

파이썬 문자열 뒤집기  (0) 2007.11.17
Visual C++ 프로젝트 정리하기  (0) 2007.05.15
작성일자가 4월 1일인 RFC  (0) 2007.04.14
보관할 파일들
  • dsw, dsp 파일은 프로젝트 파일로 꼭 있어야하는 파일이다.
  • res 폴더는 리소스 폴더로 꼭 있어야한다.
  • 기타 소스파일 & 헤더파일


삭제할 파일들
  • plg, ncb, opt, clw 파일은 그때 그때마다 생성되는 파일이므로 삭제해준다.
  • Debug, Release 폴더는 빌드 결과물이 들어간다.

'Developing' 카테고리의 다른 글

이클립스에서 EUC-KR 문서 이용  (0) 2007.11.08
작성일자가 4월 1일인 RFC  (0) 2007.04.14
파이의 정의  (0) 2007.04.03

IETF(Internet Engineering Task Force,국제인터넷표준화기구)에서 나온 RFC중 4월 1일에 작성된 문서는 유심히 볼 필요가 있다. 제목만 봐도 좀 이상하지만(예를 들면 SONET to Sonnet Translation이라든지...), 농담조의 내용을 진지하게 적어놓은 센스가 대단하다. 특히, RFC 1149의 경우에는 실제로 구현해본 사람도 있다고 한다.

이런 만우절 RFC는 위키백과에 잘 나와있다.

'Developing' 카테고리의 다른 글

Visual C++ 프로젝트 정리하기  (0) 2007.05.15
파이의 정의  (0) 2007.04.03
The Java Programming Language - 1일차  (0) 2007.02.03

프로그래밍을 하다보면 파이값을 이용할 일이 꽤 있다. 흔히 다음과 같이 정의해서 사용할 것이다.

PI = 3.14

하지만, 그다지 정확한 값이 아니고, 정확한 값을 넣기 위해서 매번 찾아보는 것은 번거로운 일이다.(물론 소수점 이하 10자리 이상을 항상 외우고다니는 일부들에게는 해당되지 않는다 :-) ) 그럴때는 다음과 같이 간단하고 정확한 파이값을 정의할 수 있다.(C언어 기준)

PI = atan(1.0) * 4

'Developing' 카테고리의 다른 글

작성일자가 4월 1일인 RFC  (0) 2007.04.14
The Java Programming Language - 1일차  (0) 2007.02.03
USACO Training - Friday the Thirteenth  (0) 2006.09.12

+ Recent posts