|
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:
- Forking with each connection
- Starting a thread for each connection
- Preforked and prethreaded variations of the above two
- Signal driven I/O
- 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.
|
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
|
None so far.
|
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 . . .
|