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.
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 HANDLE
s 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).
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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.
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
.
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
.
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
.
If there is a server with a listening socket, the client can connect with the connect
function.
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.
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.
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.
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.
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
).
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.
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:
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.
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
.
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.
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.
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.
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.
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.
The server state logic involves both the boss and server threads. Exercise 12–6 suggests an alternative approach where the boss is not involved.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
• 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.
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”).
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.
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.
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.
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.
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 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).
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.
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.
Network Programming for Microsoft Windows by Jim Ohlund is a good Winsock reference.
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.
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.