Obtaining a finer degree of control

Let us now take a more internal look at the workings of EiffelNet. The two examples that follow have the same behavior as the preceding one; since their text is less simple, they are only interesting as an illustration of the lower-level facilities that you may want to use in specific cases. If you are already familiar with socket programming, they will also give you a more precise idea of how EiffelNet encapsulates the basic socket mechanisms.

As before, we have a client and a server class, still called OUR_CLIENT and OUR_SERVER, which are part of two different systems and will run concurrently. The communication uses streams rather than datagrams; the datagram form of communication will be examined in section using datagram sockets .

A client and a server on the same machine

Note: The example classes discussed in this section appear in the subdirectory same_mach of the example directory


First, let us assume that the client and server run on the same machine, so that we will use the UNIX_ versions of the classes (the next example will be multi-machine). The communication protocol is also the same as before: the client sends a list of strings, the server returns it extended.

Here we will create and manipulate sockets directly. For that reason, both classes inherit from the EiffelNet class SOCKET_RESOURCES which introduces a number of constants and other useful socket-related features.

The two sockets must be able to refer to a common address. For a communication within a single machine, as noted, this address is a path name, again /tmp/here for this example. The address will be an argument of the creation procedure used to obtain a socket soc1 on either side:

            create soc1.make_client ("/tmp/here")
            create soc1.make_server ("/tmp/here")
The make_ procedures take care of all the hassles of establishing a socket for a client or a server: creating an address object, setting it to the given path name, binding the socket to the address, and in the client case establishing the connection. For finer control, these procedures are still available: you can create a bare socket by using the basic creation procedure make (rather than the more sophisticated make_client and make_server), then create a separate address object, associate the two, and call the bind and connect procedures.

Because communication is bidirectional, the distinction between client and server is not between who sends and who receives, although here the server only sends messages of acknowledgment. The client is the party that initiates the communication; the server is the party which stands ready to accept the communication. This difference justifies the presence of two creation procedures make_client and make_server as illustrated above. To initiate the communication, the client will execute:

            soc1.connect

To make itself ready for the communication, the server will execute:

            soc1.listen (n)

where n is a positive integer indicating the maximum number of client connection attempts that may be queued. The value 5, used in the example, is typical for n.

When you use the _SERVER classes of the predefined level, as in the earlier example, 5 is indeed the default; you can change the value to a positive integer n through the call set_queued (n) .

Whenever the server needs to exchange objects with one of the clients, it obtains access to the socket through the following sequence:

            soc1.accept
            soc2 := soc1.accepted
                ... Storage and retrieval operations using soc2 (not soc1) ...
            soc2.close

Procedure accept ensures synchronization with the client. When communication is established, accept creates a new socket which will be accessible through attribute accepted, whose value is here assigned to the local entity soc2. To receive objects, the server will use operations of the form introduced earlier ( An overview of EiffelNet, sending and receiving object structures ):

            if attached {SOME_EXPECTED_TYPE} soc2.retrieved as l_temp then
--             soc2.retrieved was attached to an object of the expected type. Now that object is attached to `l_temp'
--             Proceed with normal computation, typically involving calls of the form l_temp.some_feature
            else
--             soc2.retrieved was not attached to an object of the expected type.
            end

applying to soc2, not soc1; this makes soc1 available to accept connections with other clients, a fundamental feature of client-server mechanisms.

The operation soc2.close which terminates the above sequence closes the new socket. In principle this is not necessary, since garbage collection should eventually reclaim the socket object, and the dispose procedure of the corresponding socket class includes a call to close. But the risk exists that you run out of sockets before garbage collection reclaims all currently opened sockets, so it is preferable to include the close calls explicitly.

At the end of the processing it is necessary to close the original socket soc1 but also to unlink it. The feature cleanup from class SOCKET takes care of both closing and unlinking.

Here is the server class based on these principles. The actual processing has been put aside in a procedure process.

class
    OUR_SERVER 
inherit
 
    SOCKET_RESOURCES
 
    STORABLE
 
creation 
    make
 
feature
 
    make
            -- Accept communication with client and exchange messages
        local 
            count: INTEGER
            soc1: detachable UNIX_STREAM_SOCKET
        do 
            create soc1.make_server ("/tmp/here") 
            from 
                soc1listen (5) 
                count := 0
            until 
                count = 3
            loop 
                process (soc1) -- See below
                count := count + 1
            end 
            soc1.cleanup
        rescue 
            if soc1 /= Void then
                soc1.cleanup
            end
        end
 
    process (soc1: UNIX_STREAM_SOCKET)
            -- Receive a message, extend it, and send it back
        do
            soc1.accept
            if attached soc1.accepted as l_soc2 then
                if attached {OUR_MESSAGE} retrieved (l_soc2) as l_our_new_list then 
                    from 
                        l_our_new_list.start
                    until 
                        l_our_new_list.after
                    loop 
                        io.putstring (l_our_new_list.item) 
                        io.new_line
                        l_our_new_list.forth
                    end 
                    l_our_new_list.extend ("%N I'm back.%N") 
                    l_our_new_list.independent_store (l_soc2) 
                end
                soc2.close
            end
        end
 
end

Note that at the end the server should not only closes the original socket soc1 but also unlinks it. It is recommended to have a Rescue clause which, as here, ensures that the socket will be closed and unlinked if the system terminates abnormally before its term. Here now is the client class:

class 
    OUR_CLIENT
 
inherit
 
    SOCKET_RESOURCES
 
creation 
    make
 
feature
 
    make
            -- Establish communication with server, and exchange messages
        local
            soc1: detachable UNIX_STREAM_SOCKET
        do 
            create soc1.make_client ("/tmp/here") 
            soc1.connect
            process (soc1) -- See below
            soc1.cleanup
        rescue 
            if soc1 /= Void then
                soc1.cleanup
            end
        end
 
    process (soc1: UNIX_STREAM_SOCKET)
            -- Build a message to server, receive answer, build
            -- modified message from that answer, and print it.
        local 
            our_list: OUR_MESSAGE
        do 
            create our_list.make
            our_list.extend("This")
            our_list.extend (" is") 
            our_list.extend (" our")
            our_list.extend (" test") 
            our_list.independent_store (soc1) 
            if attached {OUR_MESSAGE} our_list.retrieved (soc1) as l_our_new_list then
                from 
                    l_our_new_list.start
                until 
                    l_our_new_list.after
                loop 
                    io.putstring (l_our_new_list.item) 
                    l_our_new_list.forth
                end 
                io.new_line
            end
        end
 
end

Communication between two different machines

Note: The example classes discussed in this section appear in the subdirectory two_mach of the example directory


Let us now assume that the client and the server will run on two separate machines. Instead of UNIX_ sockets, we must now use sockets of type NETWORK_STREAM_SOCKET.

The available creation procedures are slightly different. The server will be set up so as to listen to clients from any machine; it designates a port, identified by an integer, on which it will listen. The socket creation on the server side is then

            create soc1.make_server_port (2000)

For the client, the creation will specify two elements of information: the port number and the server. The server argument, a string, identifies the machine used as a server; it may be the host name of that machine, for example "serverhost" as used in the example; or it can be the machine's internet address, made of a sequence of numbers separated by periods, such as "127.0.0.1".

The rest of the classes is as before.

class
    OUR_SERVER
 
inherit
 
    SOCKET_RESOURCES
 
    STORABLE
 
creation 
    make
 
feature
 
    soc1: detachable NETWORK_STREAM_SOCKET
 
    make
            -- Accept communication with client and exchange messages.
        do 
            create soc1.make_server_by_port (2000) 
                ... The rest as before...
        end      
 
    process
            ... As before ...
        end
 
end 
 
 
class 
    OUR_CLIENT
 
inherit
 
    SOCKET_RESOURCES
 
creation 
    make
 
feature
 
    soc1: detachable NETWORK_STREAM_SOCKET
 
    make
        do 
            create soc1.make_client_by_port (2000, "serverhost") 
                ... The rest as before ...
        end
 
    process
                ... As before ...
        end
 
end