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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.46/5 (8投票s)

2003年8月19日

4分钟阅读

viewsIcon

108914

downloadIcon

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 structenums与一些帮助我们打包数据缓冲区到数据包中的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: 文章更新

许可证

本文没有明确的许可,但可能包含文章文本或下载文件本身的用法条款。如有疑问,请通过下面的讨论板联系作者。作者可能使用的许可列表可以在这里找到。

© . All rights reserved.