Introduction
This is my first article ever and all feedback is appreciated.
A while ago, I needed a server framework that used I/O completion ports and I did not find any solutions that fit my need. Those that I found were either too complex or not designed for reusability. Therefore I started to make my own, and this is the result. All classes are fully commented with doxygen comments.
The Framework
The server framework is basically just two classes, and those are described below.
DtServerSocket
DtServerSocket
handles the listen socket, keeps track of all clients, contains the IO Completion function and a maintenance function.
class DtServerSocket
{
protected:
SOCKET m_sdListen; DWORD m_dwPort; size_t m_nClients; size_t m_nMaxClients; DWORD m_dwServerFull; typedef vector< DtServerSocketClient* > CLIENTS;
CLIENTS m_paClients;
DtCriticalSection m_cs;
public:
DtServerSocket(void);
~DtServerSocket(void);
virtual void LoadClient(DtServerSocketClient** pClient, int& nId) = 0;
DWORD StartServer(DWORD dwListenPort, int nClients, int nMaxClients);
void StopServer(void);
virtual void Maintenance();
static void CALLBACK DoneIO(DWORD dwErrorCode,
DWORD dwNumberOfBytesTransferred,
LPOVERLAPPED lpOverlapped);
virtual const char* GetServerName() { return "A server"; };
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;
};
We derive a class called CChatServerSocket
from DtServerSocket
and override two functions:
virtual void LoadClient(Datatal::DtServerSocketClient** pClient, int& nId);
const char* GetServerName() { return "ChatServer"; };
DtServerSocketClient
DtServerSocketClient
contains all I/O functions for each client that have been accepted by DtServerSocket
.
Sending Data
To handle output buffering, we have implemented a first-in/first-out linked list to enqueue all outgoing data:
struct Outbuffer
{
char* pBuffer;
DWORD dwSize;
Outbuffer* pNext;
};
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;
}
};
When we send data, it is simply added to the list and a WriteOperation
is invoked:
bool Datatal::DtServerSocketClient::Send(char* data, int nSize)
{
m_CritWrite.Lock();
m_lOutBuffers.Append(data, (DWORD)nSize);
m_CritWrite.Unlock();
WriteLog(Datatal::LP_NORMAL, GetClientId(), "Send",
"Appending new outbuffer, size: %d", nSize);
WriteOperation();
return true;
}
ChatServer
The chatserver
class implementation:
DtServerSocket
-> CChatServerSocket
DtServerSocket | Base class for all IOCP servers.
(included in the DtLibrary ) |
CChatServerSocket | Contain functions to send chat messages to all/specific clients. |
DtServerSocketClient
-> ChatProtocol
-> CChatServerClient
DtServerSocketClient | Base class for all client sockets in the IOCP servers.
(included in the DtLibrary ) |
ChatProtocol | Chat Protocol Layer |
CChatServerClient | Client layer, keeps track of the user (logged in, username, etc.) |
Designing the Protocol
The first thing that we have to do is create a protocol that will be used to send data back and forth between client/server. The protocol is implemented as a struct
(data container), an enum
(function codes) and finally another enum
for the status codes..
<STX><USHORT><DWORD><CHAR><databuffer><ETX>
STX | ASCII 0x02 , start transaction, tells us that this is the beginning of the transaction |
USHORT | which function we want to run |
DWORD | size of the databuffer |
CHAR | status code. Will be changed to an error code if something fails. |
databuffer | All data that will be sent between the client/server is packed in this char buffer. |
ETX | ASCII 0x03, end transaction , is used to confirm that we got a complete transaction.. |
Here is everything translated into code:
enum TRANS_CODES
{
TC_LOGIN, TC_LIST_CHANNELS, TC_LIST_USERS, TC_SEND_MESSAGE, TC_SEND_MESSAGE_OUT };
enum TRANS_STATUS
{
TS_OK, TS_NO_DATA, TS_INVALID, TS_ERROR, TS_NO_ACCESS, TS_EXCEPTION };
struct Packet
{
USHORT nFunctionCode; DWORD dwDataSize; char Status; char* pData; DWORD dwBufferSize; ~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;
};
};
Implementing CChatServer
We create a class called CChatServer
, derive it from DtServerSocket
, and implement the LoadClient
function:
void CChatServerSocket::LoadClient(Datatal::DtServerSocketClient** pClient,
int nId)
{
CChatServerClient* pNewClient = new CChatServerClient;
if (pNewClient)
{
}
else
{
char szLog[128];
sprintf(szLog, "Failed to load hvd client %d", nId);
throw Datatal::DtServerException(-1, "LoadClient", szLog);
}
*pClient = pNewClient;
}
Since we do not want to do any extra initial operations on the clients, we just create them and pass them back to the base class.
Next Thing is to Create a ChatProtocol, and a Class that Implements the Protocol
The ChatProtocol
will be used both in the server and the client application. The Protocol struct
and the enum
s are placed in this class along with some static
functions that help us to pack the data buffer into the packet. ChatProtocol
is derived from DtServerSocketClient
and implement functions that translate our packets into plain char
buffers and then passes/retrieve them to/from DtServerSocketClient
.
Create ChatServerSocketClient Class and Implement the Logic in It
Normally, I handle all data using CMarkup
, a nice XML class from http://www.firstobject.com/, but in this example, the data is separated with 0x04
and I use strtok
to unpack everything. The class is derived from ChatProtocol
and we will implement all logic in it.
Functions Implemented in the chatserver
- Users cannot do anything unless they have logged in.
- All users will continue to be logged in until the client connection is closed.
- While logged in, they can send messages to everyone or someone particular.
- Users can retrieve a list of all logged in users.
Summary
That's it! I will not describe the server any further, just take a look in the code. A fully functional client has been uploaded in a separate project.
References
The server framework is based on the IOCP example made by Ben Eleizer, although it's quite heavily modified. If you want more details about IOCP, read that article.
Another good article about IOCP by Microsoft can be found here.
The client can be found at this link.
History
- 2003-08-08: First version
- 2003-08-19: Article updated
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below. A list of licenses authors might use can be found here.