SocketIO Library Documentation

The purpose of the SocketIO Library (sio for short) is to provide a safe and consistent wrapper interface to a BSD Sockets implementation.  The library also takes steps to protect against a few common pitfalls that plague poorly written BSD Sockets programs.  Specifically, it was designed to do the following: The library was written by Mike Gleason. The first incarnation dates back to some time in 1992. The library may be used and distributed freely, as long as you give due credit.

Introduction

The reader is assumed to be familiar with BSD Sockets.  An excellent source of information is W. Richard Stevens' UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI, Prentice Hall, 1998, ISBN 0-13-490012-X.
 

Connection modes

A communications exchange between two end-points can be connection-oriented or connectionless.  An connection-oriented exchange is characterized by a connection establishment, a series of messages, and a disconnection.  A connectionless exchange is characterized by one or more independent messages.  An analogy to a connection-oriented exchange would be a telephone call where one party establishes a connection to another, and a conversation ensues.  A connectionless exchange analogy would be a letter sent via the postal service -- even if a writer sends two letters to the same recipient, each letter is transported independently.
 

Message modes

For the sio library, all connection-oriented exchanges are data streams using the TCP/IP protocol.  After connection establishment, the conversation consists of one long sequence of data bytes, which is known as a stream.  Therefore there is no concept of a record boundary associated with a stream connection at the transport level, although often there is a record associated over the stream at the application level.  The important thing here is to realize that there is no flow control associated with the stream, so although the data bytes are guaranteed to arrive in order, there is no guarantee that messages will be read in the same size blocks as they were originally written.

For connectionless exchanges, sio uses the UDP/IP protocol's datagram messages.  There is an implicit definition of a record boundary, because each message is treated as a record. So, three writes results in three separate messages.

For example, let's say a sender writes three 50-byte messages.  If a receiver does a 20-byte read followed by 100-byte read followed by a 50-byte read, the receiver would get the first 20 bytes of the first message, 50 of 50 bytes of the second message, and 50 of 50 bytes of the third message.  It's important to understand that in the first read that since UDP datagrams are message oriented that the remaining 30 bytes of the first message are lost, and that although the second read is for 100 bytes, the read immediately returns as finished even though only 50 bytes were actually read.

With UDP datagrams, there are also other important implications that affect message ordering, duplication, and overall reliability.
 

Creating and disposing sockets

A socket is an I/O descriptor that is associated with one side of a communications exchange.  A socket must be either a client or a server, although after communications is established the difference is arbitrary.  A server socket is one that waits for contact to be initiated by a client on a mutually agreed upon address and port number.  A client socket initiates the first communication by sending to an existing server socket.  Client and server sockets may of course be on different machines, and different networks altogether.

A stream server socket is created using SNewStreamServer(), which returns a socket file descriptor ready to accept new client connections.  After a socket descriptor is obtained, your server program can then use SAccept() to establish a connection with a client.

A stream client socket is created using SNewStreamClient().  After a socket descriptor is obtained, your client program can use SConnectByName() or SConnect() to initiate a connection to a server.

A datagram server socket is created using SNewDatagramServer().  After a socket descriptor is obtained, it is ready to receive messages from clients using SRecvfrom() and reply back with SSendto().

A datagram client socket is created using SNewDatagramClient().  After a socket descriptor is obtained, it is ready to communicate with servers using SSendto() and SRecvfrom().

All socket descriptors are disposed of with SClose() when communication is finished.
 

Socket I/O

A stream connection uses SRead() to receive data from the stream and SWrite() to send data.  A datagram socket should use SRecvfrom() to read a message and SSendtoByName() or SSendto() to send a message.  Since each datagram communication is independent, these routines use an address specifier along with each call.
 

Installation

First, unpack the archive. If the package arrived as a '.tar.gz' or '.tgz' file, you need to run gunzip, and then tar to extract the source files. You can do that in one shot by doing "gunzip -c sio.tgz | tar xvf -". If the package you have is a '.tar' file you can use "tar xvf sio.tar". If the package you have is a '.zip' file you can use "unzip sio.zip" or "pkunzip sio.zip".

Now go to the "sio" directory you just made. There is a script you must run which will checks your system for certain features, so that the library can be compiled on a variety of UNIX systems. Run this script by typing "sh ./configure" in that directory. After that, you can look at the Makefile it made if you like, and then you run "make" to create the "libsio.a" library file.

Finally, install the library and headers. You can manually copy the files, or you can run "make install" to copy the files for you. If you choose to "make install" you may want to edit the Makefile if you do not want to install to the /usr/local tree.
 

Function Reference

AddrStrToAddr
int AddrStrToAddr(const char * const s, struct sockaddr_in * const sa, const int defaultPort);

This takes a textual internet address specification and converts it to a struct sockaddr_in.  An address string may be any of the following forms:

Additionally, the hostname may be a name or an IP address string (i.e. 192.168.1.13).

If the string contains a hostname but a port number does not appear to be present and the defaultPort parameter is greater than zero, then the address structure will use defaultPort as the port number.

The function returns a negative number if the string could not be converted, such as the case where the hostname was not found in the name service. Name service is not used unless there is a name instead of an IP address, so if you don't want to use name service, only use IP addresses.

Example:

AddrToAddrStr
char *AddrToAddrStr(char *const dst, size_t dsize, struct sockaddr_in * const saddrp, int dns, const char *fmt);

This function takes a struct sockaddr_in and converts into a readable internet textual address.

The dns parameter specifies if name service should be used to lookup the symbolic hostname from the raw address.  If zero, it expresses the raw IP address in the standard dotted-decimal format, like 192.168.1.13.

The fmt parameter is a magic cookie string, with the following cookies:

This lets you print the address in just about any way you like.  The dst parameter is the string to write the results to, and is always null-terminated.  The dst parameter is also returned as the result of the function.

Example:

DisposeSReadlineInfo
void DisposeSReadlineInfo(SReadlineInfo *srl);

This function is used to dispose of a SReadlineInfo structure that was created using InitSReadlineInfo().  You're required to use this to free dynamically allocated buffers it creates unless you specified that you wanted to use your own buffer, in which case it's optional (but recommended for clarity).

FlushSReadlineInfo
void FlushSReadlineInfo(SReadlineInfo *srl);

This rarely used function is used to reset and clear the buffer used by SReadline().  It acts similarly to a fflush(stdin).

GetSocketBufSize
int GetSocketBufSize(int sockfd, size_t *const rsize, size_t *const ssize);

This utility routine returns the size of the socket's internal buffers, as maintained by the kernel (or socket library).  It does this by calling getsockopt() with the SO_RCVBUF and SO_SNDBUF options, if they are defined.  In the event they aren't defined, it returns a negative result code.

Example:

GetSocketLinger
int GetSocketLinger(const int fd, int *const lingertime);

This utility routine returns whether linger mode has been turned on, and also sets the amount of time in the lingertime parameter.

Example:

GetSocketNagleAlgorithm
int GetSocketNagleAlgorithm(const int fd);

This utility routine returns whether the Nagle Algorithm is in effect (TCP_NODELAY mode not on).

InitSReadlineInfo
int InitSReadlineInfo(SReadlineInfo *srl, int sockfd, char *buf, size_t bsize, int tlen);

This function is used to prepare a SReadlineInfo structure for use with the SReadline() function.  The sockfd parameter specifies the socket that will be used for line buffering.  The sio library does not open or close this socket.

The buf parameter is the area of memory to use for buffering; it should be large enough to hold several lines of data.  If buf is NULL, the function will malloc() one of bsize bytes, otherwise it is assumed that buf is maintained by you and is of size bsize bytes.  If you let sio malloc() this buffer, you must also use DisposeSReadlineInfo() to free it when you're finished with it.

The tlen parameter is the timeout value (in seconds) to use for each call of SReadline().

Example:

SAccept
int SAccept(int sfd, struct sockaddr_in *const addr, int tlen);

This is does an accept(), with a timeout of tlen seconds (tlen may be zero to mean block indefinitely).  If no new connection is accepted, kTimeoutErr is returned, otherwise a new socket or (-1) is returned.  The socket is still usable if a timeout occurred, so you can call SAccept() again.

SBind
int SBind(int sockfd, const int port, const int nTries, const int reuseFlag);

This calls bind(), to the wildcard address with the specified port (not in network byte order).  The nTries parameter tells how many attempts it tries before giving up (which is useful if other processes are grabbing the same port number).  If the reuseFlag parameter is non-zero, SBind() tries to turn on the SO_REUSEADDR (and SO_REUSEPORT, if available) socket options before binding.

Normally you will not call this function directly, since SNewStreamServer() and SNewDatagramServer() do this for you.

SClose
int SClose(int sfd, int tlen);

This is close() with a timeout of tlen seconds.  Normally you don't need to worry about close() blocking, but if you have turned on linger mode, close() could block.  SClose() calls close() for up to tlen seconds, and if the timeout expires, it calls shutdown() on the socket.

SConnect
int SConnect(int sfd, const struct sockaddr_in *const addr, int tlen);

This is connect() with a timeout of tlen seconds (tlen may be zero to mean block indefinitely).  If it returns (-1) or kTimeoutErr, the socket is no longer usable and must be closed.

SConnectByName
int SConnectByName(int sfd, const char * const addrStr, const int tlen);

This is connect() with a timeout of tlen seconds (tlen may be zero to mean block indefinitely).  If it returns (-1) or kTimeoutErr, the socket is no longer usable and must be closed. The difference between SConnect() is that this function takes a textual address string instead of a struct sockaddr_in.

Example:

SListen
int SListen(int sfd, int backlog);

This isn't too useful at present, since it just does listen(sfd, backlog).  And, you will not call this function directly, since SNewStreamServer() and SNewDatagramServer() do this for you.

SNewDatagramClient
int SNewDatagramClient(void);

This returns a new datagram socket, which is ready to send (and then receive) datagrams.  This function is just socket(AF_INET, SOCK_DGRAM, 0). If successful, it returns a non-negative socket descriptor.

Example:

SNewDatagramServer
int SNewDatagramServer(const int port, const int nTries, const int reuseFlag);

This function creates a new socket and binds it to the specified port (not in network byte order) on the wildcard address.  The nTries and reuseFlag are used when it calls SBind(). If successful, it returns a non-negative socket descriptor.

Example:

SNewStreamClient
int SNewStreamClient(void);

This returns a new stream socket, which is ready to SConnect() to a server.  This function is just socket(AF_INET, SOCK_STREAM, 0).  If successful, it returns a non-negative socket descriptor.

Example:

SNewStreamServer
int SNewStreamServer(const int port, const int nTries, const int reuseFlag, int listenQueueSize);

This function creates a new socket, binds it to the specified port (not in network byte order) on the wildcard address, and turns it on for listening.  The nTries and reuseFlag are used when it calls SBind(). The listenQueueSize is for the listen() call.  If successful, it returns a non-negative socket descriptor.

Example:

SRead
int SRead(int sfd, char *const buf0, size_t size, int tlen, int retry);

This is read() on a socket descriptor, with a timeout of tlen seconds (tlen must be greater than zero).  Like read(), it can return 0, (-1), or the number of bytes read, but in addition, it can also return kTimeoutErr.

If retry is set to kFullBufferRequired, SRead() does not return until size bytes has been read or EOF is encountered.  This is useful if you expect fixed-length records, and it doesn't do much good until a complete record has been read. However, it is still possible that an EOF is encountered after some bytes have been read, and with retry set to kFullBufferRequired, you get EOF instead of that partial record.  If you set retry to kFullBufferRequiredExceptLast, you get the partial record (and EOF on the next SRead()).  Otherwise, if you should set retry to kFullBufferNotRequired and SRead() will return when there is some data read.

Example:

SReadline
int SReadline(SReadlineInfo *srl, char *const linebuf, size_t linebufsize);

It is often desirable to process data from sockets line by line, however this is cumbersome to do on a socket descriptor.  The SReadline() function allows you to do this, so you can do one call of the function and get back a line of input.  It accomplishes this much the same way the standard library's I/O routines do this, using a buffer.  However, it is usually not possible to determine if the standard library takes the same special I/O measures on socket descriptors that sio does, so using the standard library function fdopen() with a socket descriptor may or may not work the way you want.

SReadline() needs to maintain state information for each call, so a data structure is required.  You must first call InitSReadlineInfo() to initialize a SReadlineInfo structure.  After that, you may call SReadline() repeatedly until it returns 0 to indicate EOF. The function returns the number of characters in the input line, including a newline (however, carriage return characters are omitted).  You must call DisposeSReadlineInfo() if you chose to let InitSReadlineInfo() use malloc() to allocate your buffer.

Example:

SRecv
int SRecv(int sfd, char *const buf0, size_t size, int fl, int tlen, int retry);

This is the corresponding wrapper for recv(), as SRead() is for read().  You will never need this function, unless you need the special receiving flags that recv() gives you.

SRecvfrom
int SRecvfrom(int sfd, char *const buf, size_t size, int fl, struct sockaddr_in *const fromAddr, int tlen);

This is recvfrom() with a timeout of tlen seconds (tlen must be greater than zero).  Like recvfrom(), it can return 0, (-1), or the number of bytes read, but in addition, it can also return kTimeoutErr. Upon a timeout, the socket is still valid for additional I/O.

Example:

SRecvmsg
int SRecvmsg(int sfd, void *const msg, int fl, int tlen);

This is the corresponding wrapper for recvmsg(), as SRead() is for read().

SSend
int SSend(int sfd, char *buf0, size_t size, int fl, int tlen);

This is the corresponding wrapper for send(), as SWrite() is for write().  You will never need this function, unless you need the special receiving flags that send() gives you.

SSendto
int SSendto(int sfd, const char *const buf, size_t size, int fl, const struct sockaddr_in *const toAddr, int tlen);

This is sendto() with a timeout of tlen seconds (tlen must be greater than zero).  Like sendto(), it can return 0, (-1), or the number of bytes sent, but in addition, it can also return kTimeoutErr. Upon a timeout, the socket is still valid for additional I/O.

Since sendto() rarely blocks (only if the outgoing queue is full), you probably do not want to use a timeout or bother with its associated overhead; therefore Sendto() would be a better choice.

SSendtoByName
int SSendtoByName(int sfd, const char *const buf, size_t size, int fl, const char *const toAddrStr, int tlen);

This is SSendto(), only you can use a textual internet address string instead of a struct sockaddr_in.

SWrite
int SWrite(int sfd, const char *const buf0, size_t size, int tlen);

This is write() on a network socket with a timeout of tlen seconds (tlen must be greater than zero).  Like write(), it can return 0, (-1), or the number of bytes sent, but in addition, it can also return kTimeoutErr.

SelectR
int SelectR(SelectSetPtr ssp, SelectSetPtr resultssp);

This does a select() for reading with the file descriptors in the specified SelectSet.  Using a SelectSet ensures that the first argument to select is always correct (the smallest correct value, for speed) and SelectR() does not destroy the original fd_set and timeval (it copies it to resultssp before using select()).

Example:

SelectW
int SelectW(SelectSetPtr ssp, SelectSetPtr resultssp);

This does a select() for writing with the file descriptors in the specified SelectSet.  Using a SelectSet ensures that the first argument to select is always correct (the smallest correct value, for speed) and SelectW() does not destroy the original fd_set and timeval (it copies it to resultssp before using select()).

Example:

SelectSetAdd
void SelectSetAdd(SelectSetPtr const ssp, const int fd);

This adds a descriptor to the set to select on.

Example:

SelectSetInit
void SelectSetInit(SelectSetPtr const ssp, const double timeout);

Before adding members to the SelectSet structure, it must be initialized with this function.  The timeout parameter initializes the timeout to use for future Selects.

SelectSetRemove
void SelectSetRemove(SelectSetPtr const ssp, const int fd);

This removes a descriptor from the set to select on.  You will need to do this just before you close a descriptor that was in the set.

Sendto
int Sendto(int sfd, const char *const buf, size_t size, const struct sockaddr_in *const toAddr);

This is a simple wrapper for sendto(), which only handles EINTR for you.  It does not worry about SIGPIPEs.

SendtoByName
int SendtoByName(int sfd, const char *const buf, size_t size, const char *const toAddrStr);

This is a simple wrapper for sendto(), which only handles EINTR for you.  In addition, you can use a textual internet address string instead of a struct sockaddr_in.

Example:

SetSocketBufSize
int SetSocketBufSize(int sockfd, size_t rsize, size_t ssize);

This utility routine changes the size of the socket's internal buffers, as maintained by the kernel (or socket library).  It does this by calling setsockopt() with the SO_RCVBUF and SO_SNDBUF options, if they are defined.  In the event they aren't defined, it returns a negative result code.  The operation is only performed if the size is greater than zero, so if you only wanted to change the receive buffer you could set rsize to greater than zero and ssize to 0.

SetSocketLinger
int SetSocketLinger(const int fd, const int l_onoff, const int l_linger);

This is an interface to the SO_LINGER socket option.  The l_onoff parameter is a boolean specifying whether linger is on, and the l_linger parameter specifies the length of the linger time if enabled.

Example:

SetSocketNagleAlgorithm
int SetSocketNagleAlgorithm(const int fd, const int onoff);

This utility routine enables or disables the Nagle Algorithm (TCP_NODELAY mode is off or on).  Generally you won't care about this, unless you're writing an interactive application like telnet, talk, or rlogin, where response time is more important than throughput.

Example:

Sample Code

Here is an example client and server that uses the sio library routines.  The server simply takes data and uppercases any lower case data, and returns the result to the client.  To try it on port number 5123, you could run "ucase_s 5123" in one window, and "ucase_c localhost:5123" in another.
 

Server:

/* ucase_s.c */

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include "sio.h"

static void
ServeOneClient(int sockfd, struct sockaddr_in *cliAddr)
{
	char buf[32], cliAddrStr[64];
	int nread, nwrote, i;

	printf("subserver[%d]: started, connected to %s.\n", (int) getpid(),
		AddrToAddrStr(cliAddrStr, sizeof(cliAddrStr), cliAddr, 1, "<%h:%p>")
	);
	for (;;) {
		nread = SRead(sockfd, buf, sizeof(buf), 15, kFullBufferNotRequired);
		if (nread == 0) {
			break;
		} else if (nread < 0) {
			fprintf(stderr, "subserver[%d]: read error: %s\n",
				(int) getpid(), strerror(errno));
			break;
		}
		for (i=0; i<nread; i++)
			if (islower(buf[i]))
				buf[i] = toupper(buf[i]);
		nwrote = SWrite(sockfd, buf, nread, 15);
		if (nwrote < 0) {
			fprintf(stderr, "subserver[%d]: write error: %s\n",
				(int) getpid(), strerror(errno));
			break;
		}
	}
	(void) SClose(sockfd, 10);
	printf("subserver[%d]: done.\n", (int) getpid());
	exit(0);
}	/* ServeOneClient */


static void
Server(int port)
{
	int sockfd, newsockfd;
	struct sockaddr_in cliAddr;
	int pid;

	sockfd = SNewStreamServer(port, 3, kReUseAddrYes, 3);
	if (sockfd < 0) {
		perror("Server setup failed");
		exit(1);
	}

	printf("server[%d]: started.\n", (int) getpid());
	for(;;) {
		while (waitpid(-1, NULL, WNOHANG) > 0) ;
		newsockfd = SAccept(sockfd, &cliAddr, 5);
		if (newsockfd < 0) {
			if (newsockfd == kTimeoutErr)
				printf("server[%d]: idle\n", (int) getpid());
			else
				fprintf(stderr, "server[%d]: accept error: %s\n",
					(int) getpid(), strerror(errno));
		} else if ((pid = fork()) < 0) {
			fprintf(stderr, "server[%d]: fork error: %s\n",
				(int) getpid(), strerror(errno));
			exit(1);
		} else if (pid == 0) {
			ServeOneClient(newsockfd, &cliAddr);
			exit(0);
		} else {
			/* Parent doesn't need it now. */
			(void) close(newsockfd);
		}
	}
}	/* Server */


void
main(int argc, char **argv)
{
	int port;

	if (argc < 2) {
		fprintf(stderr, "Usage: %s <port>\n", argv[0]);
		exit(2);
	}
	port = atoi(argv[1]);
	Server(port);
	exit(0);
}	/* main */

Client:

/* ucase_c.c */

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#include "sio.h"

static void
Client(char *serverAddrStr)
{
	char buf[256];
	int nread, nwrote, sockfd;

	sockfd = SNewStreamClient();
	if (sockfd < 0) {
		fprintf(stderr, "client[%d]: socket error: %s\n",
			(int) getpid(), strerror(errno));
		exit(1);
	}

	if (SConnectByName(sockfd, serverAddrStr, 15) < 0) {
		fprintf(stderr, "client[%d]: could not connect to <%s>: %s\n",
			(int) getpid(), serverAddrStr, strerror(errno));
		exit(1);
	}

	printf("client[%d]: connected to <%s>.\n", (int) getpid(), serverAddrStr);
	for (buf[sizeof(buf) - 1] = '\0';;) {
		printf("client[%d]: Enter message to send -> ", (int) getpid());
		if (fgets(buf, sizeof(buf) - 1, stdin) == NULL)
			break;
		buf[strlen(buf) - 1] = '\0';	/* Delete newline. */
		if (buf[0] == '\0')
			continue;		/* Blank line. */

		/* Send the request line to the server. */
		nwrote = SWrite(sockfd, buf, strlen(buf), 15);
		if (nwrote < 0) {
			fprintf(stderr, "client[%d]: write error: %s\n",
				(int) getpid(), strerror(errno));
			break;
		}

		/* Wait for complete reply line */
		nread = SRead(sockfd, buf, nwrote, 15, kFullBufferRequired);
		if (nread == 0) {
			fprintf(stderr, "client[%d]: no reply received (EOF).\n",
				(int) getpid());
			break;
		} else if (nread < 0) {
			fprintf(stderr, "client[%d]: read error: %s\n",
				(int) getpid(), strerror(errno));
			break;
		}
		buf[nread] = '\0';
		fprintf(stdout, "client[%d]: received: %s\n",
			(int) getpid(), buf);
	}
	(void) SClose(sockfd, 10);
	printf("\nclient[%d]: done.\n", (int) getpid());
	exit(0);
}	/* Client */


void
main(int argc, char **argv)
{
	int port;

	if (argc < 2) {
		fprintf(stderr, "Usage: %s <host:port>\n", argv[0]);
		exit(2);
	}
	Client(argv[1]);
	exit(0);
}	/* main */