Complete Tutorial

The best way you can see Push Framework into action is to guide you in the development of a whole new application that will be based on it.  This tutorial is organized into :

  • Introduction : The choice about the demo application is explained.
  • Designing the Protocol : The logic of communication between the client and Push Framework-based server will be designed and implemented. We ‘ll enhance the protocol by adding an XML layer to ease the subsequent servicing code.
  • Creating the ClientFactory : We implement the way how we process the connection and login of clients.
  • Adding Services : We add here the servicing code that handles remote client requests.
  • The Chat Client : We develop the client application our server will be talking to.
  • We are done ! : Let’s test things here.

Note that you can download the full source code of this demo application in the Download section.

Introduction

In this tutorial section we will be creating a chat application as a means to feature the use of Push Framework.

A chat server provides a typical example of servers where :

  • by the nature of the application,  a large number of connected clients are expected to connect and stay for non-short periods of time.
  • Each client can send multiple information and can expect to receive any number of server responses.
  • Each client can communicate with any other client, and also can engage in group communication.
  • The server has to push data in an asynchronous manner.
  • Only information messages have to be exchanged : the server does not deal with the client side UI.

In the following, we’ll create our chat server using the Push Framework. Then, we create a simple desktop program which is the client application the user will use to connect and interact with the remote server.

Designing the Protocol

The term protocol has broad meanings. But, in case of software communication it generally encompasses 1.the way how messages are binarily represented and 2. the combination of possible sequences of exchanges between the client and the server.

In PF, the sequence (or workflow) of messages is determined by the developer. As for the messages representation, this information is gathered in form of a concrete implementation of the pure PushFramework::Protocol class.  The Protocol class contains the logic of messages serialization as well as deserialization. Normally, a serialization process involves the encoding of structured messages then their framing into the TCP stream. A de-serialization process involves the opposite operations : deframing and decoding.

Note : you can read more in the Developer Guide, “Designing the protocol” paragraph.

Serialization acts on structured data that are required to derive from PushFramework::OutgoingPacket. This is the output of your server to your clients. De-serialization produces structured data representing the input of your clients, which is then passed as requests to the servicing code.

  • OutgoingPacket : if you have data to send to peer client(s), prepare an instance of this classe and push it to the target Client or Broadcasting channel.
  • IncomingPacket : when you deframe messages you have to try provide the result in form of a new instance of this class. The same instance is latter provided to you as a parameter of the handle method of the corresponding Service instance.
class Protocol
{
public:
        //
	int serializeOutgoingPacket(OutgoingPacket& packet, DataBuffer& buffer, unsigned int& nWrittenBytes);
        virtual int encodeOutgoingPacket(OutgoingPacket& packet)=0;
        virtual int frameOutgoingPacket(OutgoingPacket& packet, DataBuffer& buffer, unsigned int& nWrittenBytes)=0;
 
        //
        int tryDeserializeIncomingPacket(DataBuffer& buffer, IncomingPacket*& pPacket, int& serviceId,
                                         unsigned int& nExtractedBytes, ConnectionContext* pContext);
        virtual int tryDeframeIncomingPacket(DataBuffer& buffer, IncomingPacket*& pPacket, int& serviceId,
                                             unsigned int& nExtractedBytes, ConnectionContext* pContext) = 0;
        virtual int decodeIncomingPacket(IncomingPacket* pPacket, int& serviceId) = 0;
 
        //
        virtual void disposeIncomingPacket(IncomingPacket* pPacket) = 0;
};

For most applications, there is nothing more convenient than using XML. For the purpose of easing the servicing code and also the client application code we will use XML to represent both outgoing packets and incoming packets.

For outgoing packets, we would like to make it possible to construct a DOM structure then serialize it out. For incoming packets, we like to construct a DOM structure by parsing the XML content.

class IncomingXMLPacket : public PushFramework::IncomingPacket
{
public:
	IncomingXMLPacket();
	virtual ~IncomingXMLPacket(void);
	virtual bool FragmentXML() = 0;
	XMLNode getInputXML();
private:
	virtual bool Decode();
};
class OutgoingXMLPacket : public PushFramework::OutgoingPacket
{
public:
	OutgoingXMLPacket();
	virtual ~OutgoingXMLPacket();
	virtual bool ConstructXML() = 0;
	XMLNode getOutputXML();
private:
	virtual bool Encode();
};

The implementation is separated in a standalone DLL project so we can re-use the code for the client side application. A third party XML parser is integrated and we define new virtual members that are triggered from with the coding and decoding methods :

  • FragmentXML : called at decoding time, to allow the extraction of member data values from the parsed DOM.
  • ConstructXML : called at encoding time to allow the construction of the XML tree before serializing it all to the sending buffer.

We design two symetric classes to be used on both sides (Client send request, server receives request, server sends response, client receives response) :

class XMLRequest : public IncomingXMLPacket, public OutgoingXMLPacket
{
public:
	XMLRequest(unsigned int serviceId);
	~XMLRequest(void);
};
class XMLResponse : public IncomingXMLPacket, public OutgoingXMLPacket
{
public:
	XMLResponse(unsigned int serviceId);
	~XMLResponse(void);
};

The Protocol encode functions calls the OutgoingXMLPacket::encode functions which in return calls the subclass version of ::ConstructXML, and the XML stream result  is saved into an internal buffer which is framed afterward.

Framing adds a header that allows both sides to know the boundaries of each message :

int XMLProtocolImpl::frameOutgoingPacket( PushFramework::OutgoingPacket& packet,
                                          PushFramework::DataBuffer& buffer, unsigned int& nWrittenBytes )
{
 OutgoingXMLPacket& xmlPacket = (OutgoingXMLPacket&) packet;
 
 nWrittenBytes = 6 + xmlPacket.getBufferLen() + 2;
 
 if(nWrittenBytes > buffer.getRemainingSize())
 return PushFramework::Protocol::eInsufficientBuffer;
 
 unsigned int sStart = SignatureStart;
 unsigned int packetLen = nWrittenBytes;
 unsigned int commandID = xmlPacket.getRequestId();
 unsigned int sEnd = SignatureEnd;
 
 buffer.Append((char*)&sStart, 2);
 buffer.Append((char*)&packetLen, 2);
 buffer.Append((char*)&commandID, 2);
 
 buffer.Append(xmlPacket.getBuffer(), xmlPacket.getBufferLen());
 
 buffer.Append((char*)&sEnd, 2);
 
 return PushFramework::Protocol::Success;
};

Thanks to this implementation, all that is needed is to conceive and design the possible messages to be exchanged between the server and the client by deriving from XMLRequest and XMLResponse.

From server to client we have :

  • DirectChatResponse
  • JoinRoomResponse
  • LoginResponse
  • ParticipantInResponse
  • ParticipantOutResponse
  • RoomChatResponse
  • RoomsResponse

From client to server :

  • LoginRequest
  • DirectChatRequest
  • JoinRoomRequest
  • LeaveRoomRequest
  • RoomChatRequest
  • LogoutRequest

This suffices to imagine a complete scenario where a chat participants login in to the server, receives the list of connected participants, the list of rooms, directs chat messages to specifics persons, receive direct chats, joins, participates in and then leaves chat rooms, then logs off and disconnects from server.

As an example this is how it is done for the DirectChatRequest packet :

class DirectChatRequest : public XMLRequest
{
public:
	DirectChatRequest(void);
	~DirectChatRequest(void);
private:
	std::wstring recipient;
	std::wstring msg;
protected:
	virtual bool FragmentXML()
	{
		recipient = getInputXML().getChildNode(L"to").getAttribute(L"val");
		msg = getInputXML().getChildNode(L"msg").getAttribute(L"val");
		return true;
	}
	virtual bool ConstructXML()
	{
		getOutputXML().addChild(L"to").addAttribute(L"val", recipient.c_str());
		getOutputXML().addChild(L"msg").addAttribute(L"val", msg.c_str());
		return true;
	}
};

It is the same for other packets : it is just a matter of guessing what data members we need then write the XML construction/fragmentation code. The subsequent servicing code will just deal with the getters and setters of these classes .

Note that the concrete implemntation of the Protocol class, XMLProtocol, has to act like an abstract factory by instantiating Request packets at deserialization time. That why we add a registration mechanism to provide the binding between services and their request packet. In the handle method of each service, it becomes a matter of casting the request packet to the proper implementation knowing by advance the correct binding. Example :

class DirectChatRequestService : public PushFramework::Service
{
public:
	virtual void handle(LogicalConnection* pClient, IncomingPacket* pRequest)
	{
		DirectChatRequest* request = (DirectChatRequest*) pRequest;
                /*Deframing, XML parsing, fragmentation have all already occured, we are ready to access
                information in the data attributes.*/
	}
};

Finally, we created a concrete implementation of the protocol class to be used in our chat server, and with the support of XML we gave ourselves the opportunity to design and handle a limiteless number of exchangeable data structures.

Creating the ClientFactory

A Push Framework – based server requires concrete implementation of the ClientFactory class. In general this class controls the process of client creation/disposal/login and disconnection.

When a new connection is accepted by the server, the framework asks you if you have to send data to it before receiving any incoming requests. This can be very useful in most cases. For example, in a challenge-response authentication like CRAM-MD5, the server creates a dynamic puzzle, saves the correct answer in a context that will be invoked upon the first incoming packet and then expect to receive the answer in upcoming messages. This way the server is gotten rid of most of illegitemate clients or clients who want to perform a technical attack against your server.

In our case, and for illustration purposes, the dynamic puzzle is a simple function f(x) = x+1 where x is randomly generated and sent to the newly connected client :

OutgoingPacket* ChatParticipantFactory::onNewConnection( ConnectionContext*& lpContext )
{
    //Send the login challenge :
    LoginPuzzleResponse* pPuzzle = new LoginPuzzleResponse;
 
    ChatConnectionCxt* pCxt = new ChatConnectionCxt;
    pCxt->puzzle = rand()%100;
 
    pPuzzle->PuzzleQuestion(pCxt->puzzle);
 
    lpContext = pCxt;
    return pPuzzle;
}

When the first request is received, x is dereferenced and its imaged is compared to what the client has given us :

int ChatParticipantFactory::onFirstRequest( IncomingPacket& request, ConnectionContext* lpContext, LogicalConnection*& lpClient, OutgoingPacket*& lpPacket )
{
    LoginRequest& packet = (LoginRequest&) request;
 
    std::string pseudo = packet.Pseudo();
 
    int question = ((ChatConnectionCxt*) lpContext)->puzzle;
 
    if (packet.LoginPuzzleReply() != ( question + 1 ) )
    {
        LoginResponse* pLoginResponse = new LoginResponse;
        pLoginResponse->BAccepted(false);
        pLoginResponse->Msg("login puzzle is false");
 
        lpPacket = pLoginResponse;
 
        return ClientFactory::RefuseRequest;
    }
 
    ChatParticipant* pClient = new ChatParticipant(pseudo);
    lpClient = pClient;
 
    return ClientFactory::CreateClient;
}
}
 
ChatParticipant* pClient = new ChatParticipant(pseudo);
lpClient = pClient;
 
return ClientFactory::CreateClient;
}

ChatParticipant derives from PushFramework::LogicalConnection and represents our remote client :

class ChatParticipant : public PushFramework::LogicalConnection
{
public:
	ChatParticipant(std::wstring pseudo);
	~ChatParticipant(void);
	virtual const wchar_t* getKey()
	{
		return pseudo.c_str();
	}
private:
	std::wstring pseudo;
};

When this data structure is returned by ::onFirstRequest, if the framework detects that the client key is a new one, then ::onClientConnected is called.  Otherwise, ie if the client already exists, the structure is disposed, and the existing one is automatically attached to the new connection (The previous physical connection is closed).

When ::onClientConnected is called, we want to notify the rest of participants by broadcasting a ParticipantInResponse message :

void ChatParticipantFactory::onClientConnected( ClientKey key )
{
  ChatParticipant* pParticipant = (ChatParticipant*) pClient;
 
    LoginResponse* pLoginResponse = new LoginResponse;
    pLoginResponse->BAccepted(true);
    pLoginResponse->Msg("welcome to server");
    pParticipant->PushPacket(pLoginResponse);
 
    ParticipantInResponse* pPacket = new ParticipantInResponse;
    pPacket->Pseudo(pClient->getKey());
    broadcastManager.PushPacket(pPacket, "participants", pClient->getKey(), 0);
}

Some other virtual functions need be implemented for the ClientFactory to become concrete. But we leave the details to the reader who wants to check the source code.

Note  that LogicalConnection objects are stored based on their unique key (defined by your custom version of ::getKet). given a key, you dereference that into a LogicalConnection object using PushFramework::FindClient.
The object should be returned back via PushFramework::ReturnClient.

These are not locking function in the sense that it is actually possible that two threads can act on a same LogicalConnection instance simultaneously. However, to achieve the safe disposal of these objects (when client disconnects or your server explicitely disconnects it), the framework needs to maintain a reference count for each object, hence the two global functions.

Adding Services

Adding services is a simple job. Just derive from the Service class, provide an implementation for the handle method and register an instance with the proper service id. This is how it goes for the following service responsible for routing direct chat requests :

class DirectChatRequestService : public PushFramework::Service
{
public:
	void handle( LogicalConnection* pClient, IncomingPacket* pRequest )
{
    ChatParticipant* pParticipant = (ChatParticipant*) pClient;
 
    DirectChatRequest* request = (DirectChatRequest*) pRequest;
 
    ChatParticipant* pRecipient = (ChatParticipant*) FindClient(request->Recipient().c_str());
    if (pRecipient)
    {
        DirectChatResponse ChatResponse;
        ChatResponse.Sender(pParticipant->getKey());
        ChatResponse.Msg(request->Msg());
 
        pRecipient->PushPacket(&ChatResponse);
 
        ReturnClient(pRecipient);
    }
}
};

So the clientkey provided by the framework is translated into a ChatParticipant object, the pRequest is casted to a DirectChatRequest, we calculated the destination LogicalConnection object by looking at the request content, then a DirectChatResponse is formed then sent via that object.

In Push Framework data broadcast is based on the notion of broadcasting queue. By creating a queue and pushing packets to it, all Client subscribing to it will asynchronously receive that data.

This is perfect for room chat : joining a room is as simple as a queue subscription call, and a room chat is as simple as pushing a packet to the proper queue.

class JoinRoomRequestService : public PushFramework::Service
{
public:
	void handle( LogicalConnection* pClient, IncomingPacket* pRequest )
{
    ChatParticipant* pParticipant = (ChatParticipant*) pClient;
    //First un-subscribe from current room :
    std::string curRoom = pParticipant->getChatRoom();
    if(curRoom != "")
    {
        broadcastManager.UnsubscribeConnectionFromQueue(pParticipant->getKey(), curRoom.c_str());
    }
 
    JoinRoomRequest* request = (JoinRoomRequest*) pRequest;
 
    std::string roomName = request->Room();
    pParticipant->setChatRoom(roomName);
    broadcastManager.SubscribeConnectionToQueue(pParticipant->getKey(), roomName.c_str());
 
    JoinRoomResponse* pResponse = new JoinRoomResponse;
    pResponse->Room(roomName);
 
    pParticipant->PushPacket(pResponse);
 
}
};

When we receive a room chat request we push a response to the proper broadcasting queue:

class RoomChatRequestService : public PushFramework::Service
{
public:
	void handle( LogicalConnection* pClient, IncomingPacket* pRequest )
{
    ChatParticipant* pParticipant = (ChatParticipant*) pClient;
 
    RoomChatRequest* request = (RoomChatRequest*) pRequest;
 
    RoomChatResponse* pResponse = new RoomChatResponse;
 
    pResponse->Sender(pParticipant->getKey());
    pResponse->Msg(request->Msg());
    pResponse->Room(request->Room());
 
    broadcastManager.PushPacket(pResponse, request->Room().c_str(), "", 0);
}
};

The Chat Client

Let’s design then implement a simple Chat client application so we can test our server. Here’s a simple prototype :

It is a simple surface with a few controls on it. The two radio buttons allows user to select the category of items he wish to interact with :

  • Direct chat participants
  • Rooms (group of chat participants)

The pseudo combobox is updated with the list of connected clients. That information will be received asynchronously like all other information. The same goes with the Rooms combo box which is synchronized with the list of available rooms. The bottom controls allows user to send chat message to either thecurrently selected participant or the currently selected room.

The central edit area shows the chat history according to current selection of particpant or room.

We are done !

Let have a look at our server main body :

int _tmain(int argc, _TCHAR* argv[])
{
	PushFramework::Server server;
 
	server.registerService(DirectChatRequestID, new DirectChatRequestService, "directChat");
	server.registerService(JoinRoomRequestID, new JoinRoomRequestService, "joinRoom");
	server.registerService(LeaveRoomRequestID, new LeaveRoomRequestService, "leaveRoom");
	server.registerService(RoomChatRequestID, new RoomChatRequestService, "roomChat");
	server.registerService(LogoutRequestID, new LogoutRequestService, "logout");
 
	server.setClientFactory(new ChatParticipantFactory);
	server.setProtocol(new ChatServerProtocol);
 
	server.setListeningPort(2010);
	server.enableRemoteMonitor(2011, 10, "alibaba");
 
	//Broadcast channels :
	broadcastManager.CreateQueue("participants", 1000, false, 10, 20);
        broadcastManager.CreateQueue("signals", 20, false,  7, 1);
 
        broadcastManager.CreateQueue("room1", 20, true, 5, 3);
        broadcastManager.CreateQueue("room2", 20, true, 5, 3);
        broadcastManager.CreateQueue("room3", 20, true, 5, 3);
 
	server.start(true);
 
	int ch;
 
	do
	{
		ch = _getch();
		ch = toupper(ch);
	} while (ch !='Q');
 
	server.stop();
 
	return 0;
}

In this block of code, we bind our Service objects to the corresponding request ids. At Receive time, we provide the request id for each de-serialized request and the framework routes it to the corresponding Service object. Our server listens on the port 2010, and we setup up the needed broadcasting queue before calling start.

Share
Fax Online    Send article as PDF