TCP/IP Echo Server

This tutorial implements a ServerSocketActor-based TCP/IP “echo” service, and a matching client. Documentation on socket support is available.. A larger TCP/IP chat server example can be found in the Actors-Demos package in class ChatRoom.

Socket support is experimental.

The server

We will need two classes for the server: one for the connection-accepting part, and one for each connected session.

EchoServiceActor

The EchoServiceActor has a single instance variable, sock, which holds an ActorProxy for a ServerSocketActor.

ActorBehavior subclass: #EchoServiceActor
    instanceVariableNames: 'sock'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

Initialization of the behavior object involves two steps. First, we create the listening socket actor. Second, we issue it some credit, allowing it to begin sending us new connections. Each connection we accept depletes its credit by one unit; we will need to replenish it.

initialize
    super initialize.
    sock := ServerSocketActor listenOnPort: 4096.
    sock issueCredit.

Readiness notification

The first event we are sent is an indication that the server socket is listening for new connections.

tcpServer: aServer ready: aBoolean
    self logAll: {
        'Echo server '.
        aBoolean ifTrue: ['is'] ifFalse: ['is not'].
        ' accepting connections'.
        String cr
    }

Accepting a connection

Second, whenever a new connection arrives, we are notified with a tcpServer:accepted: event.

tcpServer: aServer accepted: aConnection
    | session |
    self logAll: {'Connection accepted: '. aConnection}.
    session := EchoSessionActor spawn.
    session actor monitor: self.
    session connection: aConnection.
    aConnection controllingActor: session.
    sock issueCredit.

Let’s take this a step at a time.

First, we log a message to Transcript about the new connection:

self logAll: {'Connection accepted: '. aConnection}.

Then, we spawn the actor that will be responsible for this connection:

session := EchoSessionActor spawn.

We monitor the new actor, so that we are notified when it terminates:

session actor monitor: self.

We tell it about the socket it is to be using:

session connection: aConnection.

We tell the socket that the new EchoSessionActor is to be its controlling actor:

aConnection controllingActor: session.

Finally, we tell the server socket that we are ready to accept another connection when one arrives:

sock issueCredit.

Handling session termination

Finally, because we called monitor: on the EchoSessionActor, we will be sent an event when it terminates:

peer: aSession monitor: aReference terminatedWith: aReason
    self logAll: {'Session ended: '. aSession}.

EchoSessionActor

The EchoSessionActor has a single instance variable, conn, which holds an ActorProxy for a SocketActor.

ActorBehavior subclass: #EchoSessionActor
    instanceVariableNames: 'conn'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

The actor’s connection is passed to it by its EchoServiceActor with a call to connection:.

connection: aConnection
    conn := aConnection.
    conn delimiterMode: String lf.
    conn issueCredit.

The first thing it does with its new connection is configures it to use linefeed-terminated UTF-8 work units:

conn delimiterMode: String lf.

Then, having declared what kind of work unit it wishes to receive, it sends some receive credit to the connection:

conn issueCredit.

The next line of input to arrive is sent to the actor, consuming one credit unit in the process. Once credit reaches zero, the EchoSessionActor has to issue more credit to allow more data to flow in from the remote peer.

Handling incoming data

Each time a work unit arrives, it is delivered using a tcpSocket:data event.

tcpSocket: aSocket data: line
    self logAll: {'Received line: '. line}.
    conn sendLine: 'You said: ', line.
    conn issueCredit.

All the EchoSessionActor does is repeat the line back to the remote peer, and issue credit to its connection allowing the next line of input to arrive.

Handling disconnection

When the remote peer disconnects, a tcpSocketClosed:reason: event is delivered. Here, we simply terminate the EchoSessionActor.

tcpSocketClosed: aSocket reason: aReason
    self logAll: {'Disconnected: '. aReason}.
    Actor terminateNormally.

Running the service

s := EchoServiceActor spawn.

Now connect to the service on port 4096, perhaps using nc on the command line, or telnet, or the EchoClientActor developed below. You could also use the DemoSocketTerminal from the Actors-Demos package.

When you’ve finished creating and destroying connections, terminate the listening actor:

s actor kill.

This stops the EchoServiceActor, thereby closing the listening socket, but leaves the EchoSessionActors running, allowing them to continue conversing with their remote peers until they disconnect.

The client

For the client, we only need one class, representing a connected session.

As well as using this client, you can also interact with the server using DemoSocketTerminal in the Actors-Demos package.

EchoClientActor

The EchoClientActor has two instance variables: conn, which holds an ActorProxy for a SocketActor; and readers, which holds an OrderedCollection of ActorRequests.

When a line is sent to the server with sendLine:, the call to sendLine: doesn’t return until the reply is received from the server. The request object representing the call is placed in the readers queue. Lines of input are sent to the elements of the queue in order.

ActorBehavior subclass: #EchoClientActor
    instanceVariableNames: 'conn readers'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ActorExamples'

Initialization

We create the new actor, which will have the current actor as its controlling actor, and will automatically be linked to us.

initialize
    super initialize.
    conn := SocketActor connectToHost: 'localhost' port: 4096.
    readers := OrderedCollection new.

Connection establishment

The first event we receive is notification of successful connection.

tcpSocketConnected: aSocket
    conn delimiterMode: String lf.

We configure the new connection by setting it to linefeed-terminated UTF-8 work units, just like we did in EchoSessionActor.

We don’t issue any credit at this point, however: this is done in sendLine:.

Transmitting data

The sendLine: method does three things. It first suspends and stores its caller in the readers queue. Then, sends a line of text to the server. Finally, it issues a unit of credit, allowing the socket to send us the next line of input from the server—the reply.

sendLine: aString
    readers add: Actor caller.
    conn sendLine: aString.
    conn issueCredit.

Receiving data

When a line arrives from the server, we get a tcpSocket:data: event. We remove the first waiting reader (we know there will be one), and finally reply to it with the received line.

tcpSocket: aSocket data: line
    readers removeFirst resolveWith: line.

Closing the connection

The stop method terminates the actor. Because the EchoClientActor is linked to the SocketActor, this automatically causes the latter to disconnect and terminate.

stop
    Actor terminateNormally.

Example session

Start an EchoServiceActor, as described above.

Then, create a new client:

c := EchoClientActor spawn.

Sending a line results, ultimately, in a reply:

(c sendLine: 'hello') wait. "'You said: hello'"

Finally, stopping the EchoClientActor disconnects the connection:

c stop.