node.js tcp chatting server with QT client
node.js로 구현한 채팅 서버 및 QT로 GUI를 꾸민 클라이언트이다.
사용자 등록, 로그인, 대화상대 추가/삭제, 로그인/아웃 알림 및 메시지 전달 기능이 구현되어 있다.
Current features:
- enroll user
- add/remove friend
- login and retrieve my friend list
- notify logged in/out
- 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 = 0 ; var totalPacketLen = -1; var accumulatingLen = 0; var recvedThisTimeLen= 0; c on'data'function data recvedThisTimeLen = datalength; console.log'recvedThisTimeLen='+ recvedThisTimeLen ; var tmpBuffer = 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 readUInt32BE0 ; console.log'totalPacketLen=' + totalPacketLen ; while accumulatingLen >= totalPacketLen + packetHeaderLen var aPacketBufExceptHeader = totalPacketLen ; accumulatingBuffer copy aPacketBufExceptHeader0 packetHeaderLen accumulatingBufferlength ; //////////////////////////////////////////////////////////////////// //process packet data var stringData = aPacketBufExceptHeadertoString ; var usage = stringDatasubstring0 stringDataindexOf TCP_DELIMITER ; console.log"usage: " + usage ; //call handler serverFunctions usage c remoteIpPort stringDatasubstring1+stringDataindexOf TCP_DELIMITER ; //////////////////////////////////////////////////////////////////// var newBufRebuild = accumulatingBufferlength ; newBufRebuild fill ; accumulatingBuffer copy newBufRebuild0 totalPacketLen + packetHeaderLen accumulatingBufferlength ; //init accumulatingLen -= totalPacketLen +4 ; accumulatingBuffer = newBufRebuild; newBufRebuild = null; totalPacketLen = -1; if accumulatingLen <= packetHeaderLen return; else totalPacketLen = accumulatingBuffer readUInt32BE0 ; ; |
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
안녕하세요. node.js tcp chatting server 소스를 찾는중인데 구현을 정말 잘하신거 같아서, 작성하신 소스 리뷰중인데 궁금한게 있어서 댓글 남깁니다. node.js 에 관하여 아직 초보라 질문 내용이 많습니다.
답글삭제1.nodeChatServer 에서 316줄 serverFunctions ['Login'] =func(conn,remote,pack)에서 serverFunctions ['Login']의 대괄호 문법을 처음 보아서 이 대괄호 문법의 의미와 소스 분석한 답변을 부탁드립니다. logindialog.cpp의 void LogInDialog::LogIn() 와 연결이 되어있는 소스인가요?
javascript 에서 key value 로 함수를 등록한것이라고 보시면 됩니다. LogIn 함수를 등록한 것입니다. 지금쯤이면 이미 다 알고 계실듯 하네요 .
삭제serverFunctions ['Login'] == 문자열 'Login' 이라는 배열의 의미인가요?
답글삭제