Click here to Skip to main content
15,886,075 members
Articles / Desktop Programming / MFC
Article

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

,
Rate me:
Please Sign up or sign in to vote.
3.71/5 (6 votes)
25 Aug 2003Apache4 min read 79.5K   2.5K   37   7
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

DtThreadAll my classes that need a thread is derived from this one.
(included in the DtLibrary)
DtSocketBaseA base class used by all my client communication classes
(included in the DtLibrary)
DtIpSocketClass used for client ip communication
(included in the DtLibrary)
ChatProtocolThe chat protocol is defined in this class, used by client and server.
(created in the server article)
CChatSocketContains all chat functions

History

  • 2003-08-08 First version.
  • 2003-08-21 Updated the article.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Founder 1TCompany AB
Sweden Sweden

Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionquestion Pin
ycd7-Jun-10 16:55
ycd7-Jun-10 16:55 
Does this code be used with C#?
AnswerRe: question Pin
jgauffin7-Jun-10 20:04
jgauffin7-Jun-10 20:04 
Generalhelp Pin
lijianli29-Dec-08 5:56
lijianli29-Dec-08 5:56 
GeneralRe: help Pin
jgauffin4-Jan-09 20:25
jgauffin4-Jan-09 20:25 
GeneralFound in chatserver Pin
Jian123456724-May-05 5:26
Jian123456724-May-05 5:26 
General../DtLibrary/DtServerSocketClient.h missing Pin
Jian123456720-May-05 8:36
Jian123456720-May-05 8:36 
GeneralRe: ../DtLibrary/DtServerSocketClient.h missing Pin
jgauffin11-May-07 7:12
jgauffin11-May-07 7:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.