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:
DtSocketbase *pSocket;
[start catching events from the ms event model described later.]
pSocket = new DtSerialSocket;
((DtSerialSocket*)pSocket)->Connect("COM3", 9600);
char* pszBuffer = new char[20];
strcpy(pszBuffer, "Hello world!");
pSocket->Send(pszBuffer, strlen(pszBuffer));
pSocket->Disconnect();
[stop catching events]
delete pSocket;
[start catching events from the ms event model described later.]
pSocket = new DtIpSocket;
((DtIpSocket*)pSocket)->Connect("chat.myserver.com", 6667);
strcpy(pszBuffer, "Hello world!");
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)
{
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)
{
char *pszBuffer = new char[OutPacket.dwDataSize + HVD_HEADER_SIZE + 1];
if (!pszBuffer)
throw "ChatProtocol::Send, Cant create a 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);
#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)
{
char *pszBuffer = new char[OutPacket.dwDataSize + CHAT_HEADER_SIZE + 1];
if (!pszBuffer)
throw "ChatProtocol::Send, Cant create a 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;
#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 called
HandlePacket
that is declared in
CChatSocket
.
#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;
DWORD dwCopyLen = 0;
bool bCompleteBuffer = false;
if (!m_pInPacket)
{
DWORD dwSkipCount = 0;
if (nBufSize < CHAT_HEADER_SIZE)
return;
if (pInBuffer[0] != STX)
{
bool bFound = false;
for (dwSkipCount = 1; dwSkipCount < nBufSize - 1; dwSkipCount++)
{
if (pInBuffer[dwSkipCount] == STX)
{
bFound = true;
break;
}
}
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;
DWORD dwSize = 0;
memcpy(&m_pInPacket->nFunctionCode,
pInBuffer + dwSkipCount + CHARSIZE, USHORTSIZE);
memcpy(&dwSize,
pInBuffer + dwSkipCount + CHARSIZE+USHORTSIZE, DWORDSIZE);
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;
}
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;
}
else
{
dwBytesHandled = CHAT_HEADER_SIZE + dwSkipCount;
bCompleteBuffer = true;
}
}
else
{
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;
}
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++;
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)
{
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:
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.