이 블로그 검색

2012년 12월 25일 화요일

node.js chat server with QT client

node.js tcp chatting server with QT client


node.js로 구현한 채팅 서버 및 QT로 GUI를 꾸민 클라이언트이다.
사용자 등록, 로그인, 대화상대 추가/삭제, 로그인/아웃 알림 및 메시지 전달 기능이 구현되어 있다.

Current features:
  1. enroll user
  2. add/remove friend
  3. login and retrieve my friend list
  4. notify logged in/out
  5. deliver chat message

Server

net, Buffer module을 이용해서 작성되었다. 클라이언트와 주고받는 패킷의 정의는 다음과 같다.

The Packet format is : binary data + string 
4 byte header (means a length of following message) + message

The message format is :
string|(delimiter)... 

제일 먼저 32비트 정수가 포함되고 이후에는 문자열로 구성된 양식이다. 헤더와 메시지를 구분하는 별도 구분은 없다. 문자열의 길이는 이 32비트 정수에 설정 된다. 메시지의 형식은 먼저 사용 목적이 나오고 이후 구분자로 구분된 데이터들이 전송된다. 예를 들어 로그인의 경우라면 전달되는 전체 데이터는 다음과 같게 된다.

ex) Login message
4byte_integerLOGIN|SomeUserId|SomePassWord~

즉, 바이너리 데이터와 문자열이 포함되는 형식인데, 결국 다음과 같은 C 구조체가 전송되는 것과 같다.

This can be described as a C structure like this :
typedef struct _PACKET_LOGIN
{
    unsigned int nLen; //4byte, contains 34
    char szMSg [30];    
} PACKET_LOGIN;

이러한 구조체 데이터에 nLen 에는 문자열의 길이 (30)이 설정되고 szMSg 에는 "LOGIN|someUserId|SomePassWord~" 문자열이 담겨져서 node.js 서버로 전송되는것과 같다. 그러므로 전체 데이터의 길이는 헤더에 설정된 메시지 길이(30 byte) + 헤더 자체의 길이 (4byte) = 34 byte가 된다.

인터넷에서는 echo 서버, 혹은 단순히 클라이언트의 데이터를 받는 즉시 브로드캐스팅하는 예제를 볼수 있다. 하지만 tcp의 메시지 경계 없음으로 인한 데이터 fragmentation을 고려해줘야 제대로 동작하는 서버를 작성할수 있다. 이 때문에 전달되는 데이터가 완전한 하나의 패킷이 될때, 해당 처리를 해주고 만약 기대하는 길이만큼 전송이 다 되지 않았다면, 지금까지 받은 데이터를 누적시키는 작업이 필요하다.
전달되는 데이터가 모두 문자열이라면, 일반 변수를 사용해서도 가능하겠지만, 지금 구현하는것은 헤더부분이 32비트 정수형이기 때문에 (즉 바이너리 데이터 + 스트링 의 혼합) 뭔가 다른 방법이 필요하다. 이때 node.js 가 제공하는 Buffer 모듈을 사용하면 문자열 데이터 뿐만 아니라, 정수형 데이터도 처리가 가능하다.



소켓으로 데이터가 전달될때의 처리는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var accumulatingBuffer = new Buffer(0); 
var totalPacketLen   = -1; 
var accumulatingLen  =  0;
var recvedThisTimeLen=  0;

c.on('data', function(data) {        
    recvedThisTimeLen = data.length;
    console.log('recvedThisTimeLen='+ recvedThisTimeLen);
    var tmpBuffer = new Buffer( accumulatingLen + recvedThisTimeLen );
    accumulatingBuffer.copy(tmpBuffer);
    data.copy ( tmpBuffer, accumulatingLen  ); // offset for accumulating
    accumulatingBuffer = tmpBuffer; 
    tmpBuffer = null;
    accumulatingLen += recvedThisTimeLen ;
    
    if( recvedThisTimeLen < packetHeaderLen ) {        
        return;
    } else if( recvedThisTimeLen == packetHeaderLen ) {        
        return;
    } else {        
        if( totalPacketLen < 0 ) {
            totalPacketLen = accumulatingBuffer.readUInt32BE(0) ; 
            console.log('totalPacketLen=' + totalPacketLen );
        }
    }    

    while( accumulatingLen >= totalPacketLen + packetHeaderLen ) {
        var aPacketBufExceptHeader = new Buffer( totalPacketLen  ); 
        accumulatingBuffer.copy( aPacketBufExceptHeader, 0, 
         packetHeaderLen, accumulatingBuffer.length); 
        
        ////////////////////////////////////////////////////////////////////
        //process packet data
        var stringData = aPacketBufExceptHeader.toString();
        var usage = stringData.substring(0,stringData.indexOf(TCP_DELIMITER));
        console.log("usage: " + usage);
        //call handler
        (serverFunctions [usage])(c, remoteIpPort, 
            stringData.substring(1+stringData.indexOf(TCP_DELIMITER)));
        ////////////////////////////////////////////////////////////////////
                
        var newBufRebuild = new Buffer( accumulatingBuffer.length );
        newBufRebuild.fill();
        accumulatingBuffer.copy( newBufRebuild, 0, 
            totalPacketLen + packetHeaderLen, accumulatingBuffer.length  );
        
        //init      
        accumulatingLen -= (totalPacketLen +4) ;
        accumulatingBuffer = newBufRebuild;
        newBufRebuild = null;
        totalPacketLen = -1;
                
        if( accumulatingLen <= packetHeaderLen ) {        
            return;
        } else {
            totalPacketLen = accumulatingBuffer.readUInt32BE(0) ;             
        }    
    }     
});




Buffer 의 copy 메서드를 사용하여 동적으로 버퍼를 할당하여 데이터를 누적시켰다.
accumulatingBuffer.readUInt32BE(0) 를 사용하여 4바이트 정수값을 읽어서, 헤더를 제외한 메시지의 길이를 구한다. 네트워크 바이트는 빅엔디언인이기 때문에 xxxBE() 함수를 사용한다. 이후 버퍼에서 실제 문자열 메시지를 읽을때는 헤더길이(4)만큼 offset 을 주고 버퍼에 접근하면 된다. 즉,

accumulatingBuffer.copy( aPacketBufExceptHeader, 0, 
    packetHeaderLen, accumulatingBuffer.length); // offset header length

이부분이 offset만큼 간격을 두고 버퍼에서 메시지 문자열만을 얻기 위한 부분이다. 이처럼 일반적인 C언어 구조체와 같은 형식도 node.js Buffer 를 사용하면, 자유롭게 연동이 가능하다. 사용자 정보, 대화상대 정보 등은 sqlite 데이터베이스에 저장하는것으로 하였다. 이를 위해 node-sqlite3 가 사용 되었다.  클라이언트로 데이터 전송시에도 헤더 설정이 필요한데, 이때는 writeUInt32BE 함수가 사용되었다.

Client

QT 를 이용하여, 간단한  GUI를 가진 것으로 작성 하였다. node.js서버에서는 클라이언트 요청에 대해 다음과 같이 응답을 해준다.

Responses:
ex) if adding friend is success :
4byte_headerADDFRIEND|OK|friendid|online

ex) if adding friend is failure :
4byte_headerADDFRIEND|FAIL|err-string

아래 스크린샷과 소스를 참고.

소스는 다음에서 다운로드 할수 있다.
https://github.com/jeremyko/nodeChatServer
https://github.com/jeremyko/nodeChatClient









댓글 3개:

  1. 안녕하세요. node.js tcp chatting server 소스를 찾는중인데 구현을 정말 잘하신거 같아서, 작성하신 소스 리뷰중인데 궁금한게 있어서 댓글 남깁니다. node.js 에 관하여 아직 초보라 질문 내용이 많습니다.

    1.nodeChatServer 에서 316줄 serverFunctions ['Login'] =func(conn,remote,pack)에서 serverFunctions ['Login']의 대괄호 문법을 처음 보아서 이 대괄호 문법의 의미와 소스 분석한 답변을 부탁드립니다. logindialog.cpp의 void LogInDialog::LogIn() 와 연결이 되어있는 소스인가요?

    답글삭제
    답글
    1. javascript 에서 key value 로 함수를 등록한것이라고 보시면 됩니다. LogIn 함수를 등록한 것입니다. 지금쯤이면 이미 다 알고 계실듯 하네요 .

      삭제
  2. serverFunctions ['Login'] == 문자열 'Login' 이라는 배열의 의미인가요?

    답글삭제