Rather than creating a fixed-size thread pool, we now create server threads on demand.
Every time the server accepts a client connection, it creates a server
worker thread, and the thread terminates when the client quits. The server creates a separate accept thread so that the main thread can poll the global shutdown flag while the accept
call is blocked. While it is possible to specify nonblocking sockets,
threads provide a convenient and uniform solution. It’s worth noting
that a lot of the extended Winsock functionality is designed to support
asynchronous operation, and Windows threads allow you to use the much
simpler and more standard synchronous socket functionality. The thread management is improved, at the cost of some complexity, so that the state of each thread is maintained. This server also supports in-process servers
by loading a DLL during initialization. The DLL name is a command line
option, and the server thread first tries to locate an entry point in
the DLL. If successful, the server thread calls the DLL entry point;
otherwise, the server creates a process, as in serverNP.
The DLL needs to be trusted, however, because any unhandled exception
could crash or corrupt the server, as would changes to the environment.
Program 1. serverSK: Socket-Based Server with In-Process Servers
/* Chapter 12. Client/server. SERVER PROGRAM. SOCKET VERSION. */ /* Execute the command in the request and return a response. */ /* Commands will be executed in process if a shared library */ /* entry point can be located, and out of process otherwise. */ /* ADDITIONAL FEATURE: argv [1] can be name of a DLL supporting */ /* in-process servers. */
#include "Everything.h" #include "ClientServer.h"/* Defines the request and response records. */
struct sockaddr_in srvSAddr;/* Server's Socket address structure */ struct sockaddr_in connectSAddr;/* Connected socket */ WSADATA WSStartData; /* Socket library data structure */
enum SERVER_THREAD_STATE {SERVER_SLOT_FREE, SERVER_THREAD_STOPPED, SERVER_THREAD_RUNNING, SERVER_SLOT_INVALID};
typedef struct SERVER_ARG_TAG { /* Server thread arguments */ CRITICAL_SECTION threadCs; DWORDnumber; SOCKET sock; enum SERVER_THREAD_STATE thState; HANDLE hSrvThread; HINSTANCE hDll; /* Shared library handle */ } SERVER_ARG;
static BOOL ReceiveRequestMessage (REQUEST *pRequest, SOCKET); static BOOL SendResponseMessage (RESPONSE *pResponse, SOCKET); static DWORD WINAPI Server (PVOID); static DWORD WINAPI AcceptThread (PVOID); static BOOL WINAPI Handler (DWORD);
volatile static int shutFlag = 0; static SOCKET SrvSock = INVALID_SOCKET, connectSock = INVALID_SOCKET;
int main (int argc, LPCTSTR argv []) { /* Server listening and connected sockets. */ DWORD iThread, tStatus; SERVER_ARG sArgs[MAX_CLIENTS]; HANDLE hAcceptThread = NULL; HINSTANCE hDll = NULL; /* Console control handler to permit server shutdown */ SetConsoleCtrlHandler (Handler, TRUE);
/* Initialize the WS library. Ver 2.0 */ WSAStartup (MAKEWORD (2, 0), &WSStartData);
/* Open command library DLL if it is specified on command line */ if (argc > 1) hDll = LoadLibrary (argv[1]);
/* Initialize thread arg array */ for (iThread = 0; iThread < MAX_CLIENTS; iThread++) { InitializeCriticalSection (&sArgs[iThread].threadCs); sArgs[iThread].number = iThread; sArgs[iThread].thState = SERVER_SLOT_FREE; sArgs[iThread].sock = 0; sArgs[iThread].hDll = hDll; sArgs[iThread].hSrvThread = NULL; } /* Follow standard server socket/bind/listen/accept sequence */ SrvSock = socket(PF_INET, SOCK_STREAM, 0);
/* Prepare the socket address structure for binding the server socket to port number "reserved" for this service. Accept requests from any client machine. */
srvSAddr.sin_family = AF_INET; srvSAddr.sin_addr.s_addr = htonl(INADDR_ANY); srvSAddr.sin_port = htons(SERVER_PORT); bind (SrvSock, (struct sockaddr *)&srvSAddr, sizeof(srvSAddr)); listen (SrvSock, MAX_CLIENTS);
/* Main thread becomes listening/connecting/monitoring thread */ /* Find an empty slot in the server thread arg array */ while (!shutFlag) { iThread = 0; while (!shutFlag) { /* Continuously poll thread State of all server slots */ EnterCriticalSection(&sArgs[iThread].threadCs); __try { if (sArgs[iThread].thState == SERVER_THREAD_STOPPED) { /* stopped, either normally or a shutdown request */ /* Wait for it to stop, and free the slot */ WaitForSingleObject(sArgs[iThread].hSrvThread, INFINITE); CloseHandle (sArgs[iThread].hSrvThread); sArgs[iThread].hSrvThread = NULL; sArgs[iThread].thState = SERVER_SLOT_FREE; } /* Free slot or shut down. Use slot for new connection */ if (sArgs[iThread].thState == SERVER_SLOT_FREE ||shutFlag) break; } __finally { LeaveCriticalSection(&sArgs[iThread].threadCs); }
iThread = (iThread++) % MAX_CLIENTS; if (iThread == 0) Sleep(50); /* Break the polling loop */ /* An alternative: use an event to signal a free slot */ } if (shutFlag) break; /* sArgs[iThread] == SERVER_SLOT_FREE */ /* Wait for a connection on this socket */ /* Use a separate accept thread to poll the shutFlag flag */ hAcceptThread = (HANDLE)_beginthreadex (NULL, 0, AcceptThread, &sArgs[iThread], 0, NULL); while (!shutFlag) { tStatus = WaitForSingleObject (hAcceptThread, CS_TIMEOUT); if (tStatus == WAIT_OBJECT_0) { /* sArgs[iThread] == SERVER_THREAD_RUNNING */ if (!shutFlag) { CloseHandle (hAcceptThread); hAcceptThread = NULL; } break; } } } /* OUTER while (!shutFlag) */
/* shutFlag == TRUE */ _tprintf(_T("Shutdown in process. Wait for server threads\n")); /* Wait for any active server threads to terminate */ /* Try continuously as some threads may be long running. */
while (TRUE) { int nRunningThreads = 0; for (iThread = 0; iThread < MAX_CLIENTS; iThread++) { EnterCriticalSection(&sArgs[iThread].threadCs); __try { if ( sArgs[iThread].thState == SERVER_THREAD_RUNNING || sArgs[iThread].thState == SERVER_THREAD_STOPPED) { if (WaitForSingleObject (sArgs[iThread].hSrvThread, 10000) == WAIT_OBJECT_0) { CloseHandle (sArgs[iThread].hSrvThread); sArgs[iThread].hSrvThread = NULL; sArgs[iThread].thState = SERVER_SLOT_INVALID; } else if (WaitForSingleObject (sArgs[iThread].hSrvThread, 10000) == WAIT_TIMEOUT) { nRunningThreads++; } else { _tprintf(_T("Error waiting: slot %d\n"), iThread); } } } __finally { LeaveCriticalSection(&sArgs[iThread].threadCs);} } if (nRunningThreads == 0) break; }
if (hDll != NULL) FreeLibrary (hDll);
/* Redundant shutdown */ shutdown (SrvSock, SD_BOTH); closesocket (SrvSock); WSACleanup(); if (hAcceptThread != NULL) WaitForSingleObject(hAcceptThread, INFINITE)); return 0; }
static DWORD WINAPI AcceptThread (PVOID pArg) { LONG addrLen; SERVER_ARG * pThArg = (SERVER_ARG *)pArg;
addrLen = sizeof(connectSAddr); pThArg->sock = accept (SrvSock, (struct sockaddr *)&connectSAddr, &addrLen);
/* A new connection. Create a server thread */ EnterCriticalSection(&(pThArg->threadCs)); __try { pThArg->hSrvThread = (HANDLE)_beginthreadex (NULL, 0, Server, pThArg, 0, NULL); pThArg->thState = SERVER_THREAD_RUNNING; } __finally { LeaveCriticalSection(&(pThArg->threadCs)); } return 0; }
BOOL WINAPI Handler (DWORD CtrlEvent) { /* Shutdown the program */ _tprintf (_T("In console control handler\n")); InterlockedIncrement (&shutFlag); return TRUE; }
|
In-process servers could have been included in serverNP
if desired. The biggest advantage of in-process servers is that no
context switch to a different process is required, potentially
improving performance. The disadvantage is that the DLL runs in the
server process and could corrupt the server, as described in the last
bullet. Therefore, use only trusted DLLs.
The server code is Windows-specific, unlike the client, due to thread management and other Windows dependencies.
The Main Program
Program 1
shows the main program and the thread to accept client connections. It
also includes some global declarations and definitions, including an
enumerated type, SERVER_THREAD_STATE, used by each individual server thread.
Program 2
shows the server thread function; there is one instance for each
connected client. The server state can change in both the main program
and the server thread.
Program 2. serverSK: Server Thread Code
static DWORD WINAPI Server (PVOID pArg)
/* Server thread function. One thread for every potential client. */ { /* Each thread keeps its own request, response, and bookkeeping data structures on the stack. */ BOOL done = FALSE; STARTUPINFO startInfoCh; SECURITY_ATTRIBUTES tempSA = {. . .}; /* Inheritable handles */ PROCESS_INFORMATION procInfo; SOCKET connectSock; int commandLen; REQUEST request;/* Defined in ClientServer.h */ RESPONSE response;/* Defined in ClientServer.h.*/ char sysCommand[MAX_RQRS_LEN], tempFile[100]; HANDLE hTmpFile; FILE *fp = NULL; int (__cdecl *dl_addr)(char *, char *); SERVER_ARG * pThArg = (SERVER_ARG *)pArg; enum SERVER_THREAD_STATE threadState;
GetStartupInfo (&startInfoCh);
connectSock = pThArg->sock; /* Create a temp file name */ _stprintf (tempFile, _T("ServerTemp%d.tmp"), pThArg->number);
while (!done && !shutFlag) { /* Main Server Command Loop. */ done = ReceiveRequestMessage (&request, connectSock);
request.record[sizeof(request.record)-1] = '\0'; commandLen = strcspn (request.record, "\n\t"); memcpy (sysCommand, request.record, commandLen); sysCommand[commandLen] = '\0'; _tprintf (_T("Command received on server slot %d: %s\n"), pThArg->number, sysCommand);
/* Retest shutFlag; could be set in console control handler. */ done = done || (strcmp (request.record, "$Quit") == 0) || shutFlag; if (done) continue;
/* Open the temporary results file. */ hTmpFile = CreateFile (tempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &tempSA, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* Check for shared library command. For simplicity, shared */ /* library commands take precedence over process commands */ dl_addr = NULL; /* will be set if GetProcAddress succeeds */ if (pThArg->hDll != NULL) { /* Try Server "In process" */ char commandName[256] = ""; int commandNameLength = strcspn (sysCommand, " "); strncpy (commandName, sysCommand, min(commandNameLength, sizeof(commandName))); dl_addr = (int (*)(char *, char *))GetProcAddress (pThArg->hDll, commandName); /* Trust this DLL not to corrupt the server */ if (dl_addr != NULL) { /* Call the DLL */ (*dl_addr)(request.record, tempFile); } }
if (dl_addr == NULL) { /* No inprocess support */ /* Create a process to carry out the command. */ /* Same as in serverNP*/ . . . }
/* Respond a line at a time. It is convenient to use C library line-oriented routines at this point. */
/* Send temp file, one line at a time, to the client. */ /* Same as in serverNP */ . . . } /* End of main command loop. Get next command */
/* done || shutFlag */ /* End of command processing loop. Free resources; exit thread. */ _tprintf (_T("Shuting down server thread # %d\n"), pThArg->number); closesocket (connectSock);
EnterCriticalSection(&(pThArg->threadCs)); __try { threadState = pThArg->thState = SERVER_THREAD_STOPPED; } __finally { LeaveCriticalSection(&(pThArg->threadCs)); }
return threadState; }
|
The server state logic involves both the boss and server threads.
The Server Thread
Program 2
shows the socket server thread function. There are many similarities to
the named pipe server function, and some code is elided for simplicity.
Also, the code uses some of the global declarations and definitions
from Program 1.
Running the Socket Server
Run 2 shows the server in operation, with several printed information messages that are not in the listings for Programs 1 and 2. The server has several clients, one of which is the client shown in Run 1 (slot 0).
The termination at the end occurs in the accept thread; the shutdown closes the socket, causing the accept call to fail. An exercise suggests ways to make this shutdown cleaner.
A Security Note
This client/server system, as presented, is not
secure. If you are running the server on your computer and someone else
knows the port and your computer name, your computer is at risk. The
other user, running the client, can run commands on your computer that
could, for example, delete or modify files.
|