Using TCP Under Unix

Trying to see how to get started using interprocess communication under Unix (including Linux) tends to be a somewhat daunting task... there are a couple of very complete tutorials at An Introductory 4.4BSD Interprocess Communication Tutorial and An Advanced 4.4BSD Interprocess Communication Tutorial; also, the man pages for the various calls I describe here go into more detail.

The purpose of this tutorial is much more modest: it tries to make it possible for you to get your first TCP-based program up and running with as little pain as possible.

The Socket Model

The Berkeley 4BSD series was an almost unbelievable advance in Unix development. Among the features it added were virtual memory, shared memory, and sockets. Sockets were added in 4.2BSD.

Conceptually, internet sockets on a Unix system look like a numbered array of interprocess communication "ports" -- so there is a port 0, port 1, port 2, and so forth. They pretty much expect to be used in a client-server relationship; a daemon wishing to provide a service creates a socket, binds it to a port, and listens to it; a client program connects to the socket and makes requests. The daemon is also able to send messages back to the client.

Notice, by the way, that a "socket" is the communication channel, and the "port" is its address. Sort of like a telephone: the phone is the communication device, and the phone number is how you find it to talk to it.

Even though the process of establishing a socket is asymmetrical, the actual use of the socket doesn't have to be - it's sort of like making a phone call. Making the call is asymmetrical (somebody is the caller), but the conversation needn't be.

Creating a Socket (Both Client and Server)

For two processes to communicate, each must create a socket. The server will bind its socket to a port number and wait for clients to connect to it; the client will connect its socket to the server. So, the first step is the same for both the server and the client - call socket():

int socket(int domain, int type, int protocol);

The socket() call returns a file descriptor (or a -1 if there's an error). We'll use this file descriptor in the later calls.

For more information, man 2 socket

The Server Side

The next steps for the server are to bind the socket to a particular port number, to decide how long a queue of connections it will allow, and to accept connections from clients.

Binding an Address

At this point we've created a socket, but we haven't given it a name so it isn't very useful. We give the socket a name using the bind system call:

int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
This call gives the socket (which was returned by socket()) a name. For an internet socket, the name is a struct defined as

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    u_int16_t      sin_port;   /* port in network byte order */
    struct in_addr  sin_addr;  /* internet address in network byte
                                  order */
};

/* Internet address. */
struct in_addr {
    u_int32_t      s_addr;     /* address in network byte order */
};

For an internet socket, sin_family is AF_INET; sin_port is the port number, and sin_addr is the IP address.

It takes a bit of thought to realize why the IP address has to be specified - it turns out that it's pretty much universal that a machine has more than one IP address; for instance, any machine that's on a network will have addresses of 127.0.0.1 and whatever its actual network address is. Also, when a gateway machine has two ethernet cards, one will have an IP address on one of the networks, and the other will have an IP address on the other one (this will be true of your router at home, for instance). You can specify that any valid IP address for the host can be used by specifying htonl(INADDR_ANY).

One little wrinkle on this is that port numbers below 1024 are reserved - that means only processes with an effective user id of 0 (ie the root) can bind to those ports.

It's also probably a good idea to mention "host byte order" and "network byte order". If you have an object like an integer, which is larger than one byte, which you need to send across the network then you need to decide what order to send the bytes making up the object. With Intel, you send the bytes with the least significant byte first, then the second, and so on up to the most significant byte. This is called "little-endian." The standard for the network however, is to send the data most-significant byte first, then second, and so on down to the least-significant byte. This is called "big-endian" (yes, the terms are references to Gulliver's Travels).

There are macroes which will convert between host order and network order; these are htonl() for longwords and htons() for shorts. The nice thing about them is that when you compile your code for any host, they convert from that host's native order to the network order. So the code I'm showing here will work just as well on a big-endian machine as a little-endian.

For more information, man 2 bind and man 3 byteorder

Listening to the Socket

Once the socket has been created and bound, the daemon needs to indicate that it is ready to listen to it. It does this with (surprise!) the listen() system call, as in

int listen(int s, int backlog);

The main thing this does is to set a limit on how many would-be clients can be queued up trying to connect to the socket (the limit in this example is 5). If the limit is exceeded the clients don't actually get refused, instead their connection requests get dumped on the floor. Eventually they will end up retrying. This will only really matter if (1) you've got a horribly poorly written daemon, that takes significant time to respond but doesn't fork a subtask to handle it, (2) you're running a very, very busy e-commerce site, or (3) somebody's trying a Denial of Service (DOS) attack on your daemon.

For more information, man 2 listen

Accepting Connections

Finally! The server is able to accept connections by calling accept():

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

For this call, s is, as you'd expect the socket that was returned oh so long ago by the socket call. The accept() call blocks until the client connects to the socket.

The sockaddr * and socklen_t parameters are there so you can find out the address of the client when it connects (you can imagine writing a server that behaves differently depending on the client that connects). If you pass a pointer to NULL, it will be ignored.

The interesting thing about this call is that it returns a new socket. This means the daemon can communicate with the client using the newly created (and unnamed) socket, while continuing to listen on the old one.

For more information, man 2 accept

The Client Side

Next, we discuss how the client finishes connecting to the server's socket. This is done with a call to gethostbyname() to find the server, and connect() to actually hook it up.

Finding the Server

We find the server's IP address with a call to gethostbyname():

struct hostent *gethostbyname(const char *name);

This function does a lookup of a host's name in the hostname resolution system (which includes DNS) and returns its IP address.

It returns a pointer to a data structure that includes (among other things) the host's IP address. We can copy this into a struct sockaddr_in, and set the port number, with:

server_addr.sin_family = server->h_addrtype;
server_addr.sin_port = htons(PORTNUM);
server_addr.sin_addr = *(struct in_addr *)(server->h_addr);

For more information, man 3 gethostbyname.

Connecting to the Server

Now that we've found the server, we can connect to it with connect():

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

For more information, man 2 connect

Once the connection is made, you can just read and write on the socket's file descriptor to communicate with the server.

The Sample Server and Client

I've written a simple client and server that show all this; here are links to client.c (the client), server.c (the server), and portnum.h (a .h file they both need).

The server is just a simple "uppercaseifier": it takes strings from the client, and converts them to upper case. It's written using one of the three major strategies for server-writing: it forks a child for each connection to deal with the client. The other major ways to do it are (1) to spawn a thread to deal with clients, and (2) to leave the whole thing single-threaded, and make sure you're able to respond to client requests quickly enough that your listen() queue doesn't fill.

The client just generates 4096 bytes of random printable ASCII characters, sends it to the server, reads the response, and quits.

Output From the Client and Server

Here's an example of running the server on viper.cs.nmsu.edu. I'll be running the client on snowball.wb.pfeifferfamily.net, a machine inside my home domain (with a hostname that only resolves when you are in fact inside my home domain). Something to notice from this is that there is no guarantee that the messages sent from one end are the same size when they arrive at the other. The whole right number of bytes will arrive eventually, but may not be divvied up the same (expecting to read a packet when using a stream protocol is one of the fairly standard errors people make in network programming: your code has to be robust enough to handle partial reads, and reassemble messages as needed).

Server Output

viper:121% ./server
server ready to accept connections
got new client connection fd=4
server ready to accept connections
got 1024 bytes from client
got 424 bytes from client
got 1024 bytes from client
got 424 bytes from client
got 1024 bytes from client
got 176 bytes from client
got 0 bytes from client
client closed connection

Client Output

snowball:529$ ./client viper.cs.nmsu.edu > client.log
sent buffer to server
Recieved 1024 of 4096 bytes
Recieved 2472 of 4096 bytes
Recieved 4096 of 4096 bytes

Some Extra Information

There are a couple more things to know about whch make life a whole lot easier here. We're used to doing IO on a single file descriptor at a time; it turns out that there are ways both for a program to wait for input on any file descriptor, and also a way to receive a signal when input becomes available.

The select() Call

This is a system call whose purpose is to allow a process to block waiting for input on any of a set of file descriptors, and then read data when it comes available on one of them.

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

This call lets us set three different sets of file descriptors to wait on, specified by readfds, writefds, and exceptfds. For our purposes, we only care about readfds; we want it to be the set of descriptors we might see some data on.

To initialize a file descriptor set, use the macro FD_SET. If the set has been declared with

fd_set readfds;

then it is initialized with

FD_ZERO(&readfds);

You can add file descriptors to the set with

FD_SET(fd, &readfds);

where fd is the descriptor you want to add.

You'll want to use the select() in the server's main loop; initially, the readfds set should consist only of the socket you want to listen on. When you return from select() you can check if the file descriptor that can be read is that socket; if it is, you can finish the connection with the client by calling accept() at that time. If it's on one of the client file descriptors, you can read the message.

Here's another sample server; this one does everything in one big process, using select() to see what has to happen next.

SIGIO

You can make a client listen to a message by setting a handler on the SIGIO signal. To do this, you would use a signal() call to set a handler for the SIGIO signal, and then use fcntl() to have a signal delivered when data comes in. For this one, I'll steal the code straight from the IPC tutorial I link to up at the top:

signal(SIGIO, io_handler);

/* Set the process receiving SIGIO/SIGURG signals to be the current process */

if (fcntl(s, F_SETOWN, getpid()) < 0) { 
     perror("fcntl F_SETOWN"); 
     exit(1); 
}

/* Allow receipt of asynchronous I/O signals */

if (fcntl(s, F_SETFL, FASYNC) < 0) { 
     perror("fcntl F_SETFL, FASYNC"); 
     exit(1); 
}

Now, when data comes in on file descriptor s, your function io_handler() is called and you can read the data.

Here's a sample client that uses SIGIO to get the server returns.

What's My IP Address?

Here's how a program can determine the IP address of the machine it's running on


Last modified: Fri Oct 9 12:12:02 MDT 2009