Bryan Glennon is president of BPG Consulting, Inc. He has been professionally involved in software engineering for the last nine years. His recent clients include Rockwell International, Ameritech Applied Technologies and McDonalds. He can be reached at P.O. Box 841, Bensenville, IL 60106, (708) 595-6059.
Inter-Process Communication (IPC) allows one or more processes to share information. IPC methods include signals, semaphores, shared memory, pipes, and sockets. Some, such as signals and semaphores, convey only a little information, (such as whether an event has occurred). Shared memory allows several processes to read from and write to the same area of memory, to convey considerably more information. The processes must cooperate, however, to ensure that the shared memory is only accessed when it is in a "safe" state no updates are incomplete.
Pipes connect two processes more safely. One process writes data into one end of the pipe, and the other reads data from the opposite end. Pipes, however, have one major disadvantage the two processes using the pipe must share a common ancestor.
Sockets provide a communication link just like a pipe, but for processes with no common ancestor. Indeed, many versions of UNIX implement pipes by using a pair of sockets. Sockets allow separate processes to transfer information between them. The processes may be on the same or different processors.
To demonstrate socket use, I will build a small time-tracking system. This system helps the user keep track of the time spent on various tasks. A back-end, or server process, reads events from a socket, and several front-end, or client processes, send events to the socket. Implementing such a system without using IPC is possible, but the IPC version has several advantages.
In a multi-user environment, a single process (the server) handling all file access reduces the need for file or record locking in the application. The socket provides synchronization, which guarantees sequenced, reliable delivery of the messages (depending on the choice of socket types). Since the server and client process are not started by the same process (they share no common ancestor), sockets are a logical choice for the IPC mechanism. Also, by moving all of the file manipulations to the server, each client process can be smaller. All changes are localized to the server, which presents a consistent interface to all clients.
The first example shows how processes running on the same processor can communicate through sockets. I then expand the example to show server and client processes on different hosts of a connected network.
The server process waits for an event to arrive. Once an event arrives, it writes a record to the time entry file. Listing 1 shows the structure of an event. All messages between the clients and the server consist of exactly one event. The event code in the EVENT_TYPE structure is set by the client process, based on the name under which it was invoked. Listing 2 shows the code for the server process.
First, the server must create the socket by calling socket(domain, type, protocol). The socket's domain determines where the socket resides and where processes must be in order to access the socket. In Listing 2, the socket is created with a domain of AF_UNIX, placing the socket in the UNIX domain. Only processes running on the same host as the server can access a UNIX domain socket. When the operating system creates the socket, it places an entry in the file system, just as when a file is created. When connections to the socket are made, both the server and the client processes use the full path name of the socket. Sockets may also be created in the Internet domain, which allows not only processes on the same processor to access the socket, but also processes on the same network. You can access sockets in the Internet domain by specifying the address of the host processor, and the port on the socket's host.
The type parameter determines the socket type. In Sun OS, the valid socket types are SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, and SOCK_SEQPACKET. Stream sockets (SOCK_STREAM) provide reliable, two-way byte streams, and sequence is guaranteed when using stream type sockets. Stream-based sockets also support out-of-band (OOB) data. OOB data is announced to the receiving socket when it arrives, instead of being placed in the message queue. The receiving process must take special action to process this out-of-band data. When OOB data arrives, a signal (SIGUGR) is sent to the receiving process. When the process receives the signal, it can read the data, peek at the data, or ignore the data. Sending a message out-of-band is especially useful when sending an abort message that will cause the socket to cease processing any messages that could be in the socket.
Datagram sockets (SOC_DGRM) support connectionless, unreliable message transfer of fixed-length message packets. The service is unreliable because the client process does not have to establish a connection, via the connect () call, before sending the message. Without this step, the system cannot guarantee that the message is delivered. Since the sender does not have to establish a connection, less overhead is involved with this type of socket, and there is no risk of failure due to an invalid address. Since delivery is not guaranteed, DATAGRAM type sockets are best used for non-critical message transfer, or when some other form of acknowledgment is used.
The remaining socket types are used for special purpose applications: SOCK_SEQPACKET socket types are not yet implemented; they are planned to provide sequenced, reliable, connection-based transfer of messages of a fixed maximum length. SOCK_RAW type sockets provide access to the underlying network protocol. In general, only someone with superuser permissions can use this socket type.
The protocol argument allows the creator of the socket to specify which network transport protocol will be used. Implementation-dependent restrictions govern which protocol can be used with the various socket types and domains. A value of zero can be used for the protocol, which specifies that the default protocol associated with the type and domain should be used.
The call to socket() returns a small, positive integer similar to a file descriptor. This number refers to the socket in future operations. If the call to socket() fails, it returns -1. The global variable errno will be set to indicate the error. Specific errors will vary from machine to machine, but common errors include: a protocol that is not supported, lack of permissions, or exceeding system limits for buffers or descriptors.
After the server successfully creates the socket, it must associate the socket with an address. This process, called binding, is done by calling bind(socket, address, length). The first argument is just the descriptor returned by calling socket(). The structure pointer passed as the second argument is first filled in by the caller. For sockets in the UNIX domain, the full path name of the socket is placed into the structure. A pointer to this structure is then passed to bind(). The third argument, the name length, is the size of the second argument, not the length of the string that represents that path name. Between the call to socket() and the call to bind(), the socket exists in the address space appropriate for the given domain, but it has no name associated with it. bind() returns zero on a successful call, or -1 for failure. As with all system calls, the global variable errno is set to the appropriate error code. Again, the specific errors will vary, but the call will fail if, for example: the descriptor does not refer to a socket, the specified address is already in use, or the address is invalid.
When the server creates sockets in the UNIX domain, it must call unlink() to delete the file system entry once the socket is no longer needed.
After binding a socket to an address, the server calls listen(socket, count). This call establishes the number of simultaneous inbound connections that the socket will accept. It applies only to sockets of type SOCK_STREAM or SOCK_SEQPACKET. The socket parameter is the descriptor returned by the call to socket(). The count parameter specifies the maximum queue length of pending connections. If a request for connection arrives when the queue is full, the connection is denied, and errno is set to ECONNREFUSED. listen() returns a value of zero if successful, otherwise it returns -1. The call can fail for a number of reasons, including specifying a bad descriptor or a socket type other than SOCK_STREAM or SOCK_SEQPACKET.
After establishing the connection queue, the server calls accept(socket,address, len) to allow other processes to connect to the socket. The first parameter is a socket descriptor returned by socket. The second and third parameters provide the address of the connecting process, so two-way communication can be established. These parameters can be zero, in which case no information is returned, and two-way communication is not possible.
accept() clones the socket represented by the first argument when a connection is made, allowing the original socket to continue to accept connections, while communication takes place over the cloned socket. Only SOCK_STREAM type sockets can use this mechanism. accept() returns a socket descriptor which is used for communication with the accepted socket. If the call fails, it returns -1. The call can fail for various reasons, including a bad descriptor or invalid (and non-null) pointers for the address and length parameters.
After calling accept(), the server begins reading data from the returned socket descriptor using the standard system call read(). From here on, the descriptor can be read and processed like a normal file descriptor.
The server will continue to run, waiting for connections at the accept() call. When it receives an event code of SHUT_DOWN, it closes the socket using the standard system call close(). It removes the file system entry by calling unlink(). The process cleans up the time entry file and exits.
When a user wants to make a time accounting entry, he invokes the client process (or processes). There are several client processes, all of which run the same program. Under UNIX, client process names are all linked to the same program. Any of these names will invoke the program. When a child process is executed, the command name is passed as the first argument to the program. This name, (the variable argv in the example client process) determines the event to be sent to the server. Multiple program instances could be used, each with a different name, though this would waste disk space.
Once invoked, the client process builds the event message, locates the address of the server (using the agreed-upon name), sends the event, and exits. Listing 3 shows the code for a simple client process.
After evaluating argv to determine the event type, the EVENT_TYPE structure ev is filled. The client process creates a socket in the same domain and of the same type used by the server process. The socket's address is then built, using the same parameters as the server process. The client then calls connect(). Since the socket has already been created, bound to an address and is accepting connections, the connection is allowed. At this point, the client uses the standard system call write() to write an event record to the socket. Once the write call returns, the client closes the socket and exits.
You must make several minor modifications to allow the server and client processes to be on different hosts on a connected network. First, you must place the socket in the Internet domain (AF_INET), not the UNIX domain. Sockets in the Internet domain have an Internet address contained in the sockaddr structure. Also, the clients must specify the name of the service and the machine on which it is running. In the example, this is hard-coded for simplicity, but in the real world, you could approach this in one of two ways. Either the service would be so widely needed and used that a well-known port number would be assigned, and possibly even an alias for the host on which the server ran (e.g., Port 64 on machine TimeHost). Or an entry would be made in a services database, from which the clients could retrieve the address of the server by using an OS provided call (such as getservbyname under SUN OS). Listing 4 shows the server process with the network modifications.
The first change is in the type of the address structure. Instead of being a struct sockaddr, it is now a struct sockaddr_in. The call to socket specifies AF_INET as the domain, requesting that the socket be placed in the Internet domain. The address of the host (in this case, a machine called utopia) is retrieved using the gethostbyname() system call. This call returned a pointer (hp) to a host entry structure. This structure's address is copied into the socket's address structure. The call to bind() is made, and after the socket is bound to an address, the port number is printed out. The port number, if not specified, is set during the call to bind(). From this point on, the network version is the same as the earlier example.
The two function calls htons() and ntohs() convert the arguments from host byte-order to network byte-order and back.
The client process contains similar modifications. For this example only, the port number returned when the server is started must be entered on the command line. Listing 5 shows the main function of the client process with the changes for the network version.
Sockets provide a generalized platform that discrete processes can use to share information. The delivery can be reliable (by the use of stream type sockets), or unreliable (by the use of datagrams). Processes on the same or on different processors can use sockets to share information. Sockets do not require processes to share a common ancestor, so they are useful whenever the processes that need to communicate have no common ancestor and reside on different processors. Most network services use socket pairs to communicate.