Network Interface

1. Internet Addressing

You will need to use instances of class InternetAddress to identify network hosts. Address objects can be constructed in several ways, for example, all the following identify the same address:

InternetAddress hostName: 'localhost' port: 80.
InternetAddress localPort: 80.
InternetAddress address: #(127 0 0 1) port: 80.
InternetAddress address: 16r7F000001 port: 80.
InternetAddress address: '127.0.0.1' port: 80.

2. Working with TCP sockets

2.1. Connecting to a remote address

Use the #connectToRemoteAddress: method to connect to a host. The method takes an InternetAddress argument:

| socket |
socket := TcpSocket new.
socket connectToRemoteAddress: (
    InternetAddress hostName: 'localhost' port: 80).

2.2. Closing the connection

Sockets must be explicitly closed via the #close method when done.

socket close.

2.3. Sending and receiving objects using streams

Sockets provide basic read and write streams to help simplify sending and receiving data. These streams are returned by the #readStream and #writeStream methods and can be wrapped with higher-level streams for more flexibility.

For example, to send a simple HTTP request and receive the corresponding HTTP response back, we wrap the basic socket streams with Utf8 encoding and decoding streams:

| socket readStream writeStream response |
socket := TcpSocket new.
socket connectToRemoteAddress: (
    InternetAddress hostName: 'localhost' port: 80).

writeStream := Utf8EncodingStream on: socket writeStream.
writeStream 
    nextPutAll: 'GET /'; 
    nextPut: Character cr; 
    nextPut: Character lf;
    flush.

readStream := Utf8DecodingStream on: socket readStream.
response := readStream contents.

socket close

Both read and write stream reach their end when the underlying connection closes. Method #atEnd will return true and all methods invoking #atEnd, such as #next and #nextPut:, will throw an exception.

2.4. Sending and receiving bytes

Use the #sendBytes: and #receiveBytes: methods to send and receive bytes directly. Both methods take a ByteArray argument. For example, to send a simple HTTP request and receive the corresponding HTTP response using raw bytes:

| buffer numberOfBytesSent numberOfBytedReceived response |
buffer := ByteArray new: 1024.
(socket := TcpSocket new)
    connectToRemoteAddress: (
        InternetAddress hostName: 'localhost' port: 80).
numberOfBytesSent := socket sendBytes: 'GET /\U00000D\U00000A' encodeAsUtf8.
numberOfBytesReceived := socket receiveBytes: buffer.
response := buffer decodeAsUtf8.
socket close

Note that #sendBytes: and #receiveBytes: are meant to be used for sending small amounts of data only. Larger data may be sent and received in several pieces of arbitrary size, which have to be put together based on the number of bytes actually sent and received. To send and receive arbitrary-length objects, you should use the socket streams instead.

3. Working with TCP server sockets

3.1. Listening for connections

Use one of the #listen* method variants to listen for connections. For example, the following server socket will listen on port 8080 and enqueue a maximum of 10 connections:

| serverSocket |
serverSocket := TcpServerSocket new.
serverSocket listenOnLocalPort: 8080 backlog: 10.

3.2. Accepting connections

Use the #acceptConnection method to accept a connection:

| serverSocket socket |
serverSocket := TcpServerSocket new.
serverSocket listenOnLocalPort: 8080 backlog: 10.
socket := serverSocket acceptConnection.

3.3. Shutting down the server

Server sockets must be explicitly closed via the #close method when done.

serverSocket close.

4. Working with UDP sockets

Use the #sendBytes:toRemoteAddress: method to send data:

| sendingSocket numberOfBytesSent |
sendingSocket := UdpSocket new.
numberOfBytesSent := sendingSocket
    sendBytes: #(1 1 2 3 5 8 13 21) asByteArray
    toRemoteAddress: (
        InternetAddress hostName: 'localhost' port: 9080).
sendingSocket close

To receive data, first bind the socket to the receiving port, then use the #receiveBytes: method:

| receivingSocket buffer numberOfBytesReceived |
buffer := ByteArray new: 8.
receivingSocket := UdpSocket new.
receivingSocket bindToLocalPort: 9080.
numberOfBytesReceived := receivingSocket receiveBytes: buffer.
receivingSocket close.

Both sending and receiving sockets must be closed when done. Note that data may be sent and received in several pieces of arbitrary size, and have to be put together based on the number of bytes actually sent and received.

5. Working with sockets asynchronously

Socket operations such as accepting, connecting, sending, and receiving block the current Smalltalk task until they complete. These operations may be performed in separate tasks so the main application task keeps running.

For example, the following code attempts to connect in a separate task:

| socket |
socket := TcpSocket new.
[
    socket connectToRemoteAddress: (
        InternetAddress hostName: 'localhost' port: 80).
] forkAt: Task lowPriority

Sockets trigger #connected events and server sockets trigger #accepted events. These events may be used for simple notifications, for example, updating the user interface when connecting and accepting are done in separate tasks. Both #connected and #accepted event handlers take a socket as argument.

For example, the following event handler may be registered before forking the connection task shown above:

socket when: #connected evaluate: [:connectedSocket |
    Transcript cr; nextPutAll: 'connected'].

6. Example: EchoServer

This example implements a simple echo server. Clients send text requests to the server, which echoes them back in uppercase.

Download EchoServer.zip.

6.1. Implementing the echo client

The client connects to the server, sends a request string, and receives the response string in a separate task, forked from the #sendRequest:toRemoteAddress: instance method in the EchoClient class:

EchoClient >> sendRequest: aString toRemoteAddress: aSocketAddress

    | socket |
    socket := TcpSocket new.

    [
        [
            (socket connectToRemoteAddress: aSocketAddress) ifTrue: [
                | readStream writeStream |
                writeStream := Utf8EncodingStream on: socket writeStream.
                writeStream atEnd ifFalse: [
                    writeStream
                        nextPutAll: aString;
                        nextPut: Character cr;
                        nextPut: Character lf;
                        flush.
                    readStream := Utf8DecodingStream on: socket readStream.
                    readStream atEnd ifFalse: [
                        self trigger: #receivedResponse with: readStream nextLine]]]
        ] ensure: [socket close]
    ] forkAt: Task lowPriority

To get notified of the actual response, an event handler for the #receiveResponse event must be registered with the client before sending any requests to the server. The event handler takes one argument, the response.

6.2. Implementing the echo server

The server accepts connections in a separate task, forked from the #openOnLocalAddress: instance method:

EchoServer >> openOnLocalAddress: aSocketAddress

    socket isNil ifFalse: [
        socket isListening ifTrue: [^true]].

    ((socket := TcpServerSocket new)
        listenOnLocalAddress: aSocketAddress)
            ifFalse: [^false].

    self forkAcceptingTask.
    ^true

The actual accepting is done in the #acceptConnection method, which forks a processing task for every accepted connection:

EchoServer >> forkAcceptingTask

    [self acceptConnection] forkAt: Task lowPriority

EchoServer >> acceptConnection

    [socket isListening] whileTrue: [
        | clientSocket |
        clientSocket := socket acceptConnection.
        clientSocket isNil ifFalse: [
            Transcript cr; nextPutAll: 'Server connection opened...'.
            self forkProcessingTaskForSocket: clientSocket]].

    Transcript cr; nextPutAll: 'Server closed!'

The actual processing for every connection is done in the #processConnectionForSocket: method, which reads lines and writes them back as uppercase using the connection streams:

EchoServer >> forkProcessingTaskForSocket: aSocket

    [self processConnectionForSocket: aSocket] forkAt: Task lowPriority

EchoServer >> processConnectionForSocket: aSocket

    | readStream writeStream |
    readStream := Utf8DecodingStream on: aSocket readStream.
    writeStream := Utf8EncodingStream on: aSocket writeStream.

    [readStream atEnd] whileFalse: [
        writeStream
            nextPutAll: readStream nextLine asUppercase; 
            nextPut: Character cr;
            nextPut: Character lf;
            flush].
    aSocket close.

    Transcript cr; nextPutAll: 'Server connection closed!'

The connection processing task terminates on the server as soon as the client disconnects. The client disconnects by closing its connection end as soon as it receives the response string. This causes readStream atEnd to return true in the connection processing task on the server, which then closes its end of the connection and terminates.

6.3. Running the echo server and clients

To run the echo example:

  • Create and start the server up.

    Open a workspace window and evaluate the following:

    (server := EchoServer new) openOnLocalPort: 8000.
    

    The workspace now has a variable named #server holding an echo server object. This server listens for connections on the given port.

  • Create a client.

    Open a second workspace window and evaluate the following:

    (client := EchoClient new)
        when: #receivedResponse evaluate: [:response | 
            Transcript cr; nextPutAll: 'Client received ', response].
    

    The second workspace now has a variable named #client holding an echo client object. This client handles response events by logging the response to the Transcript.

  • Send requests from the client to the server.

    Evaluate the following in the second workspace:

    client 
        sendRequest: 'Hello world!'
        toRemoteAddress: (
            InternetAddress hostName: 'localhost' port: 8000)
    

    The following should appear in the Transcript window:

    Server connection opened...
    Client received HELLO WORLD!
    Server connection closed!
    
  • Shut the server down.

    Evaluate the following in the first workspace:

    server close
    

    The following should appear in the Transcript window:

    Server closed!
    

Last modified June 6, 2006.