Archive

Archive for January, 2009

Berkley Sockets

January 27th, 2009 Chris 1 comment

Quite a long time ago, I attempted to figure out how to use the Berkley Sockets API. I failed miserably [for reasons out of the scope of this post]. Recently, I decided to man up and own them. It turns out, they’re not too hard. In fact, they’re just as easy as (if a little more tedious than) any other C API. For a basic TCP/IP socket server or client, there are just a handful of functions – that’s it! This post is aimed to be a kick-start sockets programming tutorial – kind of a middle man between the Unix man pages and some of the garbage examples online (and I hope this doesn’t become one of them). I’ll cover all of the most basic code required to get a simple socket server (or client) up and running.

To create a socket server, you’ll need to create a socket with socket(), and bind() that socket to a specific port. You tell the socket to listen() for incoming connection attempts. You then accept() said attempts. From here on out, you are free to send() and recv() (that’s “receive”) data, over the given socket to the client to your hearts content. When you’re done with a given client socket, close() it. When you’re done with the socket server, close() any remaining open client sockets, and then close() the server’s socket.

To create a client application, you create a new socket with socket(). connect() the socket to the desired host, and then you can send() and recv() just as above. When you’re done, be sure to close() the socket.

What follows is a more detailed (however vastly incomplete) description of the previously mentioned functions, and I’ll keep very basic example socket code going throughout so you can follow along.

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

The socket() function is rather simple, in and of itself. It requests that the system create a socket and return the file descriptor. (Note: All things in Unix-like systems are files. Directories are files. Devices are files. Files are files. Streams are files. And yes… even sockets are files. From the application-programmer perspective, file descriptors are nothing more than integer values.) On success, socket() returns the file descriptor (an integer) of the newly created socket. On failure, -1 is returned and errno is set.

Parameters:

  1. int domain: In plain English, this is the type of socket to be created. The common value would be either AF_LOCAL (or AF_UNIX) for local communication, or for network communication, AF_INET and AF_INET6 for IPv4 and IPv6 internet protocols, respectively.
  2. int type: Specifies the communication semantics. SOCK_STREAM “provides sequenced, reliable, two-way, connection-based byte streams” (taken right from the Unix man page). Save the masochists, researchers, and uber-advanced, this is the only value you’ll ever use.
  3. int protocol: For a ‘type’ value of SOCK_STREAM, this can be 0 (zero).
int sockfd = 0;
 
/* Create a new socket and return it's file descriptor */
if ( ( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) {
  fprintf( stderr, "socket(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

int bind( int sockfd, const struct sockaddr* addr, socklen_t addrlen );

The bind() function (so aptly named) will bind the specified socket to the given port. It is the problem child of the family only because the second parameter takes some working. On success, bind() returns 0 (zero). On failure, -1 is returned and errno is set. The example assumes you have created a socket.

Parameters:

  1. int sockfd: The file descriptor of the socket to bind.
  2. sockaddr* addr: The address data structure to which we want to bind the socket. The address data structure is not altered.
  3. socklen_t addrlen: The length of the address data structure .
unsigned int port = 4000;
sockaddr_in address;
socklen_t address_length = sizeof(address);
 
/* Always initialize your data */
memset( &address, 0, address_length );
 
/* Set up the address structure */
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(port);
 
/* Bind our socket to a port */
if ( bind( sockfd, &address, address_length ) < 0 ) {
  fprintf( stderr, "bind(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

int listen( int sockfd, int backlog );

The listen() function causes a bound socket to begin queueing connection attempts to the socket. On success, listen() will return 0 (zero). On failure, listen() returns -1 and errno is set. The example assumes you have a bound socket.

Parameters:

  1. int sockfd: The file descriptor of the socket to begin listening.
  2. int backlog: The maximum queue size for connected sockets waiting to be accept()ed. This number can be small; and if you do set it higher than the system allows, it will be truncated automatically.
int backlog = 10;
/* Tell the socket to start listening for connections */
if ( listen( sockfd, backlog ) < 0 ) {
  fprintf( stderr, "listen(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

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

The accept() function will attempt to create a new server-side socket for a client-side socket which is awaiting connection. On success, accept() returns the file descriptor of the new socket. On failure, accept() returns -1 and errno is set. The example assumes you have a listening socket.

Parameters:

  1. int sockfd: The file descriptor of the server’s socket.
  2. sockaddr* addr: A new address data structure, to be filled with client information.
  3. socklen_t* addrlen: The length of the client address data structure.
int client_sockfd = 0;
sockaddr_in client_address;
socklen_t client_address_length = sizeof(address);
 
/* Always initialize your data */
memset( &address, 0, client_address_length );
 
/* Accept an incoming connection request */
if ( ( client_sockfd = accept( sockfd, &client_address, &client_address_length ) ) < 0 ) {
  fprintf( stderr, "accept(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

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

The connect() function will attempt to establish a link between the socket specified in sockfd and the host server specified by serv_addr. On success, connect() returns 0 (zero). On failure, connect() returns -1, and errno is set. The example assumes you have created a socket.

Parameters:

  1. int sockfd: The file descriptor of the socket which is to be used in the connection.
  2. sockaddr* serv_addr: An address data structure, holding information about the host.
  3. socklen_t addrlen: The length of the host address data structure.
struct addrinfo hints;
struct addrinfo* result;
int gai_error = 0;
 
/* Always initialize your data */
memset( &hints, 0, sizeof(hints) );
 
/* Setup and retrieve host information */
hints.ai_family = AF_INET; // We're just considering IPv4 here
hints.ai_socktype = SOCK_STREAM;
 
if ( ( gai_error = getaddrinfo( "sub.domain.tld", "7500", &hints, &result ) ) < 0 ) {
  fprintf( stderr, "getaddrinfo(): %snn", gai_strerror( gai_error ) );
  exit( EXIT_FAILURE );
}
 
/* Connect to the target host */
if ( connect( sockfd, result->ai_addr, result->ai_addrlen ) ) < 0 ) {
  fprintf( stderr, "connect(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

ssize_t send( int s, const void* buf, size_t len, int flags );

The send() function attempts to transmit the data in buf to the client socket named by s. On success, send() will return the number of characters sent. On failure, send() returns -1 and errno is set. The example assumes you have accepted a client socket.

Parameters:

  1. int s: The file descriptor of the client socket.
  2. void* buf: A pointer to the string of data to be sent. The string is not altered. (Must be cast to a void* – GCC does this casting implicitly)
  3. size_t len: The length of the data in buf.
  4. int flags: Any optional flags to set (multiple flags set using bitwise or).
#define MAX_BUFFER  1024
char message[MAX_BUFFER];
size_t message_length = 0;
 
/* Set up our message */
sprintf( message, "Hello, socket!n" );
message_length = strlen( message );
 
/* Send our message */
if ( send( client_sockfd, message, message_length, 0 ) < 0 ) {
  fprintf( stderr, "send(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

ssize_t recv( int s, void* buf, size_t len, int flags );

The recv() function grabs sent data from a client socket and stashes it in a local variable. On success, recv() returns the number of bytes received. On failure, recv() returns -1 and errno is set. The example assumes either an accepted socket (for server-side) or a successfully connected socket (for the cilent-side).

Parameters:

  1. int s: The file descriptor of the client socket (on the server-side) or the server socket (on the client-side).
  2. void* buf: The local variable in which to store received data.
  3. size_t len: The maximum number of bytes to store in buf.
  4. int flags: Any optional flags to set (multiple flags set using bitwise or).
#define MAX_BUFFER  1024
char message[MAX_BUFFER];
size_t message_length = 0;
 
/* Always initialize data */
memset( message, 0, MAX_BUFFER );
 
/* Receive data from the remote socket */
if ( recv( client_sockfd, message, MAX_BUFFER, 0 ) < 0 ) {
  fprintf( stderr, "recv(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

int close( int fd );

The close() function is used to close a socket connection. Every call to socket() and every call to accept() should have a matching call to close(). On success, close() returns 0 (zero). On failure, close() returns -1 and errno is set. The example assumes you have an open socket connection, either via socket() or accept().

Parameters:

  1. int fd: The file descriptor of the socket to close.
/* Close out the client socket connection */
if ( close( client_sockfd ) < 0 ) {
  fprintf( stderr, "close(): %snn", strerror( errno ) );
  exit( EXIT_FAILURE );
}

Well, that’s about all there is to it. Of course you can easily ask for more advanced and esoteric functionality from the API, you can use TCP or UDP, you can use IPv4 or IPv6, and a whole managerie of other options. Available also are more comprehensive utilities for address manipulation. See your friendly local *nix man page for the full story on anything described above, if you should find yourself rising above my measly examples. (And if you don’t have access to any *nix hosts for the manpages, then “man ” or “man (section) ” in Google will get you where you need to go as well.)

As a final word, I’m in the last development (read: testing) phases of an object oriented wrapper (that’s in C++) for the Berkley Sockets API. I’ll post it when it’s ready.

Categories: Uncategorized Tags: