Chapter 12. Network Programming with Windows Sockets

Named pipes and mailslots are suitable for interprocess communication between processes on the same computer or processes on Windows computers connected by a local or wide area network. The client/server application system developed in Chapter 11, starting with Program 11-2, demonstrated these capabilities.

Named pipes and mailslots (both simply referred to here as “named pipes” unless the distinction is important) have the distinct drawback, however, of not being an industry standard. Therefore, programs such as those in Chapter 11 will not port easily to non-Windows machines, nor will they interoperate with non-Windows machines. This is the case even though named pipes are protocol-independent and can run over industry-standard protocols such as TCP/IP.

Windows provides interoperability by supporting Windows Sockets, which are nearly the same as, and interoperable with, Berkeley Sockets, a de facto industry standard. This chapter shows how to use the Windows Sockets (or “Winsock”) API by modifying Chapter 11’s client/server system. The resulting system can operate over TCP/IP-based wide area networks, and the server, for instance, can accept requests from UNIX, Linux, and other non-Windows clients.

Readers who are familiar with Berkeley Sockets may want to proceed directly to the programming examples, which not only use sockets but also show new server features and additional thread-safe library techniques.

Winsock, by enabling standards-based interoperability, allows programmers to exploit higher-level protocols and applications, such as ftp, http, RPCs, and COM, all of which provide different, and higher-level, models for standard, interoperable, networked interprocess communication.

In this chapter, the client/server system is a vehicle for demonstrating Winsock, and, in the course of modifying the server, interesting new features are added. In particular, DLL entry points (Chapter 5) and in-process DLL servers are used for the first time. (These new features could have been incorporated in the initial Chapter 11 version, but doing so would have distracted from the development and understanding of the basic system architecture.) Finally, additional examples show how to create reentrant thread-safe libraries.

Winsock, because of conformance to industry standards, has naming conventions and programming characteristics somewhat different from the Windows functions described so far. The Winsock API is not strictly a part of the Windows API. Winsock also provides additional functions that are not part of the standard; these functions are used only as absolutely required. Among other advantages, programs will be more portable to other operating systems.

Windows Sockets

The Winsock API was developed as an extension of the Berkeley Sockets API into the Windows environment, and all Windows versions support Winsock. Winsock’s benefits include the following.

• Porting code already written for Berkeley Sockets is straightforward.

• Windows machines easily integrate into TCP/IP networks, both IPv4 and IPv6. IPv6, among other features, allows for longer IP addresses, overcoming the 4-byte address limit of IPv4.

• Sockets can be used with Windows overlapped I/O (Chapter 14), which, among other things, allows servers to scale when there is a large number of active clients.

• Sockets can be treated as file HANDLEs for use with ReadFile, WriteFile, and, with some limitations, other Windows functions, just as UNIX allows sockets to be used as file descriptors. This capability is convenient whenever there is a need to use asynchronous I/O and I/O completion ports (Chapter 14).

• Windows provides nonportable extensions.

• Sockets can support protocols other than TCP/IP, but this chapter assumes TCP/IP. See MSDN if you use some other protocol, particularly Asynchronous Transfer Mode (ATM).

Winsock Initialization

The Winsock API is supported by a DLL (WS2_32.DLL) that can be accessed by linking WS2_32.LIB with your program (these names do not change on 64-bit machines). The DLL needs to be initialized with a nonstandard, Winsock-specific function, WSAStartup, which must be the first Winsock function a program calls. WSACleanup should be called when the program no longer needs to use Winsock functionality. Note: The prefix WSA denotes “Windows Sockets asynchronous. . . .” The asynchronous capabilities will not be used here because we’ll use threads for asynchronous operation.

WSAStartup and WSACleanup, while always required, may be the only nonstandard functions you will use. A common practice is to use #ifdef statements to test the WIN32 macro (normally defined from Visual Studio) so that the WSA functions are called only if you are building on Windows. This approach assumes, of course, that the rest of your code is platform-independent.

Parameters

wVersionRequired indicates the highest version of the Winsock DLL that you need and can use. Nonetheless, Version 2.x is available on all current Windows versions, and the examples use 2.0.

The return value is nonzero if the DLL cannot support the version you want.

The low byte of wVersionRequired specifies the major version, and the high byte specifies the minor version, which is the opposite of what you might expect. The MAKEWORD macro is usually used; thus, MAKEWORD (2, 0) represents Version 2.0.

lpWSAData points to a WSADATA structure that returns information on the configuration of the DLL, including the highest version available. The Visual Studio on-line help shows how to interpret the results.

WSAGetLastError can be used to get the error; GetLastError also works but is not entirely reliable. The Examples file socket programs use WSAReportError, a ReportError variation that uses WSAGetLastError.

When a program has completed or no longer needs to use sockets, it should call WSACleanup so that WS2_32.DLL, the sockets DLL, can free resources allocated for this process.

Creating a Socket

Once the Winsock DLL is initialized, you can use the standard (i.e., Berkeley Sockets) functions to create sockets and connect for client/server or peer-to-peer communication.

A Winsock SOCKET data type is analogous to the Windows HANDLE and can even be used with ReadFile and other Windows functions requiring a HANDLE. Call the socket function in order to create (or open) a SOCKET and return its value.

Parameters

The type SOCKET is actually defined as an int, so UNIX code will port without the necessity of using the Windows type definitions.

af denotes the address family, or protocol; use PF_INET (or AF_INET, which has the same value but is more properly used with the bind call) to designate IP (the Internet protocol component of TCP/IP).

type specifies connection-oriented (SOCK_STREAM) or datagram communications (SOCK_DGRAM), slightly analogous to named pipes and mailslots, respectively.

protocol is unnecessary when af is AF_INET; use 0.

socket returns INVALID_SOCKET on failure.

You can use Winsock with protocols other than TCP/IP by specifying different protocol values; we will use only TCP/IP.

socket, like all the other standard functions, does not use uppercase letters in the function name. This is a departure from the Windows convention and is due to the need to conform to industry standards.

Socket Server Functions

In this discussion, a server is a process that accepts connections on a specified port. While sockets, like named pipes, can be used for peer-to-peer communication, this distinction is convenient and reflects the manner in which two machines connect to one another.

Unless specifically mentioned, the socket type will always be SOCK_STREAM in the examples. SOCK_DGRAM is described later in this chapter.

Binding a Socket

A server should first create a socket (using socket) and then “bind” the socket to its address and endpoint (the communication path from the application to a service). The socket call, followed by the bind, is analogous to creating a named pipe. There is, however, no name to distinguish sockets on a given machine. A port number is used instead as the service endpoint. A given server can have multiple endpoints. The bind function is shown here.

Parameters

s is an unbound SOCKET returned by socket.

saddr, filled in before the call, specifies the protocol and protocol-specific information, as described next. The port number is part of this structure.

namelen is sizeof(sockaddr).

The return value is normally 0 or SOCKET_ERROR in case of error. The sockaddr structure is defined as follows.

The first member, sa_family, is the protocol. The second member, sa_data, is protocol-specific. The Internet version of sockaddr is sockaddr_in, and we use sockaddr_in in the examples.

Note the use of a short integer for the port number. The port number and other information must also be in the proper byte order, big-endian, so as to allow interoperability. The sin_addr member has a submember, s_addr, which is filled in with the familiar 4-byte IP address, such as 127.0.0.1, to indicate the machine from which connections will be accepted. Normally, applications accept connections from any machine, so the value INADDR_ANY is common, although this symbolic value must be converted to the correct form, as in the next code fragment.

Use the inet_addr function to convert a known IP address text string (use char characters, not Unicode) into the required form so that you can initialize the sin_addr.s_addr member of a sockaddr_in variable, as follows:

sa.sin_addr.s_addr = inet_addr ("192.13.12.1");

A bound socket, with a protocol, port number, and IP address, is sometimes said to be a named socket.

Putting a Bound Socket into the Listening State

listen makes a server socket available for client connection. There is no analogous named pipe function.

nQueueSize indicates the number of connection requests you are willing to have queued at the socket. There is no upper bound in Winsock Version 2.0.

Accepting a Client Connection

Finally, a server can wait for a client to connect, using the accept function, which returns a new connected socket for use in the I/O operations. Notice that the original socket, now in the listening state, is used solely as an accept parameter and is not used directly for I/O.

accept blocks until a client connection request arrives, and then it returns the new I/O socket. It is possible to make a socket be nonblocking (see MSDN), but the server (Program 12-2) uses a separate accepting thread so that the server does not block. Call accept after the socket is placed in the listening state with calls to bind and listen.

Parameters

s, the first argument, is the listening socket.

lpAddr points to a sockaddr_in structure that gives the address of the client machine.

lpAddrLen points to a variable that will receive the length of the returned sockaddr_in structure. Initialize this variable to sizeof(struct sockaddr_in) before the accept call.

Disconnecting and Closing Sockets

Disconnect a socket using

shutdown (s, how)

where s is the value returned by accept. The how value indicates if you want to disconnect send operations (SD_SEND), receive operations (SD_RECEIVE), or both (SD_BOTH). The effects of shutting down sending and/or receiving are:

SD_SEND or SD_BOTH—Subsequent send calls will fail and the sender will send a FIN (no more data from the sender). You cannot re-enable sending on the socket.

SD_RECEIVE or SD_BOTH—Subsequent recv calls will fail and the sender will send a FIN (no more data from the sender). Any queued data or data that arrives later is lost. There is no way to re-enable receiving on the socket.

shutdown does not close the socket or free its resources, but it does assure that all data is sent or received before the socket is closed. Nonetheless, an application should not reuse a socket after calling shutdown.

If you shut down a socket for sending only, you can still receive, so if there is possibility of more data, call recv until it returns 0 bytes. Once there is no more data, shut down receiving. If there is a socket error, then a clean disconnect is impossible.

Likewise, if you shut down a socket for receiving only, you can still send remaining data. For example, a server might stop receiving requests while it still has response or other data to transmit before shutting down sending.

Once you are finished with a socket, you can close it with

closesocket (SOCKET s)

The server first closes the socket created by accept, not the listening socket. The server should not close the listening socket until the server shuts down or will no longer accept client connections. Even if you are treating a socket as a HANDLE and using ReadFile and WriteFile, CloseHandle alone will not destroy the socket; use closesocket.

Example: Preparing for and Accepting a Client Connection

The following code fragment shows how to create a socket and then accept client connections. This example uses two standard functions, htons (“host to network short”) and htonl (“host to network long”), that convert integers to big-endian form, as IP requires.1

1 Windows supports little-endian, and htons and htonl perform the required conversion. The functions are implemented on non-Windows machines to behave as required by the machine architecture.

The server port can be any unassigned short integer. Well-known ports (0–1023) and registered ports (1024–49151, with some exceptions) should not be used for your server. Select a port from the an unassigned range such as 48557–48618, 48620–49150, or 49152 and above. However, check www.iana.org/assignments/port-numbers to be certain that your port number is currently unassigned. You may also find that the port you select is in use by some other process on your computer, so you’ll need to make another selection. The examples use SERVER_PORT, defined in ClientServer.h as 50000.

image

Socket Client Functions

A client station wishing to connect to a server must also create a socket with the socket function. The next step is to connect with a server, specifying a port, host address, and other information. There is just one additional function, connect.

Connecting to a Server

If there is a server with a listening socket, the client can connect with the connect function.

Parameters

s is a socket created with the socket function.

lpName points to a sockaddr_in structure that has been initialized with the port number and IP address of a machine with a socket, bound to the specified port, that is in listening mode.

Initialize nNameLen with sizeof (struct sockaddr_in).

A return value of 0 indicates a successful connection, whereas SOCKET_ERROR indicates failure, possibly because there is no listening socket at the specified address.

Example: Client Connecting to a Server

The following code sequence allows a client to connect to a server. Just two function calls are required, but be certain to initialize the address structure before the connect call. Error testing is omitted here but should be included in actual programs. In the example, assume that the IP address (a text string such as 192.76.33.4) is given in argv [1] on the command line.

image

Sending and Receiving Data

Socket programs exchange data using send and recv, which have nearly identical argument forms (the send buffer has the const modifier). Only send is shown here.

The return value is the actual number of bytes transmitted. An error is indicated by the return value SOCKET_ERROR.

nFlags can be used to indicate urgency (such as out-of-band data), and the MSG_PEEK flag can be used to look at incoming data without reading it.

The most important fact to remember is that send and recv are not atomic, and there is no assurance that all the requested data has been received or sent. “Short sends” are extremely rare but possible, as are “short receives.” There is no concept of a message as with named pipes; therefore, you need to test the return value and resend or transmit until all data has been transmitted.

You can also use ReadFile and WriteFile with sockets by casting the socket to a HANDLE in the function call.

Comparing Named Pipes and Sockets

Named pipes, described in Chapter 11, are very similar to sockets, but there are significant usage differences.

• Named pipes can be message-oriented, which can simplify programs.

• Named pipes require ReadFile and WriteFile, whereas sockets can also use send and recv.

• Sockets, unlike named pipes, are flexible so that a user can select the protocol to use with a socket, such as TCP or UDP. The user can also select protocols based on quality of service and other factors.

• Sockets are based on an industry standard, allowing interoperability with non-Windows machines.

There are also differences in the server and client programming models.

Comparing Named Pipe and Socket Servers

When using sockets, call accept repetitively to connect to multiple clients. Each call will return a different connected socket. Note the following differences relative to named pipes.

• Named pipes require you to create each named pipe instance with CreateNamedPipe. accept creates the socket instances.

• There is no upper bound on the number of socket clients (listen limits only the number of queued clients), but there can be a limit on the number of named pipe instances, depending on the first call to CreateNamedPipe.

• There are no socket convenience functions comparable to TransactNamedPipe.

• Named pipes do not have explicit port numbers and are distinguished by name.

A named pipe server requires two function calls (CreateNamedPipe and ConnectNamedPipe) to obtain a usable HANDLE, whereas socket servers require four function calls (socket, bind, listen, and accept).

Comparing Named Pipes and Socket Clients

Named pipes use WaitNamedPipe followed by CreateFile. The socket sequence is in the opposite order because the socket function can be regarded as the creation function, while connect is the blocking function.

An additional distinction is that connect is a socket client function, while the similarly named ConnectNamedPipe is a server function.

Example: A Socket Message Receive Function

It is frequently convenient to send and receive messages as a single unit. Named pipes can do this, as shown in Chapter 11. Sockets, however, require that you provide a mechanism to specify and determine message boundaries. One common method is to create a message header with a length field, followed by the message itself, and we’ll use message headers in the following examples. Later examples use a different technique, end-of-string null characters, to mark message boundaries. Fixed-length messages provide yet another solution.

The following function, ReceiveMessage, receives message length headers and message bodies. The SendMessage function is similar.

Notice that the message is received in two parts: the header and the contents. The user-defined MESSAGE type with a 4-byte message length header is:

image

Even the 4-byte header requires repetitive recv calls to ensure that it is read in its entirety because recv is not atomic.

Note: The message length variables are fixed-precision LONG32 type to remind readers that the length, which is included in messages that may be transferred to and from programs written in other languages (such as Java) or running on other machines, where long integers may be 64 bits, will have a well-defined, unambiguous length.

image

Example: A Socket-Based Client

Program 12-1 reimplements the client program, which in named pipe form is Program 11-2, clientNP. The conversion is straightforward, with several small differences.

• Rather than locating a server using mailslots, the user enters the IP address on the command line. If the IP address is not specified, the default address is 127.0.0.1, which indicates the current machine.

• Functions for sending and receiving messages, such as ReceiveMessage, are used but are not shown here.

• The port number, SERVER_PORT, is defined in the header file, ClientServer.h.

Program 12-1 clientSK: Socket-Based Client

image

image

While the code is written for Windows, there are no Windows dependencies other than the WSA calls.

Comment: The programs in this chapter do not use generic characters. This is a simplification driven by the fact that inet_addr does not accept Unicode strings.

Running the Socket Client

The socket server is complex with long program listings. Therefore, Run 12-1 shows the client in operation, assuming that there is a running server. The commands are familiar, and operation is very similar to Chapter 11’s named pipe client.

Run 12-1 clientSK: Socket Client Operation

image

Example: A Socket-Based Server with New Features

serverSK, Program 12-2, is similar to serverNP, Program 11-3, but there are several changes and improvements.

• 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. A sample DLL is shown in Program 12-4. The DLL needs to be trusted, however, because any unhandled exception could crash or corrupt the server, as would changes to the environment.

Program 12-2 serverSK: Socket-Based Server with In-Process Servers

image

image

image

image

image

image

image

image

image

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 12-2 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 12-3 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 12-3 serverSK: Server Thread Code

image

image

image

The server state logic involves both the boss and server threads. Exercise 12–6 suggests an alternative approach where the boss is not involved.

The Server Thread

Program 12-3 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 12-2.

Running the Socket Server

Run 12-3 shows the server in operation, with several printed information messages that are not in the listings for Programs 12-2 and 12-3. The server has several clients, one of which is the client shown in Run 12-1 (slot 0).

Run 12-3 serverSK: Requests from Several Clients

image

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.

A complete discussion of security solutions is well beyond this book’s scope. Nonetheless, Chapter 15 shows how to secure Windows objects, and Exercise 12–15 suggests using Secure Sockets Layer (SSL).

In-Process Servers

As mentioned previously, in-process servers are a major enhancement in serverSK. Program 12-4 shows how to write a DLL to provide these services. Two familiar functions are shown, a word counting function and a toupper function.

Program 12-4 commandIP: Sample In-Process Servers

image

image

image

By convention, the first parameter is the command line, while the second is the name of the output file. Beyond that, always remember that the function will execute in the same thread as the server thread, so there are strict requirements for thread safety, including but not limited to the following:

• The functions should not change the process environment in any way. For example, if one of the functions changes the working directory, that change will affect the entire process.

• Similarly, the functions should not redirect standard input or output.

• Programming errors, such as allowing a subscript or pointer to go out of bounds or the stack to overflow, could corrupt another thread or the server process itself. More generally, the function should not generate any unhandled exception because the server will not be able to do anything other than shut down.

• Resource leaks, such as failing to deallocate memory or to close handles, will ultimately affect the server application.

Processes do not have such stringent requirements because a process cannot normally corrupt other processes, and resources are freed when the process terminates. A typical development methodology, then, is to develop and debug a service as a process, and when it is judged to be reliable, convert it to a DLL.

Program 12-4 shows a small DLL library with two simple functions, wcip and toupperip, which have functionality from programs in previous chapters. The code is in C, avoiding C++ decorated names. These examples do not support Unicode as currently written.

Line-Oriented Messages, DLL Entry Points, and TLS

serverSK and clientSK communicate using messages, where each message is composed of a 4-byte length header followed by the message content. A common alternative to this approach is to have the messages delimited by null characters.

The difficulty with delimited messages is that there is no way to know the message length in advance, and each incoming character must be examined. Receiving a single character at a time would be inefficient, however, so incoming characters are stored in a buffer, and the buffer contents might include one or more null characters and parts of one or more messages. Buffer contents and state must be retained between calls to the message receive function. In a single-threaded environment, static storage can be used, but multiple threads cannot share the same static storage.

In more general terms, we have a multithreaded persistent state problem. This problem occurs any time a thread-safe function must maintain information from one call to the next. The Standard C library strtok function, which scans a string for successive instances of a token, is a common alternative example of this problem.

Solving the Multithreaded Persistent State Problem

The first of this chapter’s two solutions to the persistent state problem uses a combination of the following components.

• A DLL for the message send and receive functions.

• An entry point function in the DLL.

• Thread Local Storage (TLS, Chapter 7). The DLL index is created when the process attaches, and it is destroyed when the process detaches. The index number is stored in static storage to be accessed by all the threads.

• A structure containing a buffer and its current state. A structure is allocated every time a thread attaches, and the address is stored in the TLS entry for that thread. A thread’s structure is deallocated when the thread detaches.

• This solution does have a significant limitation; you can only use one socket with this library per thread. The second solution, later in the chapter, overcomes this limitation.

The TLS, then, plays the role of static storage, and each thread has its own unique copy of the static storage.

Example: A Thread-Safe DLL for Socket Messages

Program 12-5 is the DLL containing two character string (“CS” in names in this example) or socket streaming functions: SendCSMessage and ReceiveCSMessage, along with a DllMain entry point (see Chapter 5). These two functions are similar to and essentially replace ReceiveMessage, listed earlier in this chapter, and the functions used in Programs 12-1 and 12-2.

Program 12-5 SendReceiveSKST: Thread-Safe DLL

image

image

image

image

image

The DllMain function is a representative solution of a multithreaded persistent state problem, and it combines TLS and DLLs. The resource deallocation in the DLL_THREAD_DETACH case is especially important in a server environment; without it, the server would eventually exhaust resources, typically resulting in either failure or performance degradation or both. Note: This example illustrates concepts that are not directly related to sockets, but it is included here, rather than in earlier chapters, because this is a convenient place to illustrate thread-safe DLL techniques in a realistic example.

If this DLL is to be loaded dynamically, you must load it before starting any threads that use the DLL; otherwise, there will not be a DLL_THREAD_ATTACH call to DllMain.

The Examples file contains client and server code, slightly modified from Programs 12-1 and 12-2, that uses this DLL.

Comments on the DLL and Thread Safety

DllMain, with DLL_THREAD_ATTACH, is called whenever a new thread is created, but there is not a distinct DLL_THREAD_ATTACH call for the primary thread or any other threads that exist when the DLL is loaded. The DLL’s DLL_PROCESS_ATTACH case must handle these cases.

• In general, and even in this case (consider the accept thread), some threads may not require the allocated memory, but DllMain cannot distinguish the different thread types. Therefore, the DLL_THREAD_ATTACH case does not actually allocate any memory, and there is no need to call TlsSetValue because Windows initializes the value to NULL. The ReceiveCSMessage entry point allocates the memory the first time it is called. In this way, the thread-specific memory is allocated only by threads that require it, and different thread types can allocate exactly the resources they require.

• While this DLL is thread-safe, a given thread can use these routines with only one socket at a time because the persistent state is associated with the thread, not the socket. The next example addresses this issue.

• The DLL source code on the Examples file is instrumented to print the total number of DllMain calls by type.

• There is still a resource leak risk, even with this solution. Some threads, such as the accept thread, may never terminate and therefore will never be detached from the DLL. ExitProcess will call DllMain with DLL_PROCESS_DETACH but not with DLL_THREAD_DETACH for threads that are still active. This does not cause a problem in this case because the accept thread does not allocate any resources, and even memory is freed when the process terminates. There would, however, be an issue if threads allocated resources such as temporary files; the ultimate solution would be to create a globally accessible list of resources. The DLL_PROCESS_DETACH code would then have the task of scanning the list and deallocating the resources; this is left as an exercise.

Example: An Alternative Thread-Safe DLL Strategy

Program 12-5, while typical of the way in which TLS and DllMain are combined to create thread-safe libraries, has two major weaknesses noted in the comments in the previous section. First, the state is associated with the thread rather than with the socket, so a given thread can process only one socket at a time. Second, there is the resource leak risk mentioned in the last bullet above.

An effective alternative approach to thread-safe library functions is to create a handle-like structure that is passed to every function call. The state is then maintained in the structure. The application explicitly manages the state, so you can manage multiple sockets in a thread, and you can even use the sockets with fibers (there might be one socket, or more, per fiber). Many UNIX and Linux applications use this technique to create thread-safe C libraries; the main disadvantage is that the functions require an additional parameter for the state structure.

Program 12-6 modifies Program 12-5. Notice that DllMain is not necessary, but there are two new functions to initialize and free the state structure. The send and receive functions require only minimal changes. An associated server, serverSKHA, is included in the Examples file and requires only slight changes in order to create and close the socket handle (HA denotes “handle”).

Program 12-6 SendReceiveSKHA: Thread-Safe DLL with a State Structure

image

image

image

image

Datagrams

Datagrams are similar to mailslots and are used in similar circumstances. There is no connection between the sender and receiver, and there can be multiple receivers. Delivery to the receiver is not ensured with either mailslots or datagrams, and successive messages will not necessarily be received in the order they were sent.

The first step in using datagrams is to specify SOCK_DGRAM in the type field when creating the socket with the socket function.

Next, use sendto and recvfrom, which take the same arguments as send and recv, but add two arguments to designate the partner station. Thus, the sendto function is as follows.

lpAddr points to an sockaddr address structure where you can specify the name of a specific machine and port, or you can specify that the datagram is to be broadcast to multiple computers; see the next section.

When using recvfrom, you specify the computers (perhaps all) from which you are willing to accept datagrams; also see the next section.

As with mailslots, datagram messages should be short; MSDN recommends 512 as the length limit for the data portion, as that limit avoids having the message sent in fragments.

Datagram Broadcasting

Several steps are necessary to broadcast sendto messages to multiple computers. Here are the basic steps; see MSDN for complete details:

• Set the SOCK_DGRAM socket options by calling setsockopt, specifying the SO_BROADCAST option. Also, set this option for sockets that are to receive broadcast messages.

• Set the client’s lpAddr sin_addr_in.s_addr value to INADDR_BROADCAST.

• Set the port number as in the preceding examples.

• The broadcasts will be sent to and received by all computer interfaces (that is, all computers with a datagram socket with the SO_BROADCAST option) to that port.

Using Datagrams for Remote Procedure Calls

A common datagram usage is to implement RPCs. Essentially, in the most common situation, a client sends a request to a server using a datagram. Because delivery is not ensured, the client will retransmit the request if a response, also using a datagram, is not received from the server after a wait period. The server must be prepared to receive the same request several times.

The important point is that the RPC client and server do not require the overhead of a stream socket connection; instead, they communicate with simple requests and responses. As an option, the RPC implementation ensures reliability through time-outs and retransmissions, simplifying the application program. Alternatively, the client and server are frequently implemented so as to use stateless protocol (they do not maintain any state information about previous messages), so each request is independent of other requests. Again, application design and implementation logic are greatly simplified.

Berkeley Sockets versus Windows Sockets

Programs that use standard Berkeley Sockets calls will port to Windows Sockets, with the following important exceptions.

• You must call WSAStartup to initialize the Winsock DLL.

• You must use closesocket (which is not portable), rather than close, to close a socket.

• You must call WSACleanup to shut down the DLL.

Optionally, you can use the Windows data types such as SOCKET and LONG in place of int, as was done in this chapter. Programs 12-1 and 12-2 were ported from UNIX, and the effort was minimal. It was necessary, however, to modify the DLL and process management sections. Exercise 12–12 suggests that you port these two programs back to UNIX.

Overlapped I/O with Windows Sockets

Chapter 14 describes asynchronous I/O, which allows a thread to continue running while an I/O operation is in process. Sockets with Windows asynchronous I/O are discussed in that chapter.

Most asynchronous programming can be achieved uniformly and easily using threads. For example, serverSK uses an accept thread rather than a nonblocking socket. Nonetheless, I/O completion ports, which are associated with asynchronous I/O, are important for scalability when there is a large number of clients. This topic is also described in Chapter 14, and Chapter 9 discussed the same situation in the context of NT6 thread pools.

Windows Sockets Additional Features

Windows Sockets 2 adds several areas of functionality, including those listed here.

• Standardized support for overlapped I/O (see Chapter 14). This is considered to be the most important enhancement.

• Scatter/gather I/O (sending and receiving from noncontiguous buffers in memory).

• The ability to request quality of service (speed and reliability of transmission).

• The ability to organize sockets into groups. The quality of service of a socket group can be configured, so it does not have to be done on a socket-by-socket basis. Also, the sockets belonging to a group can be prioritized.

• Piggybacking of data onto connection requests.

• Multipoint connections (comparable to conference calls).

Summary

Windows Sockets allows the use of an industry-standard API, so that your programs can be interoperable and nearly portable in source code form. Winsock is capable of supporting nearly any network protocol, but TCP/IP is the most common.

Winsock is comparable to named pipes (and mailslots) in both functionality and performance, but portability and interoperability are important reasons for considering sockets. Keep in mind that socket I/O is not atomic, so it is necessary to ensure that a complete message is transmitted.

This chapter covered the Winsock essentials, which are enough to build a workable client/server application system. There is, however, much more, including asynchronous usage; see the Additional Reading references for more information.

This chapter also provided examples of using DLLs for in-process servers and for creating thread-safe libraries.

Looking Ahead

Chapters 11 and 12 have shown how to develop servers that respond to client requests. Servers, in various forms, are common Windows applications. Chapter 13 describes Windows Services, which provide a standard way to create and manage servers, in the form of services, permitting automated service start-up, shutdown, and monitoring. Chapter 13 shows how to turn a server into a manageable service.

Additional Reading

Windows Sockets

Network Programming for Microsoft Windows by Jim Ohlund is a good Winsock reference.

Berkeley Sockets and TCP/IP

W. R. Stevens’s TCP/IP Illustrated, Volume 3, covers sockets and much more, while the first two volumes in the series describe the protocols and their implementation. The same author’s UNIX Network Programming provides comprehensive coverage that is valuable even for non-UNIX machines. Another reference is Michael Donahoo and Kenneth Calvert, TCP/IP Sockets in C: Practical Guide for Programmers.

Exercises

12–1. Use WSAStartup to determine the highest and lowest Winsock version numbers supported on the machines accessible to you.

12–2. Use the JobShell program from Chapter 6 to start the server and several clients, where each client is created using the “detached” option. Eventually, shut down the server by sending a console control event through the kill command. Can you suggest any improvements in the serverSK shutdown logic?

12–3. Modify the client and server programs (Programs 12-1 and 12-2) so that they use datagrams to locate a server. The mailslot solution in Chapter 11 could be used as a starting point.

12–4. Modify the named pipe server in Chapter 11 (Program 11-3) so that it creates threads on demand instead of a server thread pool. Rather than predefining a fixed maximum for the number of named pipe instances, allow the application to determine the maximum.

12–5. Perform experiments to determine whether in-process servers are faster than out-of-process servers. For example, you can use the word count example (Program 12-4); there is an executable wc program as well as the DLL function shown in Program 12-4.

12–6. The number of clients that serverSK can support is bounded by the array of server thread arguments. Modify the program so that there is no such bound. You will need to create a data structure that allows you to add and delete thread arguments, and you also need to be able to scan the structure for terminated server threads. An alternative, and arguably simpler, approach would be to have each server thread manage its own state without the boss thread being involved other than to ask the server threads to shut down and wait for them to complete.

12–7. Develop additional in-process servers. For example, convert the grep program (see Chapter 6).

12–8. Enhance the server (Program 12-2) so that you can specify multiple DLLs on the command line.

12–9. Investigate the setsockopt function and the SO_LINGER option. Apply the option to one of the server examples.

12–10. Ensure that serverSK is free of resource leaks. Do the same with serverSKST, which was modified to use the DLL in Program 12-5.

12–11. Extend the exception handler in Program 12-4 so that it reports the exception and exception type at the end of the temporary file used for the server results.

12–12. Extended exercise (requires extra equipment): If you have access to a UNIX machine that is networked to your Windows machine, port clientSK to the UNIX machine and have it access serverSK to run Windows programs. You will, of course, need to convert data types such as DWORD and SOCKET to other types (unsigned int and int, respectively, in these two cases). Also, you will need to ensure that the message length is transmitted in big-endian format. Use functions such as htonl to convert the message lengths. Finally, port serverSK to UNIX so that Windows machines can execute UNIX commands on a remote system. Convert the DLL calls to shared library calls.

12–13. serverSK shuts down the accept thread by closing the connection socket (see Run 12-3). Is there a better way to terminate the accept thread? Potential approaches to investigate include queuing an APC as in Chapter 10. Or, can you use the Windows extended function, AcceptEx (Chapter 14 may help)?

12–14. A comment after Program 12-5 (SendReceiveSKST) mentions that you cannot assure that DLL_THREAD_DETACH will be invoked for every thread, and, therefore, there could be resource leaks (memory, temporary files, open file handles, etc.). Implement a solution in SendReceiveSKST that uses DLL_PROCESS_DETACH to free all allocated memory. Because you cannot find the allocated memory with TlsGetValue, maintain a list of all allocated memory.

12–15. Read about the SSL in MSDN and the Additional Reading references. Enhance the programs to use SSL for secure client/server communication.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset