65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用一组通信类创建一个聊天客户端

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.71/5 (6投票s)

2003年8月19日

Apache

4分钟阅读

viewsIcon

80411

downloadIcon

2458

一对类(无 MFC),可用于 TCP/IP 和串行通信。

Sample Image - chatclient.gif

重要提示

聊天服务器可以在这里找到:https://codeproject.org.cn/internet/chatserver.asp

引言

在我上一篇文章中,我使用 IOCP 框架创建了一个聊天服务器,在本文中,我将创建一个与该服务器配合使用的聊天客户端。
本文使用的异步套接字库已用 doxygen 注释完全注释。

串行和 IP 通信

该库可用于串行和 IP 通信,并且您可以轻松地在它们之间切换。这是可能的,因为两个类都派生自同一个基类 `DtSocketBase`。 `DtSocketBase` 包含两个类都使用的一些通用函数,例如 `Connect`、`Send` 和 `OnReceive`。

一个简短的例子可能看起来像这样

 
  //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;  

聊天客户端设计

在设计聊天客户端时,我将其分为 4 个级别。

级别 1:使用 DtIpSocket 的原始套接字 IO。

`DtIpSocket` 使用原始 winsock 函数来处理出站/入站数据。

级别 2:聊天协议

`ChatProtocol` 类是在服务器中创建的,请参阅服务器文章以供参考。 `ChatProtocol` 类用于将原始缓冲区转换为聊天客户端使用的数据包。

级别 3:逻辑

逻辑放在一个名为 `ChatSocket` 的类中,GUI 使用它来接收和发送数据。

级别 4:GUI

为了从 ChatSocket 获取入站数据包,我们使用 `RegisterWindowMessage` 创建的窗口消息。要发送数据,我们只需调用 `ChatSocket` 中的正确函数,例如:`m_client.Login("jonas", "mypassword");`

发送数据

当 GUI 发送内容时,它必须通过级别 3 到达级别 1。让我们跟随登录事务。

级别 3:逻辑

1. 首先,我们检查是否已连接,如果未连接,则不执行任何操作。
套接字库有一个名为 `SetReconnect` 的属性,可用于告知库在断开连接时自动重新连接。
2. 之后,我们将数据打包到缓冲区中,然后通过数据包发送到服务器。
3. 创建数据包并使用 `SetPacketBuffer` 附加缓冲区。
4. 发送数据包。

	
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);	
}

级别 2:聊天协议

在此级别执行的唯一操作是将数据包转换为原始字符缓冲区,并将其传递给级别 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;
}

级别 1:原始套接字 IO

在此级别,我们将缓冲区加入我们的 `SendBuffer` 队列,然后触发将在工作线程中触发的 `NewData` 事件。

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;
}

完成。现在数据将尽快发送。

接收数据。

接收数据的方式与发送数据几乎相同,但顺序相反。

级别 1:原始套接字 IO

当新数据到达工作线程时,它会调用一个名为 `OnReceive` 的虚拟函数,并将所有数据传入缓冲区。然后必须由 OnReceive 处理数据,否则将被丢弃。

级别 2:聊天协议

如果数据包尚未创建,我们则创建一个新的数据包,否则我们继续将数据添加到数据包缓冲区,直到缓冲区大小与数据包头中指定的大小匹配。完成后,我们在 ETX (end transaction) 之后检查数据包结尾。如果找到,我们将数据传递给 `CChatSocket` 中声明的 `HandlePacket` 函数。
//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
 }
}

级别 3:ChatSocket

`HandlePacket` 只做一件事,就是将数据包发送到对话框。

void CChatSocket::HandlePacket(Packet* pInPacket)
{
 if (m_hWndParent)
  PostMessage(m_hWndParent, WM_CHAT_TRANS, pInPacket->Status, (LPARAM)pInPacket);
}

级别 4:GUI。

我们在窗口消息中接收数据包并进行处理。

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;
}
就这样了...

Microsoft 事件。

在 VC7 中,Microsoft 引入了一套新方法,可用于在某些事情发生时通知类。我在需要切换 IP/串行通信时使用它们。

创建一个事件

可以像这样声明一个事件来创建一个事件。

__event void OnError(int nErrorCode, const char* pszErrorDescription);

使用事件

在将使用该事件的类中,您必须指定事件、事件源和事件接收函数。

__hook(DtSocketBase::OnError, pCom, OnError);

当您想停止使用事件时,只需调用 unhook。

__unhook(DtSocketBase::OnError, pCom, OnError);

DtThread -> DtSocketBase -> DtIpSocket -> ChatProtocol -> CChatSocket

DtThread 所有需要线程的类都派生自此类。
(包含在 `DtLibrary` 中)
DtSocketBase 所有客户端通信类使用的基类。
(包含在 `DtLibrary` 中)
DtIpSocket 用于客户端 IP 通信的类。
(包含在 `DtLibrary` 中)
ChatProtocol 聊天协议在此类中定义,供客户端和服务器使用。
(在服务器文章中创建)
CChatSocket 包含所有聊天功能。

历史

  • 2003-08-08 第一版。
  • 2003-08-21 更新了文章。
© . All rights reserved.