How to create a chat client using a set of communication classes
A pair of classes (no mfc) that can be used for tcp/ip _and_ serial communication.
Important Note:
The chat server can be found here: http://www.codeproject.com/internet/chatserver.asp
Introduction
In my last article I created a chat server using a IOCP framework, in this article I will create a Chat Client that works with that server.
The asynchronous socket library that are used in this article is fully commented with doxygen comments.
Serial and IP communication
The library can be used for both serial and ip communication and you can easily switch between them. This is possible since both classes is derived from the same baseclass called DtSocketBase
. DtSocketBase
contains some general functions used by both classes, for example Connect
, Send
and OnReceive
.
A short example may look like this:
//Let's create an pointer to the base class. DtSocketbase *pSocket; // First load serial communication [start catching events from the ms event model described later.] pSocket = new DtSerialSocket; // Do an upcast and init the serial class. ((DtSerialSocket*)pSocket)->Connect("COM3", 9600); // Let's just send hello world. char* pszBuffer = new char[20]; strcpy(pszBuffer, "Hello world!"); // Buffers are freed by the lib when done with it. pSocket->Send(pszBuffer, strlen(pszBuffer)); pSocket->Disconnect(); [stop catching events] delete pSocket; // second load tcp/ip instead [start catching events from the ms event model described later.] pSocket = new DtIpSocket; // Do an upcast and init the ip class. ((DtIpSocket*)pSocket)->Connect("chat.myserver.com", 6667); strcpy(pszBuffer, "Hello world!"); // Buffers are freed by the lib when done with it. pSocket->Send(pszBuffer, strlen(pszBuffer)); pSocket->Disconnect(); [stop catching events] delete pSocket;
Chatclient design
When designing the chat client I divided it into 4 levels.
Level 1: Raw socket IO using DtIpSocket.
DtIpSocket
uses raw winsock functions to handle outgoing/incoming data..
Level 2: The chat protocol
The ChatProtocol
class were created in the server, for reference check the server article. The ChatProtocol
class is used to translate the raw buffers into packets used by the chat client.
Level 3: The logic
The logic is put in a class called ChatSocket
and it is used by the GUI to receive and send data.
Level 4: The gui
To get the incomming packets from ChatSocket we use windows messages created by RegisterWindowMessage
. To send data we simply invoke the correct funtion from ChatSocket
, for example: m_client.Login("jonas", "mypassword");
Sending data
When something is sent from the GUI, it has to go through Level 3 down to Level 1. Let's follow the login transaction.
Level 3: The logic
1. First we check if we are connected, if not we do nothing.
The socketlib have a property called SetReconnect
that can be used to tell the lib to auto-magically reconnect if disconnected.
2. After that we pack the data into a buffer that will be sent with the packet to the server.
3. Time to create the packet and attach the buffer using SetPacketBuffer
4. Send the packet.
bool CChatSocket::Login(const char* szUserName, const char* szPassword) { //check if we are connected if (!IsConnected()) return false; char szBuffer[512]; sprintf(szBuffer, "%s%c%s%c", szUserName, 0x04, szPassword, 0x04); Packet OutPacket(TRANS_CODES::TC_LOGIN); SetPacketBuffer(OutPacket, szBuffer, (int)strlen(szBuffer)); return Send(OutPacket); }
Level 2: The chat protocol
The only thing that is done at this level is to translate the packet into a raw char buffer and pass it on to Level 1.
bool ChatProtocol::Send(Packet& OutPacket) { //Create a new temp buffer char *pszBuffer = new char[OutPacket.dwDataSize + HVD_HEADER_SIZE + 1]; if (!pszBuffer) throw "ChatProtocol::Send, Cant create a buffer"; //Add data to the buffer pszBuffer[0] = STX; memcpy(pszBuffer + 1, &OutPacket.nFunctionCode, USHORTSIZE); memcpy(pszBuffer + 3, &OutPacket.dwDataSize, DWORDSIZE); memcpy(pszBuffer + 7, &OutPacket.Status, CHARSIZE); memcpy(pszBuffer + HVD_HEADER_SIZE, OutPacket.pData, OutPacket.dwDataSize); pszBuffer[OutPacket.dwDataSize + HVD_HEADER_SIZE] = ETX; TRACE("Send Trans: %d\n", OutPacket.nFunctionCode); //Send data #ifdef __SERVER_SIDE__ bool bRet = DtServerSocketClient:: Send(pszBuffer, OutPacket.dwDataSize + HVD_HEADER_SIZE + 1); #else bool bRet = DtIpSocket:: Send(pszBuffer, OutPacket.dwDataSize + HVD_HEADER_SIZE + 1); #endif return bRet; }
Level 1: Raw socket IO
What we do at this level is to enqueue the buffer in our SendBuffer
queue and then raise the NewData
event that will be triggered in the worker thread.
bool ChatProtocol::Send(Packet& OutPacket) { //Create a new temp buffer char *pszBuffer = new char[OutPacket.dwDataSize + CHAT_HEADER_SIZE + 1]; if (!pszBuffer) throw "ChatProtocol::Send, Cant create a buffer"; //Add data to the buffer pszBuffer[0] = STX; memcpy(pszBuffer + 1, &OutPacket.nFunctionCode, USHORTSIZE); memcpy(pszBuffer + 3, &OutPacket.dwDataSize, DWORDSIZE); memcpy(pszBuffer + 7, &OutPacket.Status, CHARSIZE); memcpy(pszBuffer + CHAT_HEADER_SIZE, OutPacket.pData, OutPacket.dwDataSize); pszBuffer[OutPacket.dwDataSize + CHAT_HEADER_SIZE] = ETX; //Send data #ifdef __SERVER_SIDE__ bool bRet = DtServerSocketClient::Send(pszBuffer, OutPacket.dwDataSize + CHAT_HEADER_SIZE + 1); #else bool bRet = DtIpSocket::Send(pszBuffer, OutPacket.dwDataSize + CHAT_HEADER_SIZE + 1); #endif return bRet; }
Done. Now the data will be sent as soon as possible.
Receiving data.
Receiving data is done almost the same way as sending data, but vice versa =)
Level 1: Raw socket IO
When new data arrives in the worker thread it calls a virtual function called OnReceive
with everything in the incoming buffer. The data must then be handled by OnReceive or it will be discarded.
Level 2: The chat protocol
We create a new packet if it has not been created, else we continue to add data to the packet buffer until the size of the buffer matches the one specified in the packet header. When that is done we check the packet end after an ETX (end transaction). If found, we pass the data to a function calledHandlePacket
that is declared in CChatSocket
. //Return the number of bytes that we have read from the buffer, #ifdef __SERVER_SIDE__ void ChatProtocol::OnReceive(const char* pInBuffer, size_t nBufSize) #else void ChatProtocol::HandleReceive(const char* pInBuffer, size_t nBufSize) #endif { DWORD dwBytesHandled = 0; // Number of bytes that we have handled. DWORD dwCopyLen = 0; // Number of bytes that we have copied from the buffer. bool bCompleteBuffer = false; // True if we got a complete buffer. // Check if we already have started building a packet. if (!m_pInPacket) { DWORD dwSkipCount = 0; //number of bytes we had to skip to find stx //We must get atleast 6 bytes. (stx, func <2 bytes>, size <2 bytes>, status) if (nBufSize < CHAT_HEADER_SIZE) return; //Check if we got a STX //======================================================== if (pInBuffer[0] != STX) { //loop through the array and try to find STX bool bFound = false; for (dwSkipCount = 1; dwSkipCount < nBufSize - 1; dwSkipCount++) { if (pInBuffer[dwSkipCount] == STX) { bFound = true; break; } } // didnt find a valid trans (or atleast STX) if (!bFound) return; char szLog[128]; sprintf(szLog, "Skipping %d bytes in recieve buffer", dwSkipCount); #ifdef __SERVER_SIDE__ WriteLog(Datatal::LP_HIGH, GetClientId(), "Send", szLog); #else WriteLog(Datatal::LP_HIGH, "Read", "Skipped X bytes from the recieve buffer."); #endif } m_pInPacket = new Packet; //Check if we got a complete packet DWORD dwSize = 0; memcpy(&m_pInPacket->nFunctionCode, pInBuffer + dwSkipCount + CHARSIZE, USHORTSIZE); //skip stx memcpy(&dwSize, pInBuffer + dwSkipCount + CHARSIZE+USHORTSIZE, DWORDSIZE); //skip stx, funccode //skip stx, funccode, size memcpy(&m_pInPacket->Status, pInBuffer + dwSkipCount + CHARSIZE+USHORTSIZE+DWORDSIZE, CHARSIZE); if (dwSize) { m_pInPacket->dwBufferSize = dwSize + 1; m_pInPacket->pData = new char[m_pInPacket->dwBufferSize]; if (!m_pInPacket->pData) { Disconnect(); char szLog[128]; sprintf(szLog, "Skipping %d bytes in recieve buffer", dwSkipCount); #ifdef __SERVER_SIDE__ WriteLog(Datatal::LP_HIGH, GetClientId(), "Read", "OnReceive, Failed to create packet buffer."); #else WriteLog(Datatal::LP_HIGH, "Read", "OnReceive, Failed to create packet buffer."); #endif return; } // Copy everything that we got in the buffer. dwCopyLen = (int)(nBufSize - dwSkipCount - CHAT_HEADER_SIZE); if (dwCopyLen > dwSize) { dwCopyLen = dwSize; bCompleteBuffer = true; } memcpy(m_pInPacket->pData, pInBuffer + CHAT_HEADER_SIZE + dwSkipCount, dwCopyLen); m_pInPacket->dwDataSize = dwCopyLen; m_pInPacket->pData[dwCopyLen] = 0; dwBytesHandled = dwCopyLen + CHAT_HEADER_SIZE + dwSkipCount; } // We got a buffer. else { // no buffer, handle recieve. dwBytesHandled = CHAT_HEADER_SIZE + dwSkipCount; bCompleteBuffer = true; } } //if (!pInPacket) else //We do got a buffer, but not a complete one. { // Check if we got a complete transaction with this one. if (nBufSize + m_pInPacket->dwDataSize >= m_pInPacket->dwBufferSize - 1) { dwCopyLen = m_pInPacket->dwBufferSize - m_pInPacket->dwDataSize - 1; bCompleteBuffer = true; } else dwCopyLen = (DWORD)nBufSize; memcpy(m_pInPacket->pData + m_pInPacket->dwDataSize, pInBuffer, dwCopyLen); m_pInPacket->dwDataSize += dwCopyLen; m_pInPacket->pData[m_pInPacket->dwDataSize] = 0; dwBytesHandled = dwCopyLen; } // Got a complete transaction if (bCompleteBuffer) { if ( pInBuffer[dwBytesHandled] != ETX) { #ifdef __SERVER_SIDE__ WriteLog(Datatal::LP_HIGH, GetClientId(), "Read", "Incorrect TRANS, no ETX! FuncCode: %d, Size: %d, nStatus: %d", m_pInPacket->nFunctionCode, m_pInPacket->dwDataSize, m_pInPacket->Status); #else WriteLog(Datatal::LP_HIGH, "Read", "Incorrect TRANS, no ETX! FuncCode: %d, Size: %d, nStatus: %d", m_pInPacket->nFunctionCode, m_pInPacket->dwDataSize, m_pInPacket->Status); #endif if (m_pInPacket->dwDataSize < 900) { #ifdef __SERVER_SIDE__ WriteLog(Datatal::LP_HIGH, GetClientId(), "Incorrect TRANS Data: %s", m_pInPacket->pData); #else WriteLog(Datatal::LP_HIGH, "Read", "Incorrect TRANS Data: %s", m_pInPacket->pData); #endif } Disconnect(); return; } dwBytesHandled++; //Increase one for the etx. HandlePacket(m_pInPacket); m_pInPacket = NULL; #ifdef __SERVER_SIDE__ if (nBufSize - (size_t)dwBytesHandled) OnReceive(pInBuffer + dwBytesHandled, nBufSize - (size_t)dwBytesHandled); #else if (nBufSize - (size_t)dwBytesHandled) HandleReceive(pInBuffer + dwBytesHandled, nBufSize - (size_t)dwBytesHandled); #endif } }
Level 3: ChatSocket
The only thing HandlePacket
does is to send the packet to the dialog
void CChatSocket::HandlePacket(Packet* pInPacket) { if (m_hWndParent) PostMessage(m_hWndParent, WM_CHAT_TRANS, pInPacket->Status, (LPARAM)pInPacket); }
Level 4: The GUI.
We receive the packet in the windows message and handle it:
LRESULT CChatClientDlg::OnChatTrans(WPARAM wp, LPARAM lp) { ChatProtocol::Packet* pInPacket = (ChatProtocol::Packet*)lp; switch (pInPacket->nFunctionCode) { // got an answer from the login transaction. case ChatProtocol::TC_LOGIN: if (pInPacket->Status != ChatProtocol::TS_OK) AfxMessageBox("Login failed!"); break; case ChatProtocol::TC_SEND_MESSAGE_OUT: AddMessage(pInPacket->pData); break; case ChatProtocol::TC_SEND_MESSAGE: //we got a ACK break; case ChatProtocol::TC_LIST_USERS: ListUsers(pInPacket->pData); break; default: AfxMessageBox("Got junc transaction"); } delete pInPacket; return TRUE; }That's all...
Microsoft events.
In VC7 Microsoft have introduced a new set of methods that can be used to notify classes when something have happened. I use them when I need to switch between IP/serial communication.
Create an event
A event can be created quite simply by declaring it like this:
__event void OnError(int nErrorCode, const char* pszErrorDescription);
To use the event
In the class that will use the event you have to specify the the event, the source of the event and the event receiver function:
__hook(DtSocketBase::OnError, pCom, OnError);
And when you want to stop using the event simply call unhook:
__unhook(DtSocketBase::OnError, pCom, OnError);
Classes
DtThread
-> DtSocketBase
-> DtIpSocket
-> ChatProtocol
-> CChatSocket
DtThread |
All my classes that need a thread is derived from this one. (included in the DtLibrary ) |
DtSocketBase |
A base class used by all my client communication classes (included in the DtLibrary ) |
DtIpSocket |
Class used for client ip communication (included in the DtLibrary ) |
ChatProtocol |
The chat protocol is defined in this class, used by client and server. (created in the server article) |
CChatSocket |
Contains all chat functions |
History
- 2003-08-08 First version.
- 2003-08-21 Updated the article.