Network communications using TCP/IP Sockets

Dolphin offers an add-on Sockets interface component for programming with Windows Sockets. This is delivered in compressed ZIP file, Sockets.zip. Unzip this into a Dolphin\Extras\Sockets directory. The ZIP contains the Sockets Package (Sockets.PAC) so you should use the Package Browser to load this into your Dolphin image.


Windows Sockets

Windows Sockets provide a network independent interface on top of the underlying network protocol. Microsoft Windows implements Windows Sockets based on TCP/IP.

A socket represents one end of a connection. When two parties are connected, there is a socket at each end of the connection. Once a connection is established, unsolicited data can be sent in either direction.


Listen for a connection with ServerSocket

A ServerSocket listens for a connection on a particular port. When a client requests a connection, the ServerSocket creates a new instance of Socket to represent the new connection.

Create an instance of ServerSocket and associate it with a port. Send it the #accept message, and it won't return until a client makes a connection. When it does return, the answer is an instance of Socket representing the end of the connection. Having accepted a connection, the same ServerSocket can listen for another connection.

ServerSocket class >> port: anInteger

Answers a new instance of ServerSocket associated with port anInteger.

ServerSocket >> accept

This instructs the ServerSocket to start listening for a connection.

Answers a Socket representing the next incoming connection.

Dolphin uses a semaphore to wait until a client makes a connection before returning. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The #accept can be sent from inside a forked process. This will allow other processing to continue while waiting for a connection.

Example

"Create a ServerSocket on port 12."
serverSocket := ServerSocket port: 12.

"Listen for a connection."
hostSocket := serverSocket accept.

"Fork a process to listen for a second connection."
[hostSocket2 := serverSocket accept] fork.


Monitor a ServerSocket with ServerSocketMonitor

An application that offers a service over the sockets interface will typically fork a process to repeatedly listen for connections. Whenever a connection is accepted, the application will perform some action associated with the service it is offering.

This functionality is made available by the ServerSocketMonitor class. Instances of ServerSocketMonitor are associated with a ServerSocket. The monitor can be configured by specifying a Block to handle new connections.

ServerSocket class >> on: aServerSocket

Answers a new instance of the receiver associated with aServerSocket.

ServerSocket >> acceptBlock: aBlock

Sets the acceptBlock instance variable to aBlock. This block is evaluated whenever a connection is accepted. The block should take a single argument. It will be passed the Socket representing the new connection. The default acceptBlock does nothing to handle new sockets.

Example

"Create a ServerSocket on port 12."
serverSocket := ServerSocket port: 12.

"Monitor the ServerSocket for new connections."
serverSocketMonitor := ServerSocketMonitor on: serverSocket.
serverSocketMonitor acceptBlock: [:newSocket |
        "Process the new connection represented by newSocket."
        ...
        ]


Connecting to a host

A Socket represents one end of a sockets communication. A client connects to a host by creating an instance of Socket and associating it with a port and a host. The host can be specified by name or by IP address.

Socket class >> port: anInteger hostName: aString

Answers a new instance of Socket for connecting to port anInteger and host aString.

Socket class >> port: anInteger hostAddress: aByteArray

Answers a new instance of Socket for connecting to port anInteger and host address aByteArray.

Socket >> connect

Attempt to connect the socket to a host using the details previously specified by one of the instance creation methods.

Example

"Connect to a host by name."
(clientSocket := Socket port: 12 hostName: 'pdg.intuitive.co.uk') connect.

"Connect to a host by IP address."
(clientSocket2 := Socket port: 12 hostAddress: #[194 131 7 6]) connect.


Talking bytes

The lowest level of socket communication is the sending and receiving of bytes.

Socket >> sendByte: anInteger

Sends a single byte with the value defined by anInteger (between 0 and 255 inclusive).

Socket >> receiveByte

Reads a single byte from the socket.

Answers an Integer (between 0 and 255 inclusive) representing the value of the byte which has been read.

Dolphin uses a semaphore to wait until there is sufficient data to satisfy the read request. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The read can be performed inside a forked process. This allows other processing to continue while waiting for data..

Socket >> sendByteArray: aByteArray

Sends a series of bytes defined by aByteArray.

Socket >> receiveByteArray: anInteger

Receives a sequence of anInteger bytes.

Answers a ByteArray representing the bytes received.

Dolphin uses a semaphore to wait until there is sufficient data to satisfy the read request. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The read can be performed inside a forked process. This allows other processing to continue while waiting for data..

Example

"Send a byte."
clientSocket sendByte: 255.

"Receive a byte."
anInteger := serverSocket receiveByte.

"Send a sequence of bytes."
clientSocket sendByteArray: #[251 252 253 254 255].

"Receive a sequence of bytes."
aByteArray := serverSocket receiveByteArray: 5.


Streams

Sockets come with their own streams: SocketWriteStream and SocketReadStream supporting the standard stream protocol. These stream classes implement buffering, which can dramatically increase socket performance.

SocketWriteStream class >> on: aSocket

Answers a new instance of SocketWriteStream for writing to aSocket.

SocketWriteStream >> nextPut: anInteger

Sends a single byte with the value defined by anInteger (between 0 and 255 inclusive).

SocketWriteStream >> nextPutAll: aByteArray

Sends a series of bytes defined by aByteArray.

SocketWriteStream >> flush

Ensures that any data remaining in the buffer is send via the socket.

SocketReadStream class >> on: aSocket

Answers a new instance of SocketReadStream for reading from aSocket.

SocketReadStream >> next

Reads a single byte from the socket.

Answers an Integer representing the value of the byte which has been read.

Dolphin uses a semaphore to wait until there is sufficient data to satisfy the read request. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The read can be performed inside a forked process. This allows other processing to continue while waiting for data..

SocketReadStream >> next: anInteger

Attempts to receive a sequence of anInteger bytes.

Answers a ByteArray representing the bytes received.

Dolphin uses a semaphore to wait until there is sufficient data to satisfy the read request. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The read can be performed inside a forked process. This allows other processing to continue while waiting for data..

Example

"Create a SocketWriteStream on the socket."
clientWriteStream := clientSocket writeStream.

"Create a SocketReadStream on the socket."
serverReadStream := serverSocket readStream.

"Send a byte via the SocketWriteStream."
clientWriteStream
        nextPut: 255;
        flush.

"Read a byte via the SocketReadStream."
aByte := serverReadStream next.

"Send a sequence of bytes via the SocketWriteStream."
clientWriteStream
        nextPutAll: #[1 2 3 4 5];
        flush.

"Read a sequence of bytes via the SocketReadStream."
aByteArray := serverReadStream next: 5.


Talking objects

With the help of the socket streams, we can use the binary filer to send and receive Smalltalk objects.

To send an object, create a STBOutFiler on a SocketWriteStream.

To receive an object, create a STBInFiler on a SocketReadStream.

Dolphin uses a semaphore to wait until there is sufficient data to satisfy the read request. This waiting state can be cancelled (by a developer) by typing Ctl-Break. The read can be performed inside a forked process. This allows other processing to continue while waiting for data..

Example

"Use a STBOutFiler to file an object onto a SocketWriteStream."
(STBOutFiler on: clientWriteStream) nextPut: (2/3).
clientWriteStream flush

"Use a STBInFiler to load an object from a SocketReadStream."
(STBInFiler on: serverReadStream) next


Monitoring a SocketReadStream

An application that offers a service over the sockets interface will typically fork a process to repeatedly listen for incoming messages on a particular Socket. Whenever a message is received, the application will perform some action associated with the service it is offering. This functionality is made available by the SocketReadStreamMonitor class.

Instances of SocketReadStreamMonitor are associated with a SocketReadStream. The monitor can be configured by sending it the #messageBlock: and #errorBlock: messages. Both messages take a single argument block.

SocketReadStreamMonitor class >> on: aSocketReadStream

Answers a new SocketReadStreamMonitor which can be used to monitor the parameter aSocketReadStream.

SocketReadStreamMonitor >> messageBlock: aBlock

aBlock will be evaluated for each incoming message.

The parameter to the block is the incoming object.

SocketReadStreamMonitor >> errorBlock: aBlock

aBlock will be evaluated if an error occurs when reading from the stream.

The parameter to the block is the SocketError representing the error.

Example

"Create a SocketReadStream on the socket."
serverReadStream := serverSocket readStream.

"Monitor the SocketReadStream for incoming messages."
socketReadStreamMonitor := SocketReadStreamMonitor on: serverReadStream.
socketReadStreamMonitor
        messageBlock: [:anObject |
                "Process the new connection defined by newSocket."
                ...
                ];
        errorBlock: [:aSocketError |
                "Process the error defined by aSocketError."
                ...
                ]


Exceptions

Error handling is achieved through the exception mechanism. The default outcome of an error is a walkback indicating an unhandled exception. Exceptions can be handled by enclosing the code in a block and using the on:do: method.

Example

[(clientSocket := Socket port: 12 hostName: 'unknown') connect]
        on: SocketError do: [:e | Transcript show: e errorAsString; cr]


Sample Server: ReflectorServer

The class ReflectorServer is supplied as a sample server application. A ReflectorServer maintains a document and a collection of active clients. Clients can append objects to the document, and, as each object is added, it is reflected to the other participating clients. The key components of a ReflectorServer are:

a ServerSocket to listen for connections

a ServerSocketMonitor to monitor the ServerSocket and respond to events

a Set of sockets for communicating with client applications

an OrderedCollection for holding the document.

ReflectorServer class >> onPort: anInteger

Answers a new instance of ReflectorServer which listens to port anInteger.

ReflectorServer >> onConnection: aSocket

Handles new client connections. The new connection is represented by the parameter aSocket. The default behaviour is to add the new socket to the collection of client sockets and to send the new client the current document.

ReflectorServer >> onSocket: aSocket message: anObject

Handles messages from clients. The sender of the message is identified by parameter aSocket, and the message received is the second parameter anObject. The default behaviour is to add the message anObject to the document and to reflect the message to the other clients.

ReflectorServer >> onSocket: aSocket error: aSocketError

Handles socket errors. The socket which encountered the error is identified by parameter aSocket, and the exception is the second parameter aSocketError. The default behaviour is to show an error message in the Transcript and to remove the offending socket from the client list.