如何使用一组通信类创建一个聊天客户端
一对类(无 MFC),可用于 TCP/IP 和串行通信。
重要提示
聊天服务器可以在这里找到: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 更新了文章。