如何构建基于 IOCP 框架的聊天服务器






4.46/5 (8投票s)
2003年8月19日
4分钟阅读

108914

2294
一个使用I/O完成端口的winsock服务器框架。它被设计成可重用,并且必须被重写。
引言
这是我写的第一篇文章,欢迎任何反馈。
很久以前,我需要一个使用I/O完成端口的服务器框架,但找不到任何符合我需求的解决方案。我找到的要么太复杂,要么不适合重用。因此,我开始自己制作,这是结果。所有类都用doxygen注释进行了完全注释。
框架
服务器框架基本上只有两个类,下面将对它们进行描述。
DtServerSocket
DtServerSocket
处理监听套接字,跟踪所有客户端,包含I/O完成函数和维护函数。
class DtServerSocket
{
protected:
SOCKET m_sdListen; /// Socket used to listen
DWORD m_dwPort; /// Port that we listen on
size_t m_nClients; /// Number of loaded clients
size_t m_nMaxClients; /// Maximum number of clients that we may have
DWORD m_dwServerFull; /// When the server got full
typedef vector< DtServerSocketClient* > CLIENTS;
CLIENTS m_paClients;
DtCriticalSection m_cs;
public:
DtServerSocket(void);
~DtServerSocket(void);
/// Load a client, the only thing you have to do is to derive this method
/// @param pClient should return a pointer to a DtServerSocketClient derived class
/// @param nId A id that identifies the client
virtual void LoadClient(DtServerSocketClient** pClient, int& nId) = 0;
/// @param dwListenPort The port that we should wait for connections on
/// @param nClients Number of initial clients
/// @param nMaxClients Total number of clients that the server could have
/// @returns 0 if success or system error code
DWORD StartServer(DWORD dwListenPort, int nClients, int nMaxClients);
/// Stops the server.
void StopServer(void);
/// Checks how long the clients have been connected
/// You should run this function in your main loop
virtual void Maintenance();
/// IoCompletion of the client threads calls this method
static void CALLBACK DoneIO(DWORD dwErrorCode,
DWORD dwNumberOfBytesTransferred,
LPOVERLAPPED lpOverlapped);
/// Override this one to give the server a name. Great for debugging purposes
virtual const char* GetServerName() { return "A server"; };
/// Override this one to get log printings
virtual void OnWriteLog(int nPrio, int nClientId, const char* pszCategory,
const char* pszString) const {};
void WriteLog(int nPrio, int nClientId, const char* pszCategory,
const char* pszString, ...) const;
};
我们从DtServerSocket
派生一个名为CChatServerSocket
的类,并重写两个函数。
/// This function is used to load a new client that is derived from DtServerSocketClient
virtual void LoadClient(Datatal::DtServerSocketClient** pClient, int& nId);
/// Useful for diagnostics if we run different servers in the same .exe
const char* GetServerName() { return "ChatServer"; };
DtServerSocketClient
DtServerSocketClient
包含由DtServerSocket
接受的每个客户端的所有I/O函数。
发送数据
为了处理输出缓冲,我们实现了一个先进先出链表来对所有出站数据进行排队。
/// Container for our outbuffers
struct Outbuffer
{
char* pBuffer;
DWORD dwSize;
Outbuffer* pNext;
};
/// Linked list with all our outbuffers
struct OutbufferList
{
Outbuffer* pFirst;
Outbuffer* pLast;
OutbufferList()
{
pFirst = NULL;
pLast = NULL;
}
void Append(char* pBuffer, DWORD dwSize)
{
if (!pBuffer || !dwSize) throw std::invalid_argument
("pBuffer and nSize cannot be NULL");
Outbuffer* pNewNode = new Outbuffer;
pNewNode->pBuffer = pBuffer;
pNewNode->pNext = NULL;
pNewNode->dwSize = dwSize;
if (pLast)
pLast->pNext = pNewNode;
else
pFirst = pNewNode;
pLast = pNewNode;
}
void RemoveFirst()
{
if (!pFirst) throw std::out_of_range("pFirst is NULL");
Outbuffer* pOld = pFirst;
pFirst = pFirst->pNext;
if (pOld == pLast) pLast = NULL;
delete[] pOld->pBuffer;
delete pOld;
}
};
当我们发送数据时,它只是被添加到列表中,并调用一个WriteOperation
。
/// Enqueue stuff to our outgoing buffer
bool Datatal::DtServerSocketClient::Send(char* data, int nSize)
{
//Lock outbuffer
m_CritWrite.Lock();
m_lOutBuffers.Append(data, (DWORD)nSize);
m_CritWrite.Unlock();
WriteLog(Datatal::LP_NORMAL, GetClientId(), "Send",
"Appending new outbuffer, size: %d", nSize);
//Trigger that we got a write operation
WriteOperation();
return true;
}
ChatServer
chatserver
类的实现。
DtServerSocket
-> CChatServerSocket
DtServerSocket | 所有IOCP服务器的基类。 (包含在 DtLibrary 中) |
CChatServerSocket | 包含向所有/特定客户端发送聊天消息的函数。 |
DtServerSocketClient
-> ChatProtocol
-> CChatServerClient
DtServerSocketClient | IOCP服务器中所有客户端套接字的基础类。 (包含在 DtLibrary 中) |
ChatProtocol | 聊天协议层。 |
CChatServerClient | 客户端层,跟踪用户(已登录,用户名等)。 |
设计协议
我们要做的第一件事是创建一个协议,用于在客户端/服务器之间发送和接收数据。协议实现为一个struct
(数据容器),一个enum
(函数代码)和最后另一个用于状态码的enum
。
<STX><USHORT><DWORD><CHAR><databuffer><ETX>
STX | ASCII 0x02 ,开始事务,告诉我们这是事务的开始。 |
USHORT | 我们想要运行哪个函数。 |
DWORD | databuffer 的大小。 |
CHAR | 状态码。如果出现问题,将更改为错误代码。 |
databuffer | 在客户端/服务器之间发送的所有数据都打包在此char 缓冲区中。 |
ETX | ASCII 0x03, 结束事务 ,用于确认我们收到了一个完整的事务。 |
这里是所有内容都被翻译成代码。
/// In this enum, we define the different transaction codes..
enum TRANS_CODES
{
TC_LOGIN, /// want to login
TC_LIST_CHANNELS, /// List all channels
TC_LIST_USERS, /// list all users / users on a specific channel
TC_SEND_MESSAGE, /// Set a message to a channel/user
TC_SEND_MESSAGE_OUT /// Outgoing message, (sent to client)
};
/// Status codes.
enum TRANS_STATUS
{
TS_OK, /// Everything went ok
TS_NO_DATA, /// Everything went ok, but we got no data
TS_INVALID, /// Invalid transaction
TS_ERROR, /// Something went wrong, presumably invalid data.
TS_NO_ACCESS, /// The user do not have the required credentials
TS_EXCEPTION /// Something unexpected happened.
};
/// Packet Structure - The heart of the protocol.
struct Packet
{
USHORT nFunctionCode; /// functioncode
DWORD dwDataSize; /// size of the data stored in pData
char Status; /// statuscode. 0 = ok;
char* pData; /// Buffer
DWORD dwBufferSize; /// maxsize of pData (allocated size)
~Packet() { if (pData) delete[] pData; };
Packet()
{
pData = NULL;
nFunctionCode = 0;
dwDataSize = 0;
dwBufferSize = 0;
Status = 0;
};
Packet(int FunctionCode)
{
pData = NULL;
nFunctionCode = FunctionCode;
dwDataSize = 0;
dwBufferSize = 0;
Status = 0;
};
};
实现CChatServer
我们创建一个名为CChatServer
的类,它继承自DtServerSocket
,并实现LoadClient
函数:
void CChatServerSocket::LoadClient(Datatal::DtServerSocketClient** pClient,
int nId)
{
CChatServerClient* pNewClient = new CChatServerClient;
if (pNewClient)
{
// You may call whatever you want to init the server client
}
else
{
char szLog[128];
sprintf(szLog, "Failed to load hvd client %d", nId);
throw Datatal::DtServerException(-1, "LoadClient", szLog);
}// if (pNewClient)
*pClient = pNewClient;
}
由于我们不想对客户端执行任何额外的初始操作,我们只是创建它们并将它们传回基类。
下一步是创建一个ChatProtocol和一个实现该协议的类。
ChatProtocol
将在服务器和客户端应用程序中都使用。Protocol struct
和enum
s与一些帮助我们打包数据缓冲区到数据包中的static
函数一起放在这个类中。ChatProtocol
继承自DtServerSocketClient
,并实现将我们的数据包翻译成纯char
缓冲区,然后将它们传递/从DtServerSocketClient
检索回来的函数。
创建ChatServerSocketClient类并在其中实现逻辑
通常,我使用CMarkup
(一个来自http://www.firstobject.com/的不错的XML类)处理所有数据,但在本例中,数据用0x04
分隔,我使用strtok
来解包所有内容。该类继承自ChatProtocol
,我们将在其中实现所有逻辑。
在chatserver中实现的函数:
- 用户在登录之前无法执行任何操作。
- 所有用户将继续登录,直到客户端连接关闭。
- 登录期间,他们可以向所有人或特定的人发送消息。
- 用户可以获取所有已登录用户的列表。
摘要
就是这样!我将不再进一步描述服务器,只需查看代码。一个功能齐全的客户端已在单独的项目中上传。
参考文献
服务器框架基于Ben Eleizer制作的IOCP示例,尽管它经过了相当大的修改。如果您想了解更多关于IOCP的细节,请阅读那篇文章。
可以在这里找到微软关于IOCP的另一篇好文章。
客户端可以在此链接找到。
历史
- 2003-08-08: 初版
- 2003-08-19: 文章更新
许可证
本文没有明确的许可,但可能包含文章文本或下载文件本身的用法条款。如有疑问,请通过下面的讨论板联系作者。作者可能使用的许可列表可以在这里找到。