Tuesday, August 7, 2012

A server socket class

Hello

I'm going to describe a new class, (last one before I start with the server)

We already have a class to store persistent data, now we need a way to communicate with the world.

I need a class to start a TCP socket server then accept incoming TCP connections , I need to be able to send data to any of the clients too, and finally enqueue the incoming data so I can process it when I need to do.

What the class is going to do is:
  1. Open a server socket in a port X
  2. Start a thread that will be listening in port X
  3. When we receive connections it will save the client info (ip/port) to a vector so we can communicate with the client, every client starts a new thread to get the incoming data from each client (it is a blocking-socket server)
  4. The data is stored into two different queues depending if the client has logged in  or not, I made that because I will apply different priority for the queues , the main (client updates) must be processed with maximum priority other way clients will be lagging.. in the other hand client login request can wait a pair of seconds without big problems.
To achieve that I use two classes, first one is cNetwork, it is the main server, second one os cClient, used to store information from every connected client, the header from both classes are stored in the same file:


<network.h>
#ifndef NETWORK_H
#define NETWORK_H

#define MAXLEN 1024
#include "SDL/SDL.h"
#include "SDL/SDL_net.h"
#include "SDL/SDL_thread.h"

#include <vector>
#include <queue>
#include <iostream>
#include <sstream>

using namespace std;

class cNetwork;

class cClient //the connected clients
{
public:
//functions
cClient();//creator
virtual ~cClient();//destructor
void StartThreads();//Start listener thread
static int listener( void *data );//Listener Thread
void Send(const char* Msg);//Send data to client

//variables
cNetwork *_server;//To comunicate with main server instance
TCPsocket socket;//client socket
SDL_Thread *ListenerThread;//The network client thread
int         threadReturnValueclient;
Uint16 UID;//Unique identifier
bool Connected;//socket connected?
bool Login; //loged in?

};

class cNetwork//main network server class
{
      friend class cClient;

    public:
        //functions
        cNetwork();
        virtual ~cNetwork();
        void Start(Uint16 MAXUsers, Uint16 Port);
        void CleanClient(unsigned int Client);//clean a client so we can use it
        static int Master( void *data );//Master listener function/thread
        void ClearClients();//Clear disconnected clients
        void Finish();//close all client connections and finish server

        //variables
        queue<std::string> InputQueue;//Here we store received data
        queue<std::string> LoginQueue;//Here we store login requests
        SDL_sem *ClientsLock; //Semaphore to prevent server from accesing a variable from different threads
        cNetwork *myserver;//server pointer
        vector<cClient*> Clients;//Here I store clients
        SDL_Thread *MasterThread;//The Master thread >> accepts new clients
        int         threadReturnMaster;//return value for master thread
        bool ShutDown;//closing the server?

    private:
        //private variables
        IPaddress ip;//Server ip address
        TCPsocket ServerSock; //Server socket
};

#endif // NETWORK_H


Then I have the client implementation:

<client.cpp>
#include "network.h"

cClient::cClient()
{
    //ctor
    ListenerThread = NULL;
    Connected = false;
}

cClient::~cClient()
{
    //dtor
    //Stop the thread
    SDL_KillThread( ListenerThread );
    //close the socket
    SDLNet_TCP_Close(socket);
}

void cClient::StartThreads()
{
    //Create and run the thread +
    ListenerThread = SDL_CreateThread( &listener, this );
}

int cClient::listener( void *data )
{ //While the program is not over
    cout << "Starting client listener Thread..." << endl;
    int result;
    char in_msg[MAXLEN];
    bool quit=false;

    while( quit == false )
        {
        memset(in_msg, 0, MAXLEN); // Clear in buffer
        if ( ((cClient*)data)->Connected)//If I'm connected...
            {
                result=SDLNet_TCP_Recv(((cClient*)data)->socket,in_msg,MAXLEN);//Check for incoming data
                    if(result<=0)
                        {
                            //NO DATA
                            // TCP Connection is broken. (because of error or closure)
                            SDLNet_TCP_Close(((cClient*)data)->socket);
                            cout << "Socket closed..." << endl;
                            ((cClient*)data)->Connected = false;
                            quit=true;
                        }

                stringstream idmsg;
                idmsg << ((cClient*)data)->UID << "#" << in_msg;// I will add the UID from the client to the incoming message
                try{
                    SDL_SemWait(((cClient*)data)->_server->ClientsLock);//lock smaphore to prevent client to freeze
                    cout << idmsg.str();//only for debuging

                    if  (((cClient*)data)-> Login==true)//Am I loged in?
                        {
                            ((cClient*)data)->_server->InputQueue.push(idmsg.str());  //I'm logged in
                        }else{
                            ((cClient*)data)->_server->LoginQueue.push(idmsg.str());  //New player requests
                        }
                    SDL_SemPost(((cClient*)data)->_server->ClientsLock);//unlock smaphore
                    }
                catch(exception& e)
                    {
                        cout << "QUEUE IN ERROR: " << e.what() << endl;
                    }
            }//if connected
        }//while
    cout << "Closing client listener thread..." <<endl;
    return 0;
}

void cClient::Send(const char* Msg)
{
    if (Connected==true)
        {
            int len;
            int result;
            len=strlen(Msg+1); // add one for the terminating NULL
            result=SDLNet_TCP_Send(socket,Msg,len);
            if(result<len) //If I can't send data probably I've been disconnected so....
                {
                    cout << "SDLNet_TCP_Send: " << endl << SDLNet_GetError();
                    Connected = false;
                }
        }else{
            cout << "Not connected!!" << endl;
            }
}


Notice the client stores the data in two different queues depending if the client has loged in or not, finally we have the main socket server class:

<network.cpp>
#include "network.h"

cNetwork::cNetwork()
{
    //ctor
if(SDLNet_Init()==-1) {//Start SDL_NET
cout << "SDLNet_TCP_INIT: \n " << SDLNet_GetError();
exit(2);
}

//Clean thread variables
MasterThread = NULL;
ClientsLock = NULL;
ClientsLock = SDL_CreateSemaphore( 1 );//semafor protector, previene que mas de un thread vuelque informaciĆ³n a la vez a la cola
}

cNetwork::~cNetwork()
{
    //dtor
SDLNet_TCP_Close(ServerSock);//Close socket
SDLNet_Quit();//Close SDL_NET
}


void cNetwork::CleanClient(unsigned int Client)//clean a client so we can use it
{
//cout << "cleaning client: " << Client << endl;
Clients[Client]->UID =Client;
Clients[Client]->socket = NULL;
Clients[Client]->_server = myserver;
Clients[Client]->Connected = false;
Clients[Client]->Login = false;
}

void cNetwork::Start(Uint16 MAXUsers, Uint16 Port)
{

//1-initialize MAXUsers clients
//2-Start sock server

unsigned int x;
for (x=1;x<=MAXUsers;x++)
{
Clients.push_back(new cClient());//insert a new client into the clients vector
CleanClient(Clients.size() -1);
}

cout << "OPENING SERVER SOCKET... " << endl;
if(SDLNet_ResolveHost(&ip,NULL,Port)==-1) {
cout << "SDLNet_TCP_ResolveHost:" << endl  << SDLNet_GetError();
exit(1);
}

ServerSock=SDLNet_TCP_Open(&ip);
if(!ServerSock) {
cout << "SDLNet_TCP_open: " << endl << SDLNet_GetError();
exit(2);
}

ShutDown = false;
//Create and run the threads
MasterThread = SDL_CreateThread( &Master, this );

}



int cNetwork::Master( void *data )//Master listener function/thread
{
TCPsocket new_tcpsock; //Temporary socket to store incoming connections
new_tcpsock=NULL;

cout << "Waiting for incoming connections... " << endl;

bool doneMain=false;
while(!doneMain)//MAIN LOOP
    {
    if(!new_tcpsock)//We have a new client incoming
    {
        new_tcpsock=SDLNet_TCP_Accept(((cNetwork*)data)->ServerSock); // accept a connection coming in on server_tcpsock
        SDL_Delay(5);//No new clients, wait a little
    }else{
        cout << "New client incoming..." << endl;
        unsigned int x;
        for(x=0;x<((cNetwork*)data)->Clients.size();x++)
        {
        if (((cNetwork*)data)->Clients[x]->Connected==false)
            {
            ((cNetwork*)data)->CleanClient(x);
            ((cNetwork*)data)->Clients[x]->socket=new_tcpsock;//asign the socket
            ((cNetwork*)data)->Clients[x]->Connected = true;
            ((cNetwork*)data)->Clients[x]->Login = false;
            ((cNetwork*)data)->Clients[x]->StartThreads();//start client listener thread
            break;
            }
    }//for
        new_tcpsock=NULL;//release temporary socket var

        }//if new data

if (((cNetwork*)data)->ShutDown==true)doneMain =true;

}//while
cout << "Exiting Main thread..." << endl;
return 0;
}

void cNetwork::ClearClients()//Clear disconnected clients
{
unsigned int x;
bool done = false;
//SDL_SemWait(ClientsLock);
if (Clients.size()>0)
{
x=0;
while (!done)
{
 if (!Clients[x]->Connected)
            {
            Clients.erase(Clients.begin()+x);
           // cout << "Number of clients:" << Clients.size() << endl;
            if (x>0)x--;
            }//if !connected
x++;
if(x>=Clients.size())done=true;
}
}//clients size

}

void cNetwork::Finish()
{
    ShutDown =true;
    SDL_WaitThread(MasterThread,&threadReturnMaster);
    unsigned int x;
    for(x=0;x<Clients.size();x++)
        {
            Clients[x]->Connected=false;//force disconnection
        }//for

    while (Clients.size()>0)
        {
            ClearClients();
        }
}


There are many things here, but I will resume it to the maximum, to do that, below there is a simple example to use the class:

#include "network.h"//socket server class 
 
cNetwork NET;
NET.myserver = &NET;
NET.Start(100,55555); //start networking, up to 100 clients on port 55555
 
std::sting text;
SDL_SemWait( NET.ClientsLock );
text = NET.InputQueue.front();//take a message out from the queue
NET.InputQueue.pop();
SDL_SemPost( NET.ClientsLock ); 
 
NET.Clients[X]->Send("data");//will send "data" to client X
 
NET.Finish();//finish socket server 

  • We include the class
  • Then instantiate it
  • Start the listener thread (at this moment it is fully functional)
  • Then I extract a message from the input queue (let's suppose we have a connected client that send it..)
  • I send a message to the server ("data")
  • Finally, I close the server
Want to test the class? basically start an empty project, add the three files I have described above and then add the example, after you compile the project you will be able to connect just using telnet (telnet localhost 55555)

That's all for now, in the next post I will mix everything to make the client login a reality, I will make a text based client too and hope everything is clear that way.


No comments: