Sockets
Support for client (connecting) and server (listening) TCP/IP sockets
is modeled on
Erlang’s gen_tcp
library.
Socket support is experimental.
Like Erlang, each connected socket is managed by a separate actor, and
every SocketActor
is linked to a
controlling actor, which receives socket-related events.
Unlike Erlang, credit-based flow control1 is used to manage input from a connected socket, as well as to manage the rate at which new sockets are accepted from a server socket.
Reading from a socket is asynchronous and event-based. Writing to a socket produces a promise which is resolved when the write has been delivered to the socket.
Example
See the TCP/IP Echo Server tutorial for a full client-server example.
In addition, class DemoSocketTerminal
demonstrates combining Socket
actors with actor-based Morphic programming.
Here is a very simple example, a rough HTTP client that retrieves
http://localhost/
. After the headers have been read, the client
switches from double-linefeed-separated work units
to raw chunks of data. At the moment of the change, the read credit
amount is zero, ensuring that there is no risk of accidentally reading
the wrong data in the wrong mode.
ActorProcess boot: [ | s |
s := SocketActor connectToHost: 'localhost' port: 80.
"The new SocketActor takes the current actor as controlling actor."
s lineTerminator: String crlf. "HTTP protocol requires this."
s sendLine: 'GET / HTTP/1.0'.
s sendLine: ''.
s delimiterMode: String crlfcrlf.
"This makes the first work item a string up to the header/body break."
s issueCredit. "Issue one work-unit's credit: this reads the headers."
s rawMode. "Switch to raw mode for the remainder of the connection."
s infiniteCredit. "Retrieve it all."
"NB be more careful about credit, in production use!"
Actor receiveUntil: [:v |
Transcript crlf; nextPutAll: (v printStringLimitedTo: 400); flush.
v message selector = #tcpSocketClosed:reason: ]].
ServerSocketActor
Creating a server socket
ServerSocketActor listenOnHost: interfaceDnsNameString Port: portNumber.
ServerSocketActor listenOnPort: portNumber.
These two methods spawn a fresh ServerSocketActor
that starts
listening on the given port. If an interfaceDnsNameString
is not
given, '0.0.0.0'
is used, accepting connections on any interface.
The new socket takes the calling actor as its controlling actor, and starts off with zero credit for accepting connections.
Readiness events
controllingActorProxy tcpServer: serverSocketActorProxy ready: aBoolean.
The server socket actor persists in the image until explicitly stopped. As circumstances change, such as the image being stopped and restarted, the underlying listening socket may come and go. It will not always be possible to listen on the configured interface and port number.
Therefore, the socket actor sends its controlling actor a readiness
event each time the socket is able to start listening or is forced to
stop listening. The aBoolean
field in the event will be true
when
listening is in progress, and false
when listening is on hold.
If listening could not be established, and credit for accepting new connections is non-zero, then the server socket actor will keep trying to re-establish its listening socket every few seconds.
Accepting connections
To start receiving connections, call ServerSocketActor >>
#issueCredit
each time you want to allow
an additional connection to be
accepted.
For example, you might wish to call issueCredit
once at startup
time, and then once at the top of the code handling each
tcpServer:accepted:
event.
Use #issueCredit: aNumber
to increment the available credit by
aNumber
steps in one go.
When the server has credit and a connection arrives, the controlling
actor will be sent a #tcpServer:accepted:
request:
controllingActorProxy tcpServer: serverSocketActorProxy accepted: socketActorProxy.
The socketActorProxy
refers to a SocketActor
.
Reconfiguring the listener
To alter the interface, port number, or underlying kernel-level listen
backlog size of an already-existing ServerSocketActor
, use the
following methods:
serverSocketActorProxy listenOnHost: aString port: aPortNumber backlogSize: aBacklog.
serverSocketActorProxy listenOnHost: aString port: aPortNumber.
If not supplied, aBacklog
is set to 128.
Shutting down a server socket
serverSocketActorProxy stop.
The ServerSocketActor
will terminate, activating its links as usual.
Remember that the actor links to its controlling actor! You will often
want to unlink before calling stop
:
serverSocketActorProxy actor unlink.
serverSocketActorProxy stop.
SocketActor
Creating a connected socket
There are two ways to create a connected socket: either wait for one to be accepted, or create a new outbound client socket:
SocketActor connectToHost: hostname port: portNumber.
The new socket takes the calling actor as its controlling actor, and starts off with zero credit for receiving data.
Connection events
controllingActorProxy tcpSocketConnected: socketActorProxy.
Issued when a client socket has connected.
Only issued for new, outbound client connections: when a connection is
accepted from a server socket, the tcpServer:accepted:
event takes
the place of this event.
controllingActorProxy tcpSocketClosed: socketActorProxy reason: anExceptionOrNil.
Issued when the socket closes.
Sending data
socketActorProxy send: anObject.
If anObject
is a ByteArray
, sends the raw bytes; otherwise, sends
anObject asString
, encoded into bytes as UTF-8.
socketActorProxy sendAll: aCollection.
Just like aCollection do: [:x | socketActorProxy send: x]
, but more
efficient.
socketActorProxy sendLine: anObject.
Sends anObject
followed by the current line terminator, which
defaults to
String lf
The line terminator can be retrieved and set via
socketActorProxy lineTerminator.
socketActorProxy lineTerminator: aString.
Receiving data
Data is read from the socket in work units, which can be either
- delimiter-separated spans of bytes or characters, or
- arbitrarily-sized chunks of data read in a single call to the underlying socket.
Credit for reading from the socket is issued in terms of these work units.
For example, if the work unit selected is spans separated by String
crlf
, then the credit value in the actor will be interpreted as the
number of CRLF-terminated lines of input to read from the socket and
send to the controlling actor. If the work unit separated is raw
binary data, the credit value will just denote the number of chunks to
be sent.
The default work unit type is
rawMode
.
Delimiter-separated work units
socketActorProxy delimiterMode: aString.
socketActorProxy delimiterMode: aStringOrByteArray binary: aBoolean.
Selects delimiter-separated work units.
If aBoolean
is not supplied or is false
, then UTF-8 decoding will
be automatically performed on the input. In this case, aString
or
aStringOrByteArray
must be a String
, the delimiter to watch for.
Delivered work unit items will be String
s.
If aBoolean
is supplied and is true
, then aStringOrByteArray
must be a ByteArray
. No decoding of input will be performed.
Delivered work unit items will be ByteArray
s.
Arbitrary chunk work units
socketActorProxy rawMode. "Default at actor startup time."
socketActorProxy rawModeAscii.
These select either raw binary mode or raw ASCII mode, respectively. Each time any data is available at all, the entirety of the available data will be delivered as a single work unit to the controlling actor.
For rawMode
, delivered work unit items will be ByteArray
s; for
rawModeAscii
, they will be String
s.
Using ASCII mode is almost always wrong.
Strongly prefer to either use a delimiter-separated mode, which does
UTF-8 conversion for you, or rawMode
, with UTF-8 decoding performed
in the controlling actor.
Issuing credit
socketActorProxy issueCredit.
Allows the receiver to relay one more work unit from the underlying socket.
socketActorProxy issueCredit: amount.
Allows the receiver to accept amount
more units of input from the
underlying socket. The amount
may be negative, in which case the
final credit is clamped to be non-negative.
socketActorProxy infiniteCredit.
Sets credit to infinity, allowing data to be read and delivered as fast as it arrives. This is usually not a good idea. Useful for testing, and in tightly controlled situations, but in the wild this can overwhelm your image with input.
socketActorProxy zeroCredit.
Sets credit to zero, preventing future read work (other than anything that has already completed or is currently running, but hasn’t been fully delivered to the controlling actor yet) until credit is subsequently increased.
Data delivery events
controllingActorProxy tcpSocket: socketActorProxy data: workUnit
This event is delivered to the controlling actor whenever a unit of credit has been used up in receiving a work unit from the socket.
Other events
controllingActorProxy tcpSocketTimeout: socketActorProxy.
Issued when a connection attempt, an attempt to send data, or an attempt to read data times out.
There is currently no way to set a read timeout implemented. Also, Squeak’s send timeout appears to be hard-coded.
Shutting down a connected socket
socketActorProxy stop.
The SocketActor
will disconnect (if it was connected) and close and
then terminate, activating its links as usual.
Remember that the actor links to its controlling actor! You will often
want to unlink before calling stop
:
socketActorProxy actor unlink.
socketActorProxy stop.
-
Surprisingly, “credit-based flow control” doesn’t seem to be a term in common usage outside very narrow circles such as ATM-based networking. However, it is widely known in distributed systems folklore. One example of its use is in the RabbitMQ server internals for managing flows of messages. A more in-depth look at the topic in the setting of ATM networking can be found in chapter 4 of
“Traffic Management for High-Speed Networks: Fourth Lecture International Science Lecture Series”, ed. H. T. Kung. The National Academies Press, Washington, DC. 1997. DOI.