The calls in libsockio which are used to build a server are built on an internal buffer and state machine called sbuf_t (for details on how it works, see the appendix). The library provides a routine which creates a sockserv_t and a function which runs the state machine for you. The minimum logic required to process i/o is shown in figure 3.
There are only three functions to create, run, and destroy the sockserv_t. A minimal socket server can be implemented just by calling sockserv_create() followed by sockserv_run() in an endless loop. The following example is in the sockio project directory, and is called echoserv:
The sockserv_create() function takes a single string argument that is either the name or number of a TCP port on which to listen. The owner of the program must have permission to use this port. Ports less than 1024 can only be used by the superuser.
The pointer returned by sockserv_create() should be passed back to sockserv_destroy() before your program exits, especially if you use the client_del_hook() feature. Both of these are shown in greater detail in examples below.
The pointer returned by sockserv_create() is passed back to sockserv_run(), along with a timeout value (in hundredths of seconds). The previous example uses the special value SOCKSERV_WAIT, which will wait indefinitely for a new client to connect, or for an existing client to send a new request. To add a sockserv back-door to an existing program that has some kind of polling loop, sockserv_run() can also be called with SOCKSERV_POLL which returns immediately after one round of servicing clients.
Normally, all actions are triggered from within sockserv_run() using the ``client_*_hook()'' functions described below. Each time a client has sent a complete line, your receive hook gets called. Any time it is possible to send a line to a client (which is almost every time sockserv_run() happens usually) your send hook gets called. This means the client must always be the one to initiate a disconnect.
The sockserv_del_client() function is not needed by most typical socket servers, but in some cases, the server may wish to force a client to be disconnected (after some timeout period, for example). If such behavior is desired, the server must assign unique client info pointers to each client, using the client_add_hook() described below. Then the server must pass that same key (cinfo) to sockserv_del_client() at the time it wishes to force that client to be disconnected the next time your program calls sockserv_run().
Prior to version 1.6 of the sockio library, any existing client_del_hook() that might have been set up would get called during sockserv_del_client() itself. Now, this routine only marks the client for deleting during the next call to sockserv_run(). Any client_del_hook() will be called at that time (and no other hooks will get called in the intervening time.)
As a special case, if cinfo is NULL, this function causes all of the clients to be disconnected on the next call to sockserv_run(), without destroying the server itself.
In order to make your server do something useful, a call-back function must be provided in client_recv_hook(). This function returns void, and has an arbitrary pointer as the first argument (described later) and a pointer to a buffer. Whenever a client sends in a request, this function will be called, and the client's message will be in the buffer. The call-back function should process the request, write a (null terminated) response back into the same message_in_out buffer, and return. The response will go out to the client immediately, if possible. (But if downstream FIFOs are full, it will go out on a subsequent call to sockserv_run(), but this is completely hidden from the caller.)
Using only a client_recv_hook(), it is possible to change the echoserv example into a server which shares its own environment (**envp) with any number of clients. These clients can all read and write the same environment variables by writing messages to the socket. This example is also in the project directory, and is called envserv:
The example also shows the proper way to call the counterpart to sockserv_create(), sockserv_destroy(). Without this cleanup function, some operating systems may not allow you to start a new server on the same port number for some period of time. To test your server, connect to the port with a telnet client:
> telnet localhost 5253 foo ! no such variable foo=hello 123 . OK foo . hello 123
The server may also wish to implement a close-connection command.
The actual command could be something like ``exit'', ``quit'',
``logout'' but depends on the protocol you decide to implement
in your server. When such a command is received, write the
empty string (set buf to the NUL character,
Normally, the server must write a (non-empty) response if it provides a receive hook. If the response is empty, this condition is treated the same as if an EOF condition had occurred (for example if the client closed the connection.) A common reason for implementing a disconnect command is for convenience during manual connections made with a telnet client.
The program segment in figure 6 would add a ``quit'' command to a server's receive hook:
If the server wishes to send asynchronous messages, it may do so whenever the socket is ready for writing, and there is no partially received message in the buffer. This function will be called almost every time that sockserv_run() happens, so if there is nothing to send, this function should calculate that quickly and efficiently, and just return without writing anything in message_out. If there is a message to go out, it should simply be copied into the buffer. The following figure shows how a client could be notified that something which the server is monitoring for it has changed. This uses a mailbox system, forcing the client to come back with a request to actually read the change. (The actual message could be sent instead, if the client can handle it and keep up.) Figure 7 shows the additional logic needed to add asynchronous notification.
For better throughput, multi-line transfers in the direction from the server to the client may be useful (multi-line transfers in the other direction are not supported.) As the client is ready to accept each line of a multi-line response, the client_send_hook() will be called. For the first line to be processed, the client_recv_hook() can usually set things up and then pass control to the client_send_hook() to get things started.
Typically, multi-line responses will be sent in one ``turn'' of the client, because the underlying network FIFOs are generally large enough to hold several messages. The buffer in libsockio can only hold one message at a time, however, so it is rare but possible that an entire multi-line response will not be sent at once. In this case, there is no difference in the way client_send_hook() gets called, but it should not make any assumptions about the state of anything which might have been changed by intervening transactions with other clients.
Figure 8 shows the most complete usage of sockserv_t, including support for both a mailbox and multi-line reply messages.
Both the mailbox feature and multi-line responses can be implemented by creating a second hook, client_send_hook(), which will be called whenever the server has the opportunity to send something out to the client. This call-back will typically check for previous responses that are still in progress, mailbox flags, time stamps, etc., and if it determines it wants to send anything else to a client, it does so by writing the message to the buffer (exactly the same way as the client_recv_hook() call-back would do).
This is a special version of the send hook for use in implementing protocols that require a server to send binary data responses to a client (binary communications in the other direction, from the client to the server, are not possible.) This hook is used in exactly the same way as the normal line-based send_hook, except that a maximum of SBUF_SIZE (32KB) of binary data can be copied into the data buffer, and a byte count must be specified in bytes. As with the send_hook(), if send_binary_hook() does nothing but return, nothing is sent.
If your server defines both a send_binary_hook() and a send_hook(), the binary hook is always tried first.
An example of a server that uses binary transfers can be found in /cfht/src/medusa/espadons/fli/fliserv.c.
The send_hook is usually not very useful unless you know which client is calling back. So you probably also want make use of the client_add_hook() in this case. Inside this call-back, you may allocate a per-client structure of information. The first piece of information you have about the client is its IP address, passed as four bytes in the first argument of client_add_hook(). Other information about the client can be filled in later, as transactions proceed with this client, since each of those will cause the pointer to your structure to be passed back as the first argument to client_recv_hook() and client_send_hook(). All client_add_hook() must do is allocate the structure and return the pointer. Here is an example of a simple client_add_hook call-back.
The client_add_hook() can also be used to implement some basic security. If you wish to reject all connections except those from specifically allowed IP addresses, the client_add_hook() can look at the IP address and return NULL to let sockserv_run() know that this client should be rejected. Here is how the client_add() function from the above example would look with an IP-address check added:
Whenever an end-of-file or error condition occurs with a client, this clean-up callback can be used to free the cinfo client info allocated by client_add_hook(). An example:
The buffer argument in this callback is not used.