19.4 SERVER SOCKETS IN C (Qt)


19.4 SERVER SOCKETS IN C++ (Qt)

We will now show how you can establish in C++ a server socket bound to a specific port in the same manner as we showed in Section 19.2 for Java. This we will do with the help of the Qt class QServerSocket. As mentioned earlier in Section 19.2, a server uses a server-socket object to monitor a designated port for incoming connection requests from clients. When a client request for a connection is received on the designated port monitored by the server, the server-socket object creates a new socket for communicating with the client. The server-socket then resumes its business of monitoring the designated port for fresh requests from other clients.

The rest of this section parallels Section 19.2 where we presented a Java chat server program to demonstrate the server side of the programming needed for a client-server link. As with the Java program, the chat server program in this section will consist of the main server class, ChatServer, and a ClientHandler class whose objects will keep track of the individual clients participating in a chat. The ChatServer is derived from the Qt class QServerSocket:

     class ChatServer : public QServerSocket {       // .     public:       vector<ClientHandler> clientVector;                    //(A)       ChatServer(int port);                                  //(B)       void newConnection(int socketFD);                      //(C)       ChatServer(); }; 

This makes every ChatServer object a QServerSocket object. As we did with the ServerSocket class in Java in Section 19.2, we can now construct a C++ server socket object by, say,

 ChatServer* server = new ChatServer(5000); 

if we wish for the server to monitor port 5000 for client requests.

While creating server socket objects in C++ with Qt and in Java entail very similar syntax, what it takes to get hold of the actual socket that a server will use for communicating with a client is different in the two cases. In Java, we invoked the accept method on a server socket object for this purpose; receipt of an appropriately formatted client request caused accept to spit out the socket. In Qt, this is achieved by providing an override definition for the virtual function newConnection(int socketFd). Through the mechanism of virtual functions—in the same manner that low-level events are handled in a Qt GUI program (see Chapter 17)—when a new client request for a communication link is received, the programmer supplied definition of newConnection(int socketFD) is automatically invoked with the argument set to a socket file descriptor for the new link. The ChatServer class has the following definition for this virtual function:

     void ChatServer::newConnection(int socketFD) {        QSocket* socket = new QSocket();                 //(D)        socket->setSocket(socketFD);                     //(E)        ClientHandler* clh = new ClientHandler(socket, this);        cout << "A new client checked in on socket FD "                        << socketFD << endl; } 

So, as shown in line (E), it is the setSocket function that, when invoked on an already constructed QSocket object of line (D), yields the socket that the server will use for communicating with the client. This socket is provided to the ClientHandler constructor. As with the Java program, there is a separate ClientHandler object for each client. All ClientHandler objects are stored in the vector data member clientVector shown in line (A) of the ChatServer class shown above. Part of the job of each ClientHandler object is to transmit its client's chat to all the other clients stored in the clientVector data member. Here is a partial definition of ClientHandler:

        class ClientHandler : public QObject {            // . . .            QSocket* handlerSocket;                                  //(F)            QString* chatName;                                       //(G)            QTextStream* os;                                         //(H)            ChatServer* chatServer;                                  //(I)            // . . .        }; 

The data member handlerSocket in line (F) is the QSocket object constructed by the ChatServer for this client. Each chat participant must have a name that becomes the value of the data member chatName in line (G). The member chatServer in line (I) serves as a back pointer to the ChatServer object that constructed this ClientHandler object. The data member os in line (H) is an I/O stream that a ClientHandler object uses primarily for broadcasting the strings received from each client to all other clients.

In addition to the usual complement of constructors, the ClientHandler class must be provided with a copy constructor and a copy assignment operator so that the ClientHandler objects can be stored in the vector data member clientVector of the ChatServer class:

      class ClientHandler : public QObject {          // . . . .          ClientHandler(QSocket* sock, ChatServer* cserver);          ClientHandler();          ClientHandler(const ClientHandler& cl);                    //(J)          ClientHandler&operator=(const ClientHandler&other);        //(K)      ~ClientHandler();      // ...  }; 

As mentioned in Chapter 5, when an object is inserted into a container class, it is a copy of the object as constructed by the copy assignment operator that is actually stored.

Finally, in order to read the information coming through a client socket, the ClientHandler object must respond to the readyRead signal by invoking an appropriate slot function. This slot function in our case is named readFromClient in line (L) below. It is one of the two slot functions defined for ClientHandler, the other being reportError in line (M):

     class ClientHandler : public QObject {          // . . . .     private slots:         void readFromClient();                                 //(L)         void reportError(int);                                 //(M)   }; 

After a connection is established with a client and a ClientHandler object constructed for the client, all the interaction with the client takes places through the readFromClient function of the ClientHandler class. This function works in three different modes:

  1. If chatName's value has not yet been set, use the first input supplied by the client as the chat name since this input will be provided in response to a specific request (for a chat name) from the server. After a new client has checked in and supplied his/her chat name, a message is broadcast to all the other clients to that effect.

  2. If a client has typed "bye" on his/her terminal, that means the client wants to sign out. This requires closing the socket for this client and broadcasting a message to all the other clients to that effect.

  3. Otherwise, broadcast the string received from the client to all the other clients.

  4. Finally, make sure that a fresh line on the client terminal begins with his/her own chat name followed by a colon.

The program shown below consists of three files

      ChatServer.h      ChatServer.cc      Makefile_ChatServer 

The header file ChatServer.h declares the ChatServer and the ClientHandler classes:

 
//ChatServer.h #ifndef CHATSERVER_H #define CHATSERVER_H #include <qserversocket.h> #include <qsocket.h> #include <qtextstream.h> #include <qstring.h> #include <vector> using namespace std; class ChatServer; class ClientHandler : public QObject { Q_OBJECT QSocket* handlerSocket; QString* chatName; ChatServer* chatServer; QTextStream* os; public: ClientHandler(QSocket* sock, ChatServer* cserver); ClientHandler(); ClientHandler(const ClientHandler& cl); ClientHandler& operator=(const ClientHandler* other); ~ClientHandler(); private slots: void readFromClient(); void reportError(int); }; class ChatServer : public QServerSocket { Q_OBJECT public: vector<ClientHandler> clientVector; ChatServer(int port); void newConnection(int socketFD); ~ChatServer(); }; #endif

The classes are implemented in the ChatServer.cc file:

 
//ChatServer.cc #include "ChatServer.h" #include <qapplication.h> #include <iostream> using namespace std; ChatServer::ChatServer(int port) : QServerSocket(port) { cout << "Server monitoring port" << port << endl; if (!ok()) { qWarning("Failed to register the server port"); exit(1); } } // You must provide an override implementation for // this method. When a client requests a connection, // this method will be called automatically with the // socket argument set to the filedescriptor associated // with the socket. void ChatServer::newConnection(int socketFD) { QSocket* socket = new QSocket(); socket->setSocket(socketFD); ClientHandler* clh = new ClientHandler(socket, this); cout << "A new client checked in on socket FD" << socketFD << endl; } ChatServer::ChatServer(){} ClientHandler::ClientHandler() {} // Copy constructor is needed since it is the copies of // the ClientHandler objects that will be stored in the // vector clientVector ClientHandler::ClientHandler(const ClientHandler& other) : handlerSocket(other.handlerSocket), chatName(other.chatName), chatServer(other.chatServer), os(other.os) {} ClientHandler& ClientHandler::operator=(const ClientHandler& other) { if (this == &other) return *this; cout << "ClientHandler assignment op invoked" << endl; if (handlerSocket != 0) delete handlerSocket; handlerSocket = other.handlerSocket; if (chatName != 0) delete chatName; chatName = other.chatName; if (os != 0) delete os; os = other.os; chatServer = other.chatServer; {} ClientHandler::ClientHandler(QSocket* socket, ChatServer* chatserver) : chatName(0), chatServer(chatserver), handlerSocket(socket) { os = new QTextStream(handlerSocket); (*os) << "Welcome to a chat room powered by C++\n"; (*os) << ">>>>Enter ‘bye‘ to exit <<<n"; { (*os) << "Enter chat name:"; connect(handlerSocket, SIGNAL(readyRead()), this, SLOT(readFromClien())); connect(handlerSocket, SIGNAL(error(int)), this, SLOT(reportError(int))); } // The destructor definition intentionally does not invoke // the delete operator on any of the objects pointed to // by the data members of a ClientHandler. In this program, // the most frequent invocation of the destructor is caused // by the push_back statement in the readFromClient() // function. The push_back invocation causes the vector // to be moved to a different location in the memory. The // memory occupied by the ClientHandler objects in the // vector is freed by invoking the destructor. Deleting // the memory occupied by the socket and other objects // pointed to by the data members of the ClientHandler // objects would lead to disastrous results. ClientHandler::~ClientHandler(){} void ClientHandler::reportError(int e) { cout << "error report from connectToHost" << endl; cout << "error id: " << e << endl; } void ClientHandler::readFromClient() { QSocket* sock = (QSocket*) sender(); while (sock->canReadLine()) { QString qstr = sock->readLine(); // This block is for the case when a new chatter // has just signed in and supplied his/her chat name. // The block sets the chatname of the ClientHandler // object assigned to this new user. Next it pushes // the ClientHandler object for this new user in the // vector clientVector. Subsequently, The block informs // all other current chatters that this new user has // signed in. if (chatName == 0) { chatName = new QString(qstr.stripWhiteSpace(); chatServer->clientVector.push_back(*this); for (int i=0; i<chatServer-*gt;clientVector.size(); i++) { if (*chatServer->clientVector[i].chatName != *chatName && chatServer->clientVector[i].handlerSocket != 0) { QString outgoing = "\nMessage from chat server: " + *chatName + "signed in"; *chatServer->clientVector[i].os << outgoing; } } } // This block treats the case when a chatter wants // to sign out by typing "bye". It broadcasts a message // to all the other chatters that this chatter is signing // off. This block than closes the socket. Note that // socket pointer is set to null in both the ClientHandler // object assigned to the exiting chatter and its copy the // vector clientVector. else if (qstr.stripWhiteSpace() == "bye") { for (int i=0; i<chatServer->clientVector.size(); i++) { QString outgoing("\nMessage from the chat server:" + *chatName + "signed off"); if (*chatServer->clientVector[i].chatName != *chatName && chatServer->clientVector[i].handlerSocket != 0) { *chatServer->clientVector[i].os << outgoing; } } handlerSocket->close(); handlerSocket = 0; for (int i=0; i<chatServer->clientVector.size(); i++) { if (*chatServer->clientVector[i].chatName == *chatName) chatServer->clientVector[i].handlerSocket = 0; } } // This is the normal case encountered during the // course of a chat. The string typed in by a // chatter is broadcast to all the other chatters. // The string is pre-pended by the name of the // chatter who typed in the string. else { cout << *chatName << ": " << qstr << endl; qstr.truncate(qstr.length() - 2); for (int i=0; i<chatServer->clientVector.size(); i++) { if (*chatServer->clientVector[i].chatName != *chatName && chatServer->clientVector[i].handlerSocket != 0) { QString outgoing = "\n" + *chatName + ": " + qstr; *chatServer->clientVector[i].os << outgoing; } } } // A chatter's terminal always shows his/her own // name at beginning of a new line. This way, // when a chatter types in his/her own message, it // is always on a line that starts with his/her // own name. for (int i=0; i<chatServer->clientVector.size(); i++) { if (chatServer->clientVector[i].handlerSocket != 0) { QString outgoing = "\n" + *chatServer->clientVector[i].chatName + ": "; *chatServer->clientVector[i].os << outgoing; } } } } int main(int argc, char* argv[]) { QApplication app(argc, argv); ChatServer* server = new ChatServer(5000); return app.exec(); }

Finally, here is the makefile:

 
#Makefile_ChatServer CC=g++ LDLIBS=-L$(QTDIR)/lib -lqt CFLAGS=-g -I$(QTDIR)/include ChatServer: moc_ChatServer.o ChatServer.o Makefile_ChatServer $(CC) $(LDLIBS) -o ChatServer moc_ChatServer.o ChatServer.o moc_ChatServer.cc: ChatServer.h moc -o moc_ChatServer.cc ChatServer.h moc_ChatServer.o: moc_ChatServer.cc $(CC) -c $(CFLAGS) -02 moc_ChatServer.cc ChatServer.o: ChatServer.cc ChatServer.h $(CC) -c $(CFLAGS) -02 ChatServer.cc clean: rm -f ChatServer rm -f *.o rm -f moc*.*

You can run the executable ChatServer on the machine on which you wish to host the chat server. Chat clients can then connect with the server via telnet by entering the following string in their terminals:

     telnet <name of server machine> 5000 

assuming that the port being monitored by the chat server is 5000. For experimenting with the server program on a local machine, you can run the server in one window and clients in other windows using the loopback address for the local machine. In the client windows, you'd enter the following string to establish a connection with the server:

     telnet 127.0.0.1 5000 




Programming With Objects[c] A Comparative Presentation of Object-Oriented Programming With C++ and Java
Programming with Objects: A Comparative Presentation of Object Oriented Programming with C++ and Java
ISBN: 0471268526
EAN: 2147483647
Year: 2005
Pages: 273
Authors: Avinash Kak

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net