Add Book to My BookshelfPurchase This Book Online

Chapter 13 - Interprocess Communication

UNIX Systems Programming for SVR4
David A. Curry
 Copyright © 1996 O'Reilly & Associates, Inc.

UNIX-Domain Sockets
UNIX-domain sockets are similar to named pipes, in that they provide an address in the filesystem that unrelated processes may use to communicate. They differ from named pipes in the way that they are accessed. Named pipes (FIFOs) are accessed just like any other file; in fact, a command executed from the shell whose input or output is redirected to a FIFO never need know that it is using a named pipe. On the other hand, UNIX-domain sockets are implemented using the Berkeley networking paradigm, usually called the socket interface. This interface has a set of specialized functions used to create, destroy, and transfer data over communications channels.
Interprocess communication with sockets is usually described in terms of the client-server model. In this model, one process is usually called the server ; it is responsible for satisfying the requests made of it by other processes, called clients. A server usually has a well-known address; this address is always the same, so that client programs will know where to contact it. An analogy in the real world might be the telephone number 911, which, at least in the United States, contacts the police/fire/ambulance service wherever it is dialed.
In order to use the functions described in this section, a program must be linked with the -lnsl and -lsocket libraries on Solaris 2.x, and with the -lnsl library on IRIX 5.x.
Creating a Socket
The basic unit of communication in the Berkeley networking paradigm is the socket, created with the socket function:
    #include <sys/types.h>
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
The domain argument specifies the domain, or address family, in which addresses should be interpreted; it imposes certain restrictions on the length of addresses, and what they mean. In this section, we use the AF_UNIX domain, in which addresses are ordinary UNIX pathnames. In the next chapter, we look at the AF_INET domain, which is used for Internet addresses.
There are two types of communications channels supported by sockets, selected with the type argument:
SOCK_STREAM
This type of connection is usually called a virtual circuit. It is a bidirectional continuous byte stream that guarantees the reliable delivery of data in the order in which it was sent. No data can be sent until the circuit is established; the circuit then remains intact until the conversation is complete. A telephone call is a real-world example of a virtual circuit; a FIFO is another example.
SOCK_DGRAM
This type of connection is used to send distinct packets of information called datagrams. Datagrams are not guaranteed to be delivered to the remote side of the communications channel in the same order they were sent. In fact, they are not guaranteed to be delivered at all. (This is not as undesirable as it may sound; there are many applications for which it is perfectly suited.) The U.S. mail system is a real-world example of datagrams: each letter is an individual message, letters may arrive in a different order than they were sent, and some may even get lost.
The protocol parameter specifies the protocol number that should be used on the socket; it is usually the same as the address family. In this section we use the PF_UNIX protocol family; in the next chapter we examine the PF_INET family. The protocol parameter can usually be given as 0, and the system will figure it out.
When a socket is successfully created, a socket descriptor is returned. This is a small non-negative integer, similar to a file descriptor (but with slightly different semantics). If the socket cannot be created, -1 is returned and the error information is stored in errno.
A second method for creating sockets can be used by two related processes (parent and child) to establish a full-duplex communications channel:
    #include <sys/types.h>
    #include <sys/socket.h>
    int socketpair(int domain, int type, int protocol, int sv[2]);
This creates an unnamed pair of sockets and places their descriptors in sv[0] and sv[1]. Each socket is a bidirectional communications channel. A read from sv[0] accesses the data written to sv[1], and a read from sv[1] accesses the data written to sv[0]. If the socket pair is successfully created, socketpair returns 0. Otherwise, it returns -1 and stores the error code in errno.
Server-Side Functions
The server process needs to call the bind, listen, and accept functions, in order, if it is to exchange data with a client.
Naming a socket
After creating a socket, a server process must provide that socket with a name so that client programs can access it. The function to assign a name to a socket is called bind:
    #include <sys/types.h>
    #include <sys/socket.h>
    int bind(int s, const struct sockaddr *name, int addrlen);
After completion, the communications channel referenced by the socket descriptor s will have the address described by name. In order for bind to succeed, the address must not already be in use. Because name may be of different sizes depending on the address family being used, addrlen is used to indicate its length. If bind succeeds, it returns 0. If it fails (often because the address is already in use), it returns -1 and stores an error code in errno.
In the UNIX domain, the name parameter is actually of type struct sockaddr_un, defined in the include file sys/un.h:
    struct sockaddr_un {
        short    sun_family;
        char     sun_path[108];
    };
The sun_family element is always set to AF_UNIX, identifying this address as being in the UNIX domain. The sun_path element contains the filesystem pathname of the socket. As a side effect of the implementation of UNIX-domain sockets, this file is actually created when it is bound. Before a server calls bind, it should make sure that this file does not exist and delete it if it does, or the bind will fail because the address is already in use.
Waiting for connections
If a server is providing a service via a stream-based socket, it must notify the operating system when it is ready to accept connections from clients on that socket. To do this, it uses the listen function:
    #include <sys/types.h>
    #include <sys/socket.h>
    int listen(int s, int backlog);
This function tells the operating system that the server is ready to accept connections on the socket referenced by s. The backlog parameter specifies the number of connection requests that may be pending at any given time; most operating systems silently limit this to a maximum of five. If a connection request arrives when the queue of pending connections is full, the client will receive a connection-refused error.
Accepting connections
To actually accept a connection, the server uses the accept function:
    #include <sys/types.h>
    #include <sys/socket.h>
    int accept(int s, struct sockaddr *name, int *addrlen);
When a connection request arrives on the socket referenced by s, accept returns a new socket descriptor. The server can use this new descriptor to communicate with the client; the old descriptor (the one bound to the well-known address) may continue to be used for accepting additional connections. When the connection is accepted, if name is not null, the operating system stores the address of the client there, and stores the length of the address in addrlen. If accept fails, it returns -1 and places the reason for failure in errno.
Connecting to a Server
In order to connect to a server using a stream-based socket, the client program calls the connect function:
    #include <sys/types.h>
    #include <sys/socket.h>
    int connect(int s, struct sockaddr *name, int addrlen);
This function connects the socket referenced by s to the server at the address described by name. The addrlen parameter specifies the length of the address in name. If the connection is completed, connect returns 0. Otherwise, it returns -1 and places the reason for failure in errno.
A client may use connect to connect a datagram socket to the server as well. This is not strictly necessary, and does not actually establish a connection. However, it does enable the client to send datagrams on the socket without having to specify the destination address for each datagram.
Transferring Data
To transfer data on a stream-based connection, the client and server may simply use read and write. Two other functions are also used with stream-based sockets:
    #include <sys/types.h>
    #include <sys/socket.h>
    int recv(int s, char *buf, int len, int flags);
    int send(int s, const char *buf, int len, int flags);
These functions are like read and write, except that they have a fourth argument. This argument allows the program to specify flags that affect how the data is sent or received. Only one flag has any meaning in the UNIX domain:
MSG_PEEK
If specified in a call to recv, the data is copied into buf as usual, but it is not “consumed.” Another call to recv will return the same data. This allows a program to “peek” at the data before reading it, to decide how it should be handled.
When using datagram-based sockets, the server does not call listen or accept, and the client (generally) does not call connect. Thus, there is no way for the operating system to determine automatically where data on these sockets is to be sent. Instead, the sender must tell the operating system each time where the data is to be delivered, and the receiver must ask where it came from. Two other functions accomplish this:
    #include <sys/types.h>
    #include <sys/socket.h>
    int recvfrom(int s, char *buf, int len, int flags,
            struct sockaddr *from, int *fromlen);
    int sendto(int s, const char *buf, int len, int flags,
            struct sockaddr *to, int tolen);
The sendto function sends len bytes from buf via the socket referenced by s to the server located at the address given in to. The tolen parameter specifies the length of the address. The number of bytes actually transferred is returned, or -1 if an error occurred. There is no indication whether or not the data actually reaches its destination. The recvfrom function receives up to len bytes of data from the socket referenced by s and stores them in buf. The address from which the data came is stored in from, and fromlen is modified to indicate the length of the address. The number of bytes received is returned, or -1 if an error occurs.
Destroying the Communications Channel
One way to close a socket is with the close function, with the side effect that if the socket refers to a stream-based socket, the close will block until all data has been transmitted.
Another way is with the shutdown function:
    #include <sys/types.h>
    #include <sys/socket.h>
    int shutdown(int s, int how);
This function shuts down either or both sides of the communications channel referenced by s, depending on the value of how. If how is 0, the socket is shut down for reading; all further reads from the socket return end-of-file. If how is 1, the socket is shut down for writing; all further writes to the socket will fail. This also informs the operating system that no effort need be made to deliver any outstanding data on the socket. If how is 2, then both sides of the socket are shut down and it essentially becomes useless.
Putting it All Together
Examples 13-6 and 13-7 show small server and client programs that transfer data using a virtual circuit. These two programs are identical in operation to the programs in Examples 13-4 and 13-5, except that they are implemented using UNIX-domain sockets.
Example 13-6:  socket-srvr
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#define SOCKETNAME  "mysocket"
int
main(void)
{
    char buf[1024];
    int n, s, ns, len;
    struct sockaddr_un name;
    /*
     * Remove any previous socket.
     */
    unlink(SOCKETNAME);
    /*
     * Create the socket.
     */
    if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(1);
    }
    /*
     * Create the address of the server.
     */
    memset(&name, 0, sizeof(struct sockaddr_un));
    name.sun_family = AF_UNIX;
    strcpy(name.sun_path, SOCKETNAME);
    len = sizeof(name.sun_family) + strlen(name.sun_path);
    /*
     * Bind the socket to the address.
     */
    if (bind(s, (struct sockaddr *) &name, len) < 0) {
        perror("bind");
        exit(1);
    }
    /*
     * Listen for connections.
     */
    if (listen(s, 5) < 0) {
        perror("listen");
        exit(1);
    }
    /*
     * Accept a connection.
     */
    if ((ns = accept(s, (struct sockaddr *) &name, &len)) < 0) {
        perror("accept");
        exit(1);
    }
    /*
     * Read from the socket until end-of-file and
     * print what we get on the standard output.
     */
    while ((n = recv(ns, buf, sizeof(buf), 0)) > 0)
        write(1, buf, n);
    close(ns);
    close(s);
    exit(0);
}
Example 13-7:  socket-clnt
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/un.h>
#define SOCKETNAME  "mysocket"
int
main(void)
{
    int n, s, len;
    char buf[1024];
    struct sockaddr_un name;
    /*
     * Create a socket in the UNIX
     * domain.
     */
    if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        exit(1);
    }
    /*
     * Create the address of the server.
     */
    memset(&name, 0, sizeof(struct sockaddr_un));
    name.sun_family = AF_UNIX;
    strcpy(name.sun_path, SOCKETNAME);
    len = sizeof(name.sun_family) + strlen(name.sun_path);
    /*
     * Connect to the server.
     */
    if (connect(s, (struct sockaddr *) &name, len) < 0) {
        perror("connect");
        exit(1);
    }
    /*
     * Read from standard input, and copy the
     * data to the socket.
     */
    while ((n = read(0, buf, sizeof(buf))) > 0) {
        if (send(s, buf, n, 0) < 0) {
            perror("send");
            exit(1);
        }
    }
    close(s);
    exit(0);
}
    % socket-srvr &
    % socket-clnt < /etc/motd
    Sun Microsystems Inc.   SunOS 5.3       Generic September 1993

Previous SectionNext Section
Books24x7.com, Inc © 2000 –  Feedback