/*! \file server.cpp This is the Server class implementation. It implements a standard non-blocking select() server. \author Jon A. Lambert \date 05/02/2003 \version 0.30 */ #include "sysconfig.h" #include "server.h" #include "event.h" /*! Constructor for Server */ Server::Server(EventQueue& r_inque, EventQueue& r_outque, Log& r_lgfile) : mShutdown(false), mrLog(r_lgfile), mrInQueue(r_inque), mrOutQueue(r_outque) { } /*! Destructor for Server \note If we shutdown normally we should not have any connections in our list but we will attempt to delete them anyways. Maybe we should assert this. */ Server::~Server() { ConnList::iterator i; for (i = mConns.begin(); i != mConns.end(); i++) { delete (*i); } } /*! Boot initializes the Server and gets it ready to accept incoming Connections. \return true if server boots correctly, false if an error occurs. */ bool Server::Boot(unsigned short port) { // Get a TCP protocol socket for the server to listen on. mAcceptor = socket (AF_INET, SOCK_STREAM, 0); if (mAcceptor == INVALID_SOCKET) { ServerLog().Write("ERROR-Server(socket): %d", WSAGetLastError()); return false; } // Set the listening socket to be non-blocking if (!SetNonBlocking(mAcceptor)) return false; // Set up listening socket to allow local address reuse int sockopt_val = 1; // Non-zero means turn on for boolean options, // some options require other types and values. if (setsockopt(mAcceptor, SOL_SOCKET, SO_REUSEADDR, (char *)&sockopt_val, sizeof(sockopt_val)) == SOCKET_ERROR) { ServerLog().Write("ERROR-Server(setsockopt): %d",WSAGetLastError()); closesocket(mAcceptor); return false; } // Associate our local address with this socket mTo.sin_family = AF_INET; // protocol - internet mTo.sin_addr.s_addr = INADDR_ANY; // accept on whatever IP we're set up as mTo.sin_port = htons(port); // port must be in network byte order if (bind(mAcceptor, (struct sockaddr *)&mTo, sizeof(mTo)) == SOCKET_ERROR) { ServerLog().Write("ERROR-Server(bind): %d",WSAGetLastError()); closesocket(mAcceptor); return false; } // Let's start listening for incoming connections if (listen(mAcceptor, 5) == SOCKET_ERROR) { // Connection backlog of 5. ServerLog().Write("ERROR-Server(listen): %d",WSAGetLastError()); closesocket(mAcceptor); return false; } /* mNumFds is calculated for the select() function - Windows ignores it as the underlying FD sets are constructed differently than Unixes On Unixes FDs are include all files, pipes and sockets open. We need to calculate the highest FD number for the Unix select. In bootup the highest FD is the listening socket so... */ mNumFds = mAcceptor; return true; } /*! Run starts the Server running to process incoming connection, input and output requests. It also executes commands from input requests. /param parms Not used. /note This should be where we begin threading. Not in Boot. /todo Polling time should runtime reconfigurable. */ #pragma argsused void Server::Run(void * parms) { ServerLog().Write("INFO-StartServer(): Server thread entered."); struct timeval timeout = { 0, 1 }; // Will poll for 1 millisecond while (!mShutdown) { // Loops until a SHUTDOWN_E message is received. ConnList::iterator i, curr; // Reset our socket interest set InitFDs(); // Poll our socket interest set for about a millisec. if (select(mNumFds + 1, &mInputFDs, &mOutputFDs, NULL, &timeout) == SOCKET_ERROR) { ServerLog().Write("ERROR-Server(select): %d",WSAGetLastError()); break; } // If we have incoming connections on our listening port handle them. if (FD_ISSET(mAcceptor, &mInputFDs)) { AcceptConnection(); } // If we have incoming data on any connections handle it. for(i = mConns.begin(); i != mConns.end(); i++) { if (FD_ISSET((*i)->GetSocket(), &mInputFDs)) { (*i)->HandleInput(); // Loop through fully formed messages (CRLF). string* msg = (*i)->ReadMsg(); while(msg) { // notify the chat driver. mrOutQueue.Push(new Event(MESSAGE_E, (*i)->GetSocket(), msg->length(), msg->c_str())); delete msg; // ReadMsg creates a new string - we must delete it. msg = (*i)->ReadMsg(); } // while } // if } // for // Process the event queue. ProcessQueue(); // If we have outgoing data ready on any connections handle it. for(i = mConns.begin(); i != mConns.end(); i++) { if (FD_ISSET((*i)->GetSocket(), &mOutputFDs)) (*i)->HandleOutput(); } // Cleanup sockets that have been scheduled for disconnection. for(i = mConns.begin(); i != mConns.end();) { curr = i++; if ((*curr)->CanBeDisconnected()) { (*curr)->Disconnect(); // Tell the chat driver about it. mrOutQueue.Push(new Event(DISCONNECT_E, (*curr)->GetSocket(), 0, NULL)); delete (*curr); mConns.erase(curr); } } } // while ShutdownServer(); ServerLog().Write("INFO-StartServer(): Server thread exited."); } /*! AcceptConnection handles an incoming connection request on the server's listening port by creating a Connection object for the socket. \remarks Dang this code looks dense. */ void Server::AcceptConnection() { struct sockaddr_in addr; // An address structure for our incoming connection. int addrlen = sizeof(addr); // Try to accept the connection SOCKET psock = accept(mAcceptor, (struct sockaddr *)&addr, &addrlen); if (psock != INVALID_SOCKET) { // Set the socket to non-blocking. if (SetNonBlocking(psock)) { // The socket set up correctly. Let us add a new Connection. Connection *conn = new Connection(psock, this); ServerLog().Write("INFO-Server(accept): Connection %d accepted", psock); mConns.push_back(conn); // Put it on the connection list. // Tell the chat server we have a new connection mrOutQueue.Push(new Event(CONNECT_E, conn->GetSocket(), 0, NULL)); } } else { // Weve encountered an error during accept. ServerLog().Write("ERROR-ServerAccept(accept): %d", WSAGetLastError()); } } /*! InitFDs initializes the server's select queue and then re-registers interest in all the connection's sockets in the server's user list including the server's listening port itself. \note It may well be more efficient to register or unregister interest on a connection by connection basis in the various connection handling routines. \remarks [1] On Windows, setting interest in all connected sockets for output will cause our select loop to spin. So we only set it if we know there is output waiting. */ void Server::InitFDs () { FD_ZERO (&mInputFDs); // Clears our interest sets out. FD_ZERO (&mOutputFDs); FD_SET (mAcceptor, &mInputFDs); // Register interest in listening socket. for(ConnList::iterator i = mConns.begin(); i != mConns.end(); i++) { /* mNumFds is calculated for the select() function - Windows ignores it as the underlying FD sets are constructed differently than Unixes On Unixes FDs are include all files, pipes and sockets open. We need to calculate the highest FD number for the Unix select. */ mNumFds = max(mNumFds, (*i)->GetSocket()); FD_SET((*i)->GetSocket(), &mInputFDs); // Always interested in input. if ((*i)->HasOutput()) // See [1] above. FD_SET((*i)->GetSocket(), &mOutputFDs); } } /*! ProcessQueue attempts to process all the events contained in the eventqueue coming from our chat engine. \remarks The are only two kinds of messages. - @shutdown - requests the server to shutdown - everything else is interpreted as public conversation \todo Implement a disconnect request coming from driver. Allow specialized Telnet commands to come from driver, and filter them through the connection depending on whether it is capable. */ void Server::ProcessQueue() { // manage event queue Event* e; while ((e = mrInQueue.Pop()) != NULL) { switch (e->mEventType) { case DISCONNECT_E: // We've been told by the chat driver to kill a connection. { // Not yet implemented. } break; case CONNECT_E: // Should never happen - included for completeness break; case MESSAGE_E: // A message from the chat driver to route to a connection here. { // Test for null message data and ignore it if (e->mpData && e->mDataLen) { string msg; msg.append(e->mpData, e->mDataLen); for(ConnList::iterator c = mConns.begin(); c != mConns.end(); c++) { if ((*c)->GetSocket() == e->mClientId) { (*c)->SendMsg(msg); // Register interest as we have data ready on the wire FD_SET ((*c)->GetSocket(), &mOutputFDs); } } } } break; case SHUTDOWN_E: // We've been requested to shutdown so let's comply. mShutdown = true; break; case NONE_E: break; } // switch delete e; } // while } /*! ShutdownServer requests each of the connections to disconnect in the server's user list, deletes the connections, and erases them from the user list. It then closes its own listening port. */ void Server::ShutdownServer() { ServerLog().Write("INFO-Server(shutdown): Server shutting down"); ConnList::iterator ci, curr; for(ci = mConns.begin(); ci != mConns.end();) { curr = ci++;// :WARN: The list containers iterator is invalidated on erase. // So we need to increment it first and delete the old. (*curr)->Disconnect(); // Tell chat driver about each connection as it leaves mrOutQueue.Push(new Event(DISCONNECT_E, (*curr)->GetSocket(), 0, NULL)); delete (*curr); mConns.erase(curr); } closesocket(mAcceptor); mrOutQueue.Push(new Event(SHUTDOWN_E, 0, 0, 0)); } /*! ServerLog returns a reference to this server's logging utility so that messages can be written to it by its associates and aggregates. \return a reference to this server's Log */ Log& Server::ServerLog() { return mrLog; } /*! ServerLog returns a reference to this server's logging utility so that messages can be written to it by its associates and aggregates. \param s The socket to be set non-blocking. \return true if successful, false if unsuccessful. */ bool Server::SetNonBlocking(SOCKET s) { unsigned long ioctl_cmd = 1; // Non-zero means turn on for ioctl commands. if (ioctlsocket(s, FIONBIO, &ioctl_cmd) == SOCKET_ERROR) { ServerLog().Write("ERROR-Server(SetNonBlocking): %d on socket %d", WSAGetLastError(), s); closesocket(s); return false; } return true; }