C/C++ Users Journal, February 2006

C++/CLI Sockets

Interprocess communication, C++/CLI style

By Rex Jaeschke

Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the Standards for C++/CLI, CLI, and C#. Rex can be reached at http://www.RexJaeschke.com/.

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.

Introduction

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.

Server-Side Sockets

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.

Client-Side Sockets

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

Serialization Over Sockets

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.

Exercises

To reinforce the material I've discussed, perform the following activities:

  1. If a server socket's connection queue is full, what happens to new client connection requests?
  2. What happens if the server terminates while a client has a socket open to it, or vice versa?
  3. Try running the server program and two copies of the client program. As we have seen, the server is only equipped to deal with one client, making it a less-than-production-strength server. To make the server handle multiple clients simultaneously, it needs to be multithreaded. Make the appropriate changes and test your new server with two, three, or more clients running such that each client is serviced independently of the others.
  4. For example, here is some output when two clients are run:
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

  1. In a previous installment on inheritance (CUJ, June 2005), we saw a family of financial-related transaction types. Using the source files from that installment, write a client that creates and sends a variety of transactions of the three concrete types Deposit, Transfer, and Withdrawal to a multithreaded server that simply calls the corresponding PostTransaction function. Test your application with two, three, or more clients running simultaneously, with each one sleeping for two seconds in between message sends. The communication path need only go from the client to the server because the server has no information to return.