C/C++ Users Journal, February 2006
It has become increasingly common for large applications to be broken down into a set of cooperating programs, which communicate with each other using some sort of communications protocol. These programs might run on different machines, under different operating systems, and be written in different languages. They might also be running on the same machine. In fact, these programs might even really be different threads in the same program.
This month, I'll discuss sockets, the means by which C++/CLI programs can communicate with each other (and, most likely, with programs written in other languages as well). For the most part, I'll discuss communication between programs running on the same machine. While communication across a network is indeed an important topic, this article is about interprocess communication, not networking.
Consider an application involving a database query facility. One program, called the server, waits for requests from another program, called a client. When a request is received, the server executes the query and returns the results (or perhaps an error message) to the client. In many cases, there will be numerous clients, all sending queries to the same server at the same time, requiring the server to be a more sophisticated program than are the clients.
While in some environments the server runs on a machine dedicated to that task, a server could simply be a program running alongside numerous others, some of which are servers and/or clients in other applications. In fact, if our database server needs access to files not resident on its own system, it may well be a client of a file server on some other system. A single program might have a server thread and one or more client threads. Therefore, we must be careful when using the terms client and server. While they might convey a familiar meaning in the abstract sense, they might be used quite differently in different physical implementations.
From a generic viewpoint then, a client is a consumer of services provided by a server, and a server can be a client of some other service.
Let's start by looking at a simple yet representative server program (see Listing 1). This program waits for a client to send it a pair of integers. The server then adds these values together and sends the result back to the client.
The socket-related support we need is provided by the namespaces System::Net and System::Net::Sockets, so we make them available. (This also requires we reference the assembly System.dll during build.) In addition, because communication via a socket involves a stream, we need the machinery from System::IO.
When executed, the server needs to know the port number it should monitor for connection requests from a client. This integer value is supplied via a command-line argument. Typically, a port number is in the range 0-65535, where port numbers 0-1023 are reserved for special purposes. Therefore, unless a system is documented otherwise, port numbers 1024-65535 are available for use by application program servers.
As shown in case 1, we can find out the range of port numbers that are valid for a particular system via the public static fields MinPort and MaxPort in class IPEndPoint.
In case 2, we find out our own host name, resolve that to an IPHostEntry, and from that find out the IP address for that host. Then in case 3, we combine that IP address and the port number supplied to create an object of type IPEndPoint, which allows the server to connect to a service.
A Socket is created (see case 4) as a managed version of an Internet transport service, and once that socket is created, it is bound to a specific endpoint through the Bind function (see case 5). The Socket then declares that it is "open for business" by listening for connection requests (see case 6). The argument passed to Listen indicates the length of the connection-pending request queue. Because we are only waiting for a single client to connect, a value of 1 is sufficient.
When a Socket is created, the constructor takes a series of arguments, of which only one possible combination is shown. Numerous others are available; refer to the documentation for the enumerations AddressFamily, SocketType, and ProtocolType for more information.
Sockets are created in blocking mode by default, as is shown by the output from case 7. That is, they wait indefinitely for a connection request.
A blocked Socket wakes up when it receives a connection request from a client. By calling Accept (as in case 8), it accepts that request and creates another Socket on which it can perform communications with that client. We see then that the server has two Sockets: one to listen for client connection requests and another to communicate with a connected client. Of course, a server can have connections with multiple clients at the same time, in which case it will have one Socket for each.
In our simple example, we are only interested in dealing with the first client that requests a connection, so once we have that, we can close the socket on which we were listening for connection requests (see case 9).
In cases 10-12, we establish a NetworkStream with the newly connected Socket, along with a reader and a writer that we'll use to receive requests from and send results to that Socket.
The server loops indefinitely, reading pairs of integers, computing their sum, and returning the result back to the client. When end-of-file is detected on the input stream (by the client's having closed its end of the Socket), an exception of type EndOfStreamException is thrown, resulting in the closing of the I/O stream and socket. Then the server terminates.
The call to Socket::ShutDown in case 18 disables the ability to both receive and send on that Socket. Now since the server is in the process of shutting down because its only client told it to, this function call is redundant. However, it's useful to know how to do this in case a server needs to terminate prematurely.
The reason for catching an IOException in case 17, is to handle the situation in which the client terminates prematurely, before it can close its end of the Socket.
Let's look at the client-side program (see Listing 2). After connecting to the server, the client sends the server a number of pairs of random-valued integers, waiting for each pair's sum to be returned before sending the next. We see then that the client and server communications are synchronized with each other in that no new pair of values is sent until the result of the previous pair has been received.
As with the server, the client gets an IP address and combines it with a port number to make an IPEndPoint. It then connects to the server, which is blocked in listening mode.
There is a constant three-second delay in between each send/receive operation simply to slow down the programs so we can observe the output from the operations as they occur.
Here's an example of the output produced by the server program when told to use port 2500:
Server listener blocking status is True New connection accepted Received values 42 and 69, sent result 111 Received values 66 and 71, sent result 137 Received values 7 and 93, sent result 100 Received values 43 and 65, sent result 108 Received values 45 and 3, sent result 48 Shutting down server
and here's the corresponding output from the client when told to use port 2500 and to send five pairs of values:
Sent values 42 and 69, received result 111 Sent values 66 and 71, received result 137 Sent values 7 and 93, received result 100 Sent values 43 and 65, received result 108 Sent values 45 and 3, received result 48 Notified server we're shutting down Shutting down client
The client and server in the previous sections exchanged only values of a primitive type, such as int; however, it is very probable that applications will need to send and receive objects of various user-defined types as well, and that involves serialization, a topic we covered in an earlier installment.
Consider the case in which a banking application involves the use of a number of transaction types, such as Deposit, Transfer, and Withdrawal, each of which is derived from Transaction. A server can handle multiple clients that send it any number and combination of these kinds of transactions, simply by setting up the appropriate serialization and deserialization machinery.
To reinforce the material I've discussed, perform the following activities:
Client 2600 4 Sent values 56 and 35, received result 91 Sent values 48 and 20, received result 68 Sent values 6 and 97, received result 103 Sent values 76 and 9, received result 85 Notified server we're shutting down Shutting down client Client 2600 2 Sent values 69 and 66, received result 135 Sent values 84 and 45, received result 129 Notified server we're shutting down Shutting down client Server 2600 Waiting for new connection request New connection accepted Started thread Thread-1 Waiting for new connection request Executing thread Thread-1 Received values 56 and 35, sent result 91 New connection accepted Started thread Thread-2 Waiting for new connection request Executing thread Thread-2 Received values 69 and 66, sent result 135 Received values 48 and 20, sent result 68 Received values 84 and 45, sent result 129 Received values 6 and 97, sent result 103 Shutting down server thread Thread-2 Received values 76 and 9, sent result 85 Shutting down server thread Thread-1