Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to create a chat client using a set of communication classes

0.00/5 (No votes)
25 Aug 2003 1  
A pair of classes (no mfc) that can be used for tcp/ip _and_ serial communication.

Sample Image - chatclient.gif

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 called HandlePacket 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here