Here's a hands-on guide to answering the design questions posed by Windows sockets on NT
Chuck Chan, Margaret K. Johnson, Keith Moore, and David Treadwell
To reduce the resource investment required for client software or to ensure easy porting to a range of platforms, many developers of client/server applications choose to use a sockets interface as the IPC (interprocess communications) mechanism between a client and a service. Most Unix utilities, including ftp and Telnet, are written to a sockets interface. On NetWare, this is true of many NLM (NetWare loadable module) environments, such as SNA (Systems Network Architecture) gateways and databases.
WinSock (Windows Sockets) is becoming a common programming-interface choice for independent software vendors that are porting existing se
rvices to Windows NT or that must work with multiple-client platforms that do not share a common higher-level protocol. (For details about WinSock alternatives and NT's networking components, see the text box ``Windows NT Network Architecture'' on page 94.)
Unlike other networking APIs, a sockets interface does not introduce a protocol on the wire. This means that an application written for one vendor's sockets interface is on-the-wire-compatible with an application written for another vendor's interface. For example, applications written for Novell's communications interface--ECB/ESR (Event Control Block/Event Service Routine)--can communicate with a WinSock application that is listening on an IPX/SPX socket. Similarly, Unix applications written for the Berkeley sockets interface or TLI (Transport-Level Interface) can communicate with a WinSock application listening on a TCP or UDP (User Datagram Protocol) socket.
Those who write services for Windows NT have additional advantages offered by the
Service Control Manager. These benefits include automatic service start-up (independent from user log-ins); automatic start-up of any dependent services; service program installation and control (e.g., service start, stop, pause, and query state) via Win32 APIs; access to services on other machines on the network; the ability to revert to the ``last known good'' configuration; and the ability to manage access-control lists for security.
But before you rush off and write your own WinSock service, you should understand two important areas: address resolution and the threading model. Address-resolution issues that must be resolved include how to register the service in a namespace (e.g., NetWare's Bindery), how the client will find the service, and what protocols you should use. Issues that are involved in determining the appropriate threading model include how to use file handles, choosing the best thread structure, and how to implement asynchronous support. In this discussion we will address these matt
ers, providing guidelines for those who want to write a WinSock service for Windows NT.
Address Resolution
Service-address registration and resolution and the selection of an appropriate threading model constitute two of the most important considerations you encounter when writing a WinSock service. We will look at addressing first.
When a service loads, it registers its address, and perhaps additional information, with one or more namespaces (see the figure ``Service-Address Registration and Resolution''). The service then listens for incoming requests. When a client needs a service, it looks up the service's address in one or more namespaces. The client binds to the address and then starts sending data to, and receiving data from, the service.
Each namespace handles registration and resolution of a service's address differently. What's more, different namespaces have different characteristics. In Unix environments, for example, services commonly rely on a preconfigured DNS (Domain Na
ming System), as well as the /etc/services file, to determine the address. For service lookup, the client commonly uses the gethostbyname() function to resolve the host name into an IP address.
In NetWare environments that use SAP (Service Advertisement Protocol) to resolve service addresses, services send out periodic SAP requests that register a word object type with a 48-character service name. Entries registered by the service are dynamic, and clients can access them by sending a SAP request for services of a given object type or by scanning the object database (i.e., the Bindery).
In NetWare environments that use the NDS (NetWare Directory Service), a service must first add itself to the directory schema, defining the properties of the service object, and it must then add the object within a container of an NDS tree. The service object is prominent within the NDS tree and thus does not have to broadcast its existence.
Ideally, the choice of where a service registers itself should be
left to a network's administrator. Services should not have to determine which namespace to register in or what the differences between namespaces are. Likewise, to find a property of a service, such as its service address, clients shouldn't need to know which namespace they require.
Dealing with multiple protocols can be tricky because protocol families use different addressing structures. For example, TCP uses IP addresses, and SPX uses IPX addresses. Both addressing schemes can get you to the same machine running a particular service; however, you must treat a 4-byte IP address differently from a 12-byte IPX address.
New APIs, called RnR (Registration and Resolution) APIs, have been added to the Windows NT 3.5 SDK (Software Development Kit) to address multiple-protocol issues. (You can obtain a copy of the RnR specification via ftp on rhino.microsoft.com under winsock\winsock2\nameres.) Microsoft is working with the WinSock 2.0 group to evolve the RnR APIs into the WinSock 2.0 name-resolution
standard. The new release of the SDK for Windows NT 3.5 includes the necessary header files, library files, and documentation.
Among the new RnR APIs are the following:
-- EnumProtocols, which enumerates the protocols that are available on the computer. It also returns information about the quality of service of the available protocols, such as whether each is connectionless, whether delivery is guaranteed, and so on.
-- GetAddressByName, which, given a service-type GUID (globally unique identifier) and name, resolves the network address. Multiple addresses might be returned.
-- GetNameByType, which, given a service-type GUID, returns a human-readable name for the service type.
-- GetTypeByName, which is the reverse of GetNameByType.
-- SetService, which sets the properties of a service. The dwOperation parameter lets a caller specify whether to add or remove a new service type or to register or delete a service instance.
-- GetService, which obtains informa
tion about a particular service.
Note that service enumeration for client-side browsing will be added to a later version of Windows NT.
Threading Issues
Another key consideration when writing a WinSock service is the best way to handle network I/O. The architecture of a WinSock service heavily influences the service's performance, resource resolution, and effectiveness when it is used with a large number of clients. The text box ``Threading Models'' on page 91 explains five of the most common threading models for small and large networks.
Which one should you choose? No single model is best for every service. Selecting the right one for an individual service depends on the constraints and goals of that service. Does it need to handle hundreds of clients? Is performance critical? Is it acceptable for the service to be complex? The service developer must evaluate the trade-offs between performance and complexity when choosing which model to follow.
The Details
Now that we've
highlighted key design considerations, we'll roll up our sleeves and look at some code. The sample code supplied in this article is an implementation of a simple multiprotocol Windows NT echo service called EchoExample. The server accepts incoming socket connections from clients running the RnrClnt application, and the clients send uninterpreted data over the socket. The server receives the data and then writes it back to the client unaltered. When the transfer is complete, the client closes the socket and exits. The server then closes its socket and returns to its pretransfer state.
First we'll describe how EchoExample handles name registration and resolution using RnR APIs. For brevity, we show only the relevant parts of the code. (A complete listing can be obtained from ftp on rhino.microsoft.com under winsock\winsock2\nameres\samples.)
Remember that name registration and resolution have three basic parts. First, a service may need to perform up-front configuration. Then, the server side must
register itself so that its clients can find it. Finally, the clients need to resolve the network address of the server from a human-readable name.
GUID Creation
Because EchoExample is not a well-known service (as ftp is on Unix or file and print are on NetWare), the first step is assigning it a unique identifier. The service-installation code resides within the RnRSetup.c file. RnR uses a GUID, a 128-bit value that lets you uniquely identify a service without relying on service-type registration with a central authority.
Creating a GUID involves running the UUIDGEN.EXE utility, which ships with the SDK. The service then uses this GUID, which never changes. UUIDGEN generated the GUID shown in the listing ``GUID Creation'' for EchoExample.
Before the service registers itself within a namespace, it adds itself to the list of services known to RnR. Because EchoExample is not a well-known service, its GUID and other parameters, such as its object type for NetWare networks, are likewise no
t well known.
You can use the new SetService() API both during service installation and while loading a service. During service installation, you can call SetService() with SERVICE_ADD_TYPE to add a new service type to the system. Typically, the installation program does this, although the service's developer should make provisions for rerunning this code if namespace providers are added to the system after the service is installed.
Calling SetService() with SERVICE_ADD_ TYPE adds a service type for use only with RnR APIs. You must call additional Win32 APIs to add EchoExample as a Windows NT service. When a service is removed from the system, it should do the reverse, calling SetService() with SERVICE_DELETE_TYPE to clean up.
In the code shown in the listing ``Service Types,'' we pass a SERVICE_INFO structure with all the necessary information to SetService(), using SERVICE_ADD_TYPE as the operation and NS_DEFAULT (i.e., all default namespaces) as the namespace.
Service Registratio
n
When the service starts, it registers itself by calling SetService() with the SERVICE_REGISTER operation. The service should do this after it has completed its initialization and is ready to begin serving clients. When registering, the service must supply several pieces of information, the most important being the GUID that identifies the type, the name identifying the service instance, and the service address.
Because a service can work over multiple transports, its address is described by the SERVICE_ADDRESSES structure. For each socket, we typically determine the local address by calling getsockname() and then add this address to the SERVICE_ADDRESSES structure. This is done in a transport-independent manner. SetService() is routed to each namespace provider, which uses an address's family type to decide whether it should advertise that address. When the service stops, it is important that it deregister itself by calling SetService() with SERVICE_DEREGISTER to remove itself from the various na
mespaces (see the listing ``Service Registration'').
Services that use NS_DEFAULT let the administrator determine the namespaces that the service is registered with. In addition, the service doesn't need to be changed when a new namespace provider is added or deleted.
Name Resolution
The EchoExample client code resides within the RnRClnt.c file. The client must do several things before it starts talking to an EchoExample server. First, it calls EnumProtocols() to determine which protocols available on the system meet the type of service required. When the client finds the desired protocols, GetAddressByName() is used to resolve the network addresses for those protocols (see the listing ``Name Resolution'').
The client and service components that we've highlighted in the preceding section, the RnR specification, and the sample code should all provide you with a clear impression of how to address name registration and resolution, as well as protocol transparency, within a heterogeneous n
etworked environment.
The Threading Model
As mentioned earlier, when writing a service you must choose a thread model that strikes a balance among several conflicting system requirements. Here we will focus on a variation of the one-thread-per-client model (see the text box ``Threading Models'') as implemented in RnrSvc, the RnR sample service for EchoExample. In the interest of brevity, this example puts an upper limit on the number of worker threads.
Like any other multithreaded application, a Windows NT service must carefully synchronize access to shared resources. NT provides a rich set of synchronization primitives. RnrSvc uses a critical-section object to protect resources shared by multiple threads. Critical sections provide mutual-exclusion synchronization among the threads of a single process.
Only a single thread is permitted to enter a critical section at a time. If a second thread attempts to enter a critical section, it is blocked until the first thread leaves. If multiple
threads are blocked on one critical-section object, only one thread is released after the owning thread leaves the critical section. These qualities make critical sections ideal for shared-resource synchronization in a multithreaded service.
Any server that handles more than one client simultaneously has to maintain a certain amount of state information for each connected client. This information is critical during service shutdown, when all connected clients are forcibly disconnected from the server. RnrSvc keeps an open socket handle as part of the state information that is maintained for each connected client. During service shutdown, these open socket handles are closed, which terminates the client sessions.
RnrSvc includes the following modules:
-- CLIENT.C: Manages client connections and state information. There are four versions; each one implements a different threading model.
-- CONNECT.C: The connection thread responsible for accepting incoming connection requests. As
connection requests are accepted, they are routed to CLIENT.C for processing.
-- GLOBALS.C: Global variables shared by all modules.
-- LOG.C: Event-logging functions. They can be built with BUILD_STANDALONE_EXE # defined to effectively disable logging.
-- MAIN.C: Start-up code. This module contains all functions for communicating with the NT Service Controller. It can be built with BUILD_STANDALONE_EXE # defined to create a stand-alone executable file rather than a service. This makes debugging a bit simpler.
-- RNRUTIL.C: General RnR utility functions useful for writing other services and applications.
-- CLIENT.C: Encapsulates all client-manipulation functions. As new clients connect to the server, they are tracked by parallel arrays of socket handles and thread handles. Whenever new entries are added to the arrays, a corresponding worker thread is created. When a client disconnects from the server, the worker thread removes the corresponding entries from the tracking arra
ys. When the service is shut down, the active list is scanned and all active clients are terminated.
Four global variables track connected clients: CRITICAL_SECTION (RnrpLock), SOCKET (RnrpActiveSockets[MAX_THREADS]), HANDLE (RnrpActiveThreads[MAX_THREADS], and DWORD (RnrpNumActive). RnrpLock synchronizes access to the other global variables, RnrpActiveSockets holds the socket handles of all active clients, RnrpActiveThreads holds the thread handles of all active clients, and RnrpNumActive stores the current number of active clients.
Service Initialization and Termination
The service-initialization thread, RnrClientInitialize, initializes the critical section lock and active client counter.
InitializeCriticalSection
( &RnrpLock );
RnrpNumActive = 0;
Service termination (RnrClientTerminate) gets a bit complex. Its most important tasks are ensuring that worker threads shut down quickly and ensuring they do so in an orderly fashion. The service scans RnrpActiveS
ockets, and all sockets belonging to active clients are closed. As the sockets are closed, the array entries are set to INVALID_SOCKET to prevent the worker threads from closing their own thread handles (see the listing ``Service Termination''). This enables the termination thread to wait for any remaining worker threads to terminate by calling the WaitForMultipleObjects API on the RnrpActiveThreads array.
Whenever a new client connects to the service, RnrClientHandler is called to manage the client session. RnrClientHandler checks RnrpNumActive to see if the tracking arrays have room for another client. If they do, then the newly accepted client socket is saved in RnrpActive Sockets, and a new worker thread is created by calling the CreateThread API.
The worker thread, RnrpWorkerThread, calls a utility function, RnrpHandleTransfer, to echo the data back to the client. (RnrpHandleTransfer is shared by the single-thread, single-client-at-a-time; the one-thread-per-client; and the worker-threads-w
ith-synchronous-I/O models.) After the transfer is complete, RnrpWorkerThread scans RnrpActiveSockets to find the client socket.
If the socket is found, the thread handle is closed. The socket-array entry and thread-array entries are removed from the arrays, and RnrpNumActive is decremented. If the socket is not found, it means that the service is shutting down, and the arrays are left untouched.
RnrpHandleTransfer performs the actual transfer. It sits in a loop, reading data from the client and then writing it back unaltered. If the receiving or sending portion of the transfer fails for any reason, the transfer is aborted. When a transfer is completed, the client socket is closed.
NT offers portability, scalability, and built-in networking support for popular transports. This brief tour, together with the sample code and additional on-line references, should give you a jump start for writing your own service for NT.
GUID Creation
//
// GUID for EchoExample cr
eated with UUIDGEN:
// ``47da8500-96a1-11cd-901d-204c4f4f5020''
//
GUID ServiceGuid = { 0x47da8500, 0x96a1, 0x11cd, 0x90, 0x1d,
0x20, 0x4c, 0x4f, 0x4f, 0x50, 0x20 };
Service Registration
err = getsockname(
SocketHandle, // opened socket
sockAddr, // receives socket address
&size) ; // size of address
... package sockAddr in the serviceInfo structure
err = SetService(
NS_DEFAULT, // for all default namespaces
SERVICE_REGISTER, // we want to register (advertise)
0, // no flags specified
&serviceInfo, // SERVICE_INFO structure
NULL, // no asynchronous support yet
&statusFlags) ; // returns status flags
Name Resolution
protocolCount = EnumProtocols( NULL, buffer, &bytesRequired );
... select the desired protocols
err = Get
AddressByName(
NS_DEFAULT,
ServiceType,
ServiceName,
protocols,
0, // dwResolution
NULL, // lpServiceAsyncInfo
buffer,
&bytesRequired,
NULL, // lpAliasBuffer
NULL // lpdwAliasBufferLength
);
Service Termination
... call worker routine to perform the echo transfer ...
//
// Find the client socket in the active socket array. If found,
// close the corresponding thread handle and remove the socket
// and thread-array entries.
//
EnterCriticalSection( &RnrpLock );
for( Index = 0 ; Index
<
RnrpNumActive ; Index++ ) {
if( RnrpActiveSockets[Index] == ClientSocket ) {
CloseHandle( RnrpActiveThreads[Index] );
//
//Remove the socket and thread-array entries.
//
RnrpNumActive--;
memmove( &RnrpActiveSockets[Index],
&RnrpActiveSockets[Index+1],
( RnrpNumActive - Index ) * sizeof(SOCKET) );
memmove( &RnrpActiveThreads[Index],
&RnrpActiveThreads[Index+1],
( RnrpNumActive - Index ) * sizeof(HANDLE) );
break;
}
}
LeaveCriticalSection( &RnrpLock );
Figure: Service-Address Registration and Resolution
Services can register their addresses with different types of namespaces: Unix commonly relies on DNS, while NetWare environments use SAP. Clients call namespaces to locate and bind to services.
Illustration: Service Types
err = SetService(
NS_DEFAULT, // all default namespaces
SERVICE_ADD_TYPE, // add the new service type
0, // dwFlags not used
&serviceInfo, // the service info structure
NUL
L, // lpServiceAsyncInfo
&statusFlags // additional status information
);
The authors are members of Microsoft's Windows NT development team. Chuck Chan is the lead software-design engineer responsible for NetWare connectivity in NT networking. Margaret K. Johnson is a program manager for NT networking components. Keith Moore is a software-design engineer responsible for designing and implementing the NT ftp server. Lead software-design engineer David Treadwell has primary responsibility for Windows Sockets and is one of the authors of the WinSock specification.