Part 1 of a series of articles on network programming explains the basic concepts of socket programming under UNIX with focus on TCP. The reader is expected to have a good grasp of the C programming language and some basic understanding of the TCP/IP protocol stack.
Concepts
The socket API has been developed for the Berkely Software Distribution (BSD) and has since widely acknowledged as the state-of-the-art multipurpose network API and employed by effectively every UNIX system. When doing networking under UNIX it’s very likely that communication is done via sockets. A socket is uniquely identified by its associated IP address and port number. This way it’s possible to open several connections using the same IP address but different port numbers, each socket serving a completely independent connection.
First of all, sockets must be established (properly setup) before data can be sent to a remote or local host. It has a name, which is called a socket descriptor and an integer used to identify a specific socket among API calls. A socket is also established on the endpoint of your communication, either by explicitly doing so, which is the case for clients, or implicitly when a server is listening for incoming connections. A socket is associated with the following characteristics:
- Address family: identifies what kind of address of a protocol is to be used for this socket
- Socket type: indicates how data is transfered, e.g. as a record based sequence of bytes or as a bytestream, etc.
- Protocol family: specifies what kind of protocol is used, e.g. IP, ISO, etc.
System Calls and Data Structures
All three previously outlined parameters must be supplied as arguments to the socket system call which returns a socket descriptor or -1 on error:
#include <sys/socket.h> int socket(int family, int type, int protocol);
family can be one of the following constants:
| Constant | Address family |
AF_UNIX |
Unix Domain Sockets |
AF_INET, AF_INET6 |
Internet Protocol Version 4 and 6 |
AF_APPLETALK |
Appletalk Protocol |
There are lots of protocol families not listed here but you can find an almost complete list in socket.h. type is a constant indicating the socket type:
SOCK_STREAM- Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM- Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET- Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each read system call.
SOCK_RAW- Provides raw network protocol access; useful for protocol testing.
SOCK_RDM- Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET- Obsolete and should not be used in new programs.
The most important ones are SOCK_STREAM for TCP and SOCK_DGRAM for UDP. Finally there is protocol that specifies which protocol to use for the specified address family and socket type. If only one protocol is available you can safely set it to 0, otherwise use one of the constants below (defined in socket.h:
IPPROTO_TCP- Transmission Control Protocol
IPPROTO_UDP- User Datagram Protocol
IPPROTO_IP- For use with Internet Protocol suite (TCP/IP)
IPPROTO_RAW- For raw socket communication
Be aware that you cannot mix values for family, type and protocol arbitrarily. They must match. For example you cannot specify AF_UNIX and then give SOCK_DGRAM.
Writing a server and a client both involves calling certain functions of the BSD socket API. An important difference is the calling sequence for client and server applications:
Calling sequence for server applications:
socket(): create a suitable socket and obtain socket descriptorbind(): assign socket a name (actually not a name but an IP and a port)listen(): listen for incoming connections to serveaccept(): serve incomming connections from connection queue
Calling sequence for client applications:
socket(): create a suitable socket and obtain socket descriptorconnect(): connect to remote endpoint
As we can see, writing a server applications involves a couple of function calls whereas clients only acquire a socket and call connect().
To open a socket for TCP communication, call socket() as follows:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket()"); /* handle error */ exit(-1); }
The last argument indicates that the default protocol family for AF_INET should be used. It’s equivalent to IPPROTO_TCP
Now we have created a socket, but cannot use it for communication, yet. We must first bind it calling bind():
#include <sys/socket.h> int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
The first argument is the socket descriptor of the socket we want to bind, secondly we provide a valid sockaddr structure (described shortly), and provide the length of the structure.
The sockaddr structure is a generic structure large enough to hold the largest of all address structures (yes, there are more address structures to come). Its primary purpose is to easy conversion of different structures, since at the time of the API design ANSI C was not existent and a void pointer was not known which would have served the purpose. It is defined as follows:
struct sockaddr { unsigned short sa_family; /* address family */ char sa_data[14]; /* 14 bytes of protocol address */ }
Most of the time you’ll have to deal with either sockaddr_in for IPv6 or sockaddr_in6 for IPv6. Both are similar an defined as follows:
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 */ }; struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6. */ in_port_t sin6_port; /* Port number. */ uint32_t sin6_flowinfo; /* traffic class, flow info. */ struct in6_addr sin6_addr; /* IPv6 address. */ uint32_t sin6_scope_id; /* Interfaces for a scope. */ }
A very confusing point about address structures is the embedded in_addr and in6_addr which holds the IP address in network byte order, so you don’t have to convert between byte orders as shown below. The origin of this construct is unknown to the author (please email me or leave a comment if you have details).
Before calling bind and passing the structure you will have to set certain fields. Usually you’ll first clear all fields and then set them appropriately:
sockaddr_in server; memset((char *)&server, 0, sizeof(sockaddr_in)); server.sin_family = AF_INET; server.sin_port = htons(PORT); server.sin_addr.s_addr = INADDR_ANY;
Since byte order may be different on host systems and on networks, it has been agreed upon a default byte order for data that is going to be transfered between endpoints: network byte order (NBO). Accordingly, host byte order (HBO) is the byte order used on the local host and can be either big endian or little endian. All data that is passed through the protocol stack must be converted to NBO as we have seen with the port member of sockaddr_in. Header in.h in the netinet directory provides for some useful function to convert between HBO and NBO:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
At first glance these function names are cryptic but translate as follows: h is for host, n for network and l and s for long (double word, 32 bits) and short (single word, 16 bits) respectively.
Back to bind. The function takes a socketaddr structure but for IP we use either sockaddr_in or sockaddr_in6. We will have to cast the structure when passing it to bind:
if (bind(fd, (struct sockaddr *)&server, sizeof(server)) < 0) { perror("bind()"); exit(-1) }
To put the server into listening mode call listen():
#include <unistd.h> int listen(int sock_descriptor, int backlog);
passing the socket descriptor as the first argument and the length of the connection queue as desired. The maximum length of the queue depends on a system configuration value, but it’s safe to pass a value between 5 to 20.
When new connection requests arrive they are put into a queue of incomplete connections. To serve a connection we have to call accept that fetches the first incomplete connection
#include <sys/socket.h> int accept(int fd, struct sockaddr *addr, socklen_t *addrlen);
First argument is, as usual, the socket descriptor. The next two arguments are filled in by the kernel, all you need to do is send the appropriate data. It is worth noting that accept() returns a new socket descriptor that refers to a completed connection. The descriptor of the incomplete connection is no longer needed and can be closed:
#include <unistd.h> int close(int fd);
When writing a client we do not call bind(), listen() or accept(), but instead connect():
#include <unistd.h> int connect(int fd, const struct sockaddr *address, socklen_t addrlen);
with fd as the socket descriptor, address pointing to a sockaddr structure containing the peer address and addrlen specifying the length of address.
Writing a TCP Client and Server
In the preceding section we have seen which functions to call in order to set up a client and a server. Let’s get our hands dirty and write a small client. It is supposed to send simple commands to a server that returns a response.
Client code
#include <sys/types.h> #include <netinet/in.h> #include <socket.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <netdb.h> char buf[BUFSIZ]; main (int argc, char **argv) { int sock, n; struct sockaddr_in sa; struct hostent *host; if (argc != 3) { printf("Usage: %s n", argv[0]); exit (-1); } if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror("socket"); exit(-1); } if ((host = gethostbyname(argv[1])) == NULL) { perror("gethostbyname"); exit(-1); } memcpy((char*)&sa.sin_addr, (char*)host->h_addr, host->h_length); sa.sin_family = AF_INET; sa.sin_port = htons((u_short)atoi(argv[2])); if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("connect"); exit(-1); } while (strcmp(buf, "quitn") != 0) { while (!((buf[strlen(buf) - 2] == '>') && (buf[strlen(buf) - 1] == '>'))) { if ((n = read(sock, buf, sizeof(buf))) < 0) { perror("read"); exit(-1); } buf[n] = 0; printf("%s", buf); } if (strcmp(buf, "[. in new line terminates]n>>") == 0) { while (buf[0]!='.') { fgets(buf, sizeof(buf), stdin); if (write(sock, buf, strlen(buf))<0) { perror("write"); exit(-1); } } } else { fgets(buf, sizeof(buf), stdin); if (write(sock, buf, strlen(buf))<0) { perror("write"); exit(-1); } } } return (0); }
Server code
#include <sys/types.h> #include <netinet/in.h> #include <socket.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <netdb.h> int main (int argc, char **argv) { struct sockaddr_in sa; struct sockaddr_in caller; int ssd, csd, length, retval, action; char buf[BUFSIZ]; char* prompt[] = { ">>", "Available commands: n tput - to send a text to server thelp - to view this helpn tquit - to close the connectionn>>", "Teminate with '.' in a new line.n>>", "Enter a commandn>>" }; if (argc != 2) { fprintf(stdout, "usage: %s n", argv[0]); exit(-1); } sa.sin_family= AF_INET; sa.sin_addr.s_addr = INADDR_ANY; sa.sin_port= htons((u_short)atoi(argv[1])); if ((ssd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror ("socket"); exit(-1); } if (bind(ssd, (struct sockaddr *)&sa, sizeof sa) < 0) { perror ("bind"); exit(-1); } length = sizeof(sa); listen(ssd, 5); if ((csd = accept(ssd, (struct sockaddr *)&caller, &length)) < 0) { perror ("accept"); exut(-1); } write(csd, prompt[0], strlen(prompt[0])); while (action != 2) { if ((retval = read(csd, buf, sizeof(buf))) < 0) { perror("Reading stream messagen"); exit(-1); } buf[retval] = ''; if (strcmp(buf, "helpn") == 0) action = 0; if (strcmp(buf, "putn") == 0) action = 1; if (strcmp(buf, "quitn") == 0 || retval==0) action = 2; if (strcmp(buf, ".n") == 0) action = 3; switch (action) { case 0: write(csd, prompt[1], strlen(prompt[1])); action = -1; break; case 1: write(csd, prompt[2], strlen(prompt[2])); action = 4; break; case 2: printf("nEnding connection ...n"); break; case 3: write(csd, prompt[3], strlen(prompt[3])); action = -1; break; case 4 : printf("%s", buf); break; default: write(csd, prompt[3], strlen(prompt[3])); } } close(csd); close(ssd); return (0); }
The client works as follows: It connects to the server specified by server name and port at the command line and displays a prompt waiting for input. Entering help returns a simple help message:
% ./tcpsrv1 9000 & % ./tcpcli1 localhost 9000 >> help Available commands: put - to send a text to server help - to view this help quit - to close the connection Teminate with '.' in a new line. Enter a command >> put Hello server! Hello server! >>; quit Ending connection ...
The put command simply causes the server to echo its input. quit ends the connection.
To simplify things we call gethostbyname() to obtain the server’s IP address. Just take a look at the manpage for gethostbyname to find out how it works (it’s very easy to use).
Just as seen in the client code gethostbyname is utilized in the server code. I previously said that a new descriptor is returned by accept. Take a close look at the code and you will see that we pass ssd to accept and assign the new descriptor to csd which is used for communication while connected to the client.
Improving the Server
Our server has one major drawback: only one client can be served at any given time. We need a way to create another server process for every client requesting a connection. The easiest way to accomplish this is to call fork() right after calling accept:
while (1) { pid_t pid; if ((csd = accept(ssd, (struct sockaddr *)&caller, &length)) < 0) { perror ("accept"); exit(-1); } if ((pid = fork()) == 0) { /* child */ /* process client */ } else if (pid < 0) { /* fork() failed */ } close(ssd); }
The code for handling the client is done in the if branch of fork and could be transfered into a separate function to improve readability. When back in parent we close the original socket immediately, since it’s not needed anymore, because all communication is handled by the child process. By enclosing accept and fork in a while loop that never breaks we are able to server multiple clients. This is possible because accept blocks until another connection is ready to be accepted, that is after completing the three-way-handshake.
The new server code
#include <sys/types.h> #include <netinet/in.h> #include <socket.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <netdb.h> char buf[BUFSIZ]; char* prompt[] = { ">>", "Available commands: \n \tput - to send a text to server\n \thelp - to view this help\n \tquit - to close the connection\n", "Teminate with '.' in a new line. >>", "Enter a command\n>>" }; int retval; int action; main (int argc, char **argv) { struct sockaddr_in sa; struct sockaddr_in caller; int ssd, csd, length; if (argc != 2) { fprintf(stdout, "usage: %s\n", argv[0]); exit(-1); } sa.sin_family= AF_INET; sa.sin_addr.s_addr = INADDR_ANY; sa.sin_port= htons((u_short)atoi(argv[1])); if ((ssd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { perror ("socket"); exit(-1); } if (bind(ssd, (struct sockaddr *)&sa, sizeof sa) < 0) { perror ("bind"); exit(-1); } length = sizeof(sa); listen(ssd, 5); while (1) { pid_t pid; if ((csd = accept(ssd, (struct sockaddr *)&caller, &length)) < 0) { perror ("accept"); exit(-1); } if ((pid = fork()) == 0) /* child */ process_client(csd); else if (pid < 0) perror("fork"); close(ssd); } } void process_client(int fd) { write(fd, prompt[0], strlen(prompt[0])); while (action != 2) { if ((retval = read(fd, buf, sizeof(buf))) < 0) { perror("Reading stream messagen"); exit(-1); } buf[retval] = ''; if (strcmp(buf, "help\n") == 0) action = 0; if (strcmp(buf, "put\n") == 0) action = 1; if (strcmp(buf, "quit\n") == 0 || retval==0) action = 2; if (strcmp(buf, ".n") == 0) action = 3; switch (action) { case 0: write(fd, prompt[1], strlen(prompt[1])); action = -1; break; case 1: write(fd, prompt[2], strlen(prompt[2])); action = 4; break; case 2: printf("nEnding connection ...\n"); break; case 3: write(fd, prompt[3], strlen(prompt[3])); action = -1; break; case 4 : printf("%s", buf); break; default: write(fd, prompt[3], strlen(prompt[3])); } } close(ssd); }
Client handling has been put into its own function and the needed variables have been made global (you may want to make these static to limit their scope). Everytime a new connection is ready to be accepted a new child process is forked to serve the request. process_client() is called by the child and it vanishes when the quit command is given thereby breaking the while loop closing the socket descriptor of this connection.
What’s next?
Next week I’ll show, how to write a multi-threaded server that does not fork to serve a client but creates a new thread which is quite different from the approach we have just seen. Additionally we will see how to employ the send and recv system calls which can be used with connectionless UDP and connection-oriented TCP. Thanks for reading.
graegerts