An Overview of The Eiffel Sockets Library

Core Concepts

A rather poor quality diagram of some major classes

Presently, much of what will be ESockets is not yet complete. Yet we can still focus on its key features here. The framework can be divided into the following clusters (although the literal division into subdirectories has not occurred):

  • Hardware Sockets
  • Utility Classes
  • Multiplexers and the Multiplexed
The Hardware Socket classes are pretty much what one would expect: a basic wrapping of the functionality of TCP/IP sockets. There is a stream socket abstraction along with a corresponding server socket. Non-blocking is directly supported, as it is critical for other parts of the library.
To Do: Other fine-tuning of sockets should probably be provided in utility classes, or perhaps I'll provide mixins into the existing socket classes. Time will tell. UDP sockets strike me as less important, but lots of folks ask for them, so they will be provided sooner or later. Local domain sockets and other Unix specific things should appear last, and then as optional adjunct classes. Anyhow, there is much work to be done for the hardware classes, but it is all pretty routine.

The utility classes will encompass those classes which support or aid other aspects of the system.

Currently only one utility class is present: ES_LINE_SEPARATOR, which allows you to accumutate streamed data in small (or large) chunks, and then access it one line at a time.

To Do: Currently there are no utility classes in the distribution, but clearly I will have to provide an interface to the DNS framework; I may very well just steal the NETWORK_HOST class from the old ESockets. A class to categorize socket error messages should also be provided.

Regarding the multiplexer framework, it is what is most interesting in ESockets, and what takes the most careful work.

The nature of multiplexing will, I fear, be difficult for some at first, so I shall take some time to explain it here. First off, if you are familiar with event driven programming for a GUI, then you already pretty much understand how to handle multiplexed connections, with the caveat that networked data tends to be streamed and no record boundaries are preserved.

First, I shall describe the problem that multiplexing solves. Unlike I/O with files, I/O through network sockets is unpredictable. That is, for a file, you can pretty much assume that the operating system will respond to your I/O request in a timely manner. With sockets (and also with keyboards and mice) you cannot make this assumption. Perhaps data will arrive from the network soon, perhaps you will wait a long time. Typically, calls to read data from a network are blocking. to see what this means, consider this ESockets call:


socket.receive -- Read data from the network.  
When this command is executed, the process will stop, blocking, until there is some data. This wait could be forever if no data arrives. For some applications this might be acceptable. For instance, for a simple time server that waits in the background and returns a short time string to each connection. Or, the time client, which the user will run from the command line and which can be canceled with ctrl-c if the connection does not complete in a timely manner.

However, this is unacceptable for a server, or a GUI application, where the application will stop responding to mouse or keyboard input while it is blocked waiting for the socket. In some cases a timeout can be applied to the socket, by various means. It might tell the socket to stop waiting after, say, 30 seconds, and to then issue an error message.

But consider a busy server with many connections. It cannot afford to spend any time waiting on one connection that it could spend servicing other connections.

Several traditional solutions exist:

  1. Forking with each connection
  2. Starting a thread for each connection
  3. Preforked and prethreaded variations of the above two
  4. Signal driven I/O
  5. Multiplexing
These are all good methods in their respective application, but for various reasons I choose number 5. The two threading methods are strong contenders, but I avoid them as thread support between Eiffel vendors is uncertain. The forking models are specific to a Unix architecture, and thus unsuitable for a portable framework. Signal driven I/O is very low-level and complex, not appropriate for an application level library.

This leaves multiplexing. In a multiplexed framework, the application can have many sockets open simultaneously, each entered into the multiplexer. You can think of the multiplexer as a dispatcher engine that waits for activity on a socket and then informs the application of the event, either that the socket has data to be read, or that the socket is free to write some further data.

If this sounds similar to the event loop you're used to for GUI programming, you're right. In fact, for X Windows based systems, such as the Eiffel GTK libraries, it is exactly the same dispatcher for your sockets as well as your GUI. For a server, on the other hand, the multiplexer will be contained within the application, but the principle is the same.

Programming with a multiplexer can be hard. Unlike the normal stream metaphor, where you structure your program to read data when you need it, then produce the output; with a multiplexed scheme, you must respond to data when it comes, even in very small or very large pieces. You cannot program your logic to read a line, instead you receive some number of bytes: perhaps a line; perhaps a few characters; perhaps 7 lines, plus three characters from the next. Also, if your application writes a large quantity of data, you will break it into smaller buffered portions (of a few thousand bytes), and send these when the socket is clear -- all while monitoring other sockets to see if they are ready for I/O.

ESockets tries to make this somewhat easier for the programmer. First off, when writing small portions of data, you needn't be concerned with multiplexing the writes, as the library will detect the sockets state and do the right thing automatically. You'll only need to explicitly manage buffering output when writing large bodies of data at once (we call this output driven I/O). Reading data is much like responding to mouse clicks for your GUI. In ESockets you'll redefine special features of the base multiplexed classes, and provide your own response to arriving data.

As a simple example:


   feature -- Callbacks
   
      data_ready (data: STRING) is
            -- Data has arrived.
         do
            from
               line_separator.put (data)
            until
               not line_separator.has_full_lines
            loop
               process_line (line_separator.item)
               safely_put ("Line processed%N")
                                 -- Write to network.
               line_separator.forth
            end
         end
         
      end_of_data is
            -- The data stream is finished.
         do
            if
               line_separator.has_residue
            then
               process_line (line_separator.residue)
            end
            
            clean_up
         end
This illustrates two features in a larger class, the first processes new chunks of data as they arrive, using an object line_separator to, presumably, break up the stream into lines. process_line is likely some feature that performs a request, or some such. See if you can figure out what end_of_data does. Hint: it is called when the stream has no more data to send.

If the example is still confusing, remind yourself this: you never call the above features in your own code. You write the routines, but they are called by the library automatically at the appropriate times.

I hope that this gives a flavor of event driven programming with sockets. If you are still confused, carefully review the ECHO_SERVER example in the distribution.

Below, I shall describe some of the important ESockets classes.

Hardware Sockets

ES_STREAM_SOCKET [Shortform]
  When An ES_STREAM_SOCKET is a connection to another process, perhaps on another computer, through which you can send and receive streams of data. This is contrasted with packet sockets (note: packet sockets are not yet implemented in ESockets) that send individual frames of data to variable hosts. You will create one of these object for each connection you wish to make to a remote host. Also, you will receive a socket of this type from the library when a remote host initiates the connection with your application (see the ES_SERVER_SOCKET below for details of the process).
  Nature ES_STREAM_SOCKET is a normal class.
  Creating You can create an ES_STREAM_SOCKET in the normal way using its make method. If there wasn't an error forming the socket it will not be open but not yet connected to another process. You also acquire ES_STREAM_SOCKET's when another host connects to a waiting ES_SERVER_SOCKET. Such a sockets is not only open, but already connected to the remote host.
  Using An unconnected, but open, ES_STREAM_SOCKET can be connected to another process by calling its connect feature with the address of the remote host and the port number that the remote process is listening to. Once connected, use send_all to send data, and receive to receive. receive will usually block -- unless there is inbound data waiting. send_all can also block if sending heavy loads. error will be true if an error occurs. It is important to check this after each call. When done with the connection call close, at which point the socket is useless. In some cases you will want to tell the remote process that you have no more data to send, but still want to hear its responses. In such a case call send_end_of_data.
  Example


feature read_from_cool_server is
      -- Read data from a cool server!
   local
      socket: ES_STREAM_SOCKET
   do
      !!socket.make
         if socket.error then handle_error end
         
      socket.connect ("207.8.154.2", 80)
         if socket.error then handle_error end
         
      socket.send_all ("GET /index.html%N")

      from
         socket.receive
      until
         socket.end_of_data or socket.error
      loop
         io.put_string (socket.received_data)
         socket.receive
      end

      if socket.error then handle_error end

   end

      
ES_SERVER_SOCKET [Shortform]
  When The above stream sockets can be used alone if you wish to initiate the connection to a remote process. If you want to receive connections, however, you will need a class like ES_SERVER_SOCKET. Such a class listens to a particular port and provides ES_STREAM_SOCKET's, one for each inbound connection received.
  Nature ES_SERVER_SOCKET is a normal class.
  Creating ES_STREAM_SOCKETS are created the standard way, using their make feature.
  Using Once created, you must call the start_listening feature and specify which host address (normally "All" -- meaning all local addresses) and which port (a number between 1 and 65535) to listen on. At this point remote processes can connect to this socket. Call get_next_connection to acquire one inbound connection; this call will block if no connection is waiting. The remote_connection feature will be set to the stream socket associated with the most recently acquired connection. You can copy this reference and use it independently of the server socket. Call close when done.
  Example

feature server is
      -- Make a server then cycle through connections.
   local
      socket: ES_SERVER_SOCKET
   do
      !!socket.make
         if socket.error then handle_error end
      
      socket.start_listening ("All", 5001)
                       -- Listen on each local IP
                       -- address, and on port 5001.
                              
         if socket.error then handle_error end
         
      from
         socket.get_next_connection
      until
         socket.error
      loop
         socket.next_connection.send_all
              ("Thank you for connecting to me.%N")
             -- Send a nice message to the remote host.
             
         socket.next_connection.close
             -- Close them down,
	     -- a very simple server this is!
              
         socket.get_next_connection
      end
   end

      
Utility Classes

None so far.

Multiplexers and the Multiplexed

ES_MULTIPLEXED_SOCKET [Shortform]
  When An ES_MULTIPLEXED_SOCKET is used when you wish to multiplex your I/O connections (see above for a discussion of multiplexing). It does not replace the ES_STREAM_SOCKET, but instead itself uses a stream socket to perform the actual data transfer.
  Nature ES_MULTIPLEXED_SOCKET is a deferred template class. You will inherit from this class to provide specific functionality.
  Creating All instances of this class will actually be of user defined decendants, and the creation methods will vary accordingly. However, this class does have a make routine that you should call from your creation routines to set up certain invariants. That make method requires a connected stream socket, which you must provide.
  Using To use this class, you must define these routines:
  • data_ready
  • end_of_data
  • read_error_notification
  • write_error_notification
The first two define what actions to take when receiving data from the remote host. To send data, if it is a small amount, you may call safely_put. For larger amounts see below. You can control whether to respond to available data by calling set_to_read; by default it is set not to read. Usually the first thing one does with a multiplexed socket is to turn on reading. When done with the connection, call either clean_up or clean_up_when_ready. The latter ensures that any data buffered to be sent goes out before closing. This works even with output-driven I/O (see below).
  Advanced When writing large amounts of data, over a few thousand bytes, you should not use safely_put, but should instead manage the flow of data yourself. This is effected through the four features:
  • set_to_write
  • refill_write_buffer
  • add_to_buffer
  • new_buffer
You use the first to control if the socket will be called on to write (this is turned off whenever the buffer is flushed, so you'll need to reset it). refill_write_buffer is called whenever the buffer has become empty. You should redefine it to generate more data -- at best a few thousand bytes -- whenver it is called. Buffer this data either with add_to_buffer or with new_buffer; the latter just replaces the exising buffer, using the one that you provide -- this avoids some copying. The routines of the parent class will then ensure that the data is sent when possible, then call refill_write_buffer again as needed. After your last data is buffered (not after it is sent!), you may call either clean_up_when_ready or send_end_of_data_when_ready, as you see fit.
  Example


class
   ECHO_SERVER_ECHO_SOCKET_SIMPLE
--
-- Echos a socket's data. This is a simplified version
-- of the example appearing in the distribution.
--

inherit

   ES_MULTIPLEXED_SOCKET
   
creation

   make
      -- Uses the make provided by the parent class.
   
feature -- Callbacks

   data_ready (text: STRING) is
         -- The data text has been read
	 -- and must be processed.
     do
            -- Put the data right back
	    -- from where it came.
         safely_put (text)
      end

   end_of_data is
         -- An end of data has been reached.
      do
            -- Flag to clean up when
	    -- buffers are flushed.
         clean_up_when_ready
      end
      
   read_error_notification,
         write_error_notification is
         -- Called when an error occurs on I/O.
      do
            -- Clean up the connection immediately
         clean_up
      end

end -- class ECHO_SERVER_ECHO_SOCKET_SIMPLE

      
ES_MULTIPLEXED_SERVER_SOCKET [Shortform]
  When In a multiplexed application, server sockets are managed within an ES_MULTIPLEXED_SERVER_SOCKET object. This allows server sockets to coexist with other sockets without blocking.
  Nature ES_MULTIPLEXED_SERVER_SOCKET is a deferred template class. You will inherit from this class to provide specific functionality.
  Creating Since all instances of this class will be of decendant classes, the creation routines will vary. However, there is a feature make defined that takes a listening server socket and sets up certain invariants. You will need to provide this socket.
  Using To use this class, you must define these routines:
  • new_connection
  • error_notification
The first is called whenever a new imbound connection is available. It provides a connected ES_STREAM_SOCKET. Usually this routine will create some decendant of ES_MULTIPLEXED_SOCKET (see above) to handle the connection. Call set_to_receive_connections after this object is first created. When done, call clean_up.
  Example

class
   ECHO_SERVER_LISTENING_SOCKET_SIMPLE
--
-- Listens on a requested port and spawns
-- ECHO_SERVER_ECHO_SOCKET_SIMPLE's.
-- This is a simplified version of the
-- example in the distribution.
--

inherit

   ES_MULTIPLEXED_SERVER_SOCKET
      rename make as parent_make end
   
creation

   make
   
feature -- Overrides

   new_connection (socket: ES_STREAM_SOCKET) is
         -- The new connection socket has arrived
	 -- and must be handled.
      local
         echo_socket: ECHO_SERVER_ECHO_SOCKET_SIMPLE
      do
            -- Take the physical socket and make a
	    -- multiplexed object from it.
         !!echo_socket.make (socket)
         echo_socket.set_to_read (True)
      end
   
   error_notification is
         --  Called when an error occurs on I/O.
      do
         clean_up
      end
      
feature {NONE} -- Creation

   make (port: INTEGER) is
         -- Create a listening socket on port `port'.
      require
         good_port: port > 0 and port <= 65535
      local
         socket: ES_SERVER_SOCKET
         ex: expanded EXCEPTIONS
      do
            -- Make the physical socket.
         !!socket.make
         if
            socket.error
         then
            ex.raise (socket.error_name)
         end
         
            -- Start listening on the selected port.
         socket.start_listening ("All", port)
         if
            socket.error
         then
            socket.close
            ex.raise (socket.error_name)
         end
         
            -- Ensure the other invariants, including
            -- entering Current into the multiplexer.
         parent_make (socket)

      ensure
         in_multiplexer: in_multiplexer
      end

end -- class ECHO_SERVER_LISTENING_SOCKET_SIMPLE
      

To be continued . . .

Copyright 2000 by Jeffrey Straszheim
<stimuli@shadow.net>