GNU Smalltalk SimpleEcho TCP Server

Or: Getting started with Smalltalk TCP servers and classes

I was complaining that I couldn't find a simple "next step" tutorial for learning how to use GNU Smalltalk's TCP.ServerSocket class. Well, I decided to give a shot at such a thing myself, having never written a Smalltalk program before, and created a file "SimpleEcho.st" to fiddle around with making a simple TCP echo server in Smalltalk.

The first thing to do was to figure out how to create a server socket, wait for a connection, write some data to the client, close the connection, and end:

"SimpleEcho -- a simple TCP echo server in smalltalk"

"Load the TCP Package"
PackageLoader fileInPackage: 'TCP'!

"Create a new server socket on port 8000"
Smalltalk at: #ss put: (TCP.ServerSocket port: 8000)!

"Blocking wait for connection"
ss waitForConnection!

"Accept the socket"
Smalltalk at: #s put: (ss accept)!

'Hello, user!' displayOn: s!
(String with: (Character value: 10)) displayOn: s!
s flush!
s close!

Firing up another trusty shell window and issuing the command telnet localhost 8000 I saw "Hello, user!" and my connection was closed. Hooray.

But this isn't as useful as being able to send such a message to multiple clients, one at a time. So we introduce (for now) the infinite loop after creating the server socket:

[ true ] whileTrue: [
  | s |
  ss waitForConnection.
  s := (ss accept).
  'Hello, user!' displayOn: s.
  (String with: (Character value: 10)) displayOn: s.
  s flush.
  s close
]

Now we're getting somewhere. One of the things going on in the above that isn't easily grasped from the tutorial is that "| s |" bit -- once we're inside a BlockClosure (inside "[" and "]") we don't get to use "Smalltalk at:" to store our variables. Instead we get to declare local variables, and in this example, we declared a local variable "s" and kept on going.

Next up, we'd like to turn this into a real echo server, instead of simply spitting a line of text to the client and hastily closing the connection. And we'd like to permit multiple clients to connect at the same time. Here's some code to mull over:

[ true ] whileTrue: [
  | s |
  ss waitForConnection.
  s := (ss accept).
  [
    [ true ] whileTrue: [
      | msg |
      msg := (s nextLine).
      msg displayOn: s.
      (String with: (Character value: 10)) displayOn: s.
      s flush
    ]
  ] fork
]

All right. We see a few new things here. Let's start with "[ ... ] fork". Again what we're doing is defining another BlockClosure and calling "fork" on it -- basically whenever Smalltalk gets here, a new process is launched and control returns to our main loop. Next up, another (for now) infinite loop, where we have another local variable "msg", and we see that streams (such as sockets) have a "nextLine" message we can use to read a line of input from our clients.

When you run this code, you can telnet from multiple clients to your port, and have every line you type echoed back to you. Not particularly useful yet, and you might see some interesting errors if you close one of your telnet sessions (since we have an infinite loop trying to read the next line from that socket!) but it's a start towards actually figuring out how to write at least minimally useful Smalltalk code.

Now we'd like to see what all this "Object Oriented" stuff is about, and set off defining this in the form of a class. Here is what I came up with as my final file listing for this entry:

"SimpleEcho -- a simple TCP echo server in smalltalk"

"Load the TCP package"    
PackageLoader fileInPackage: 'TCP'!

"Define a class for our server, it stores (so far) a server socket"
Object subclass: #SimpleEchoServer
  instanceVariableNames: 'ss'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'SimpleEcho'!

"Define a class method to create an instance given a port number"    
!SimpleEchoServer class methodsFor: 'instance creation'!

port: anInteger
  | ses |
  ses := super new.
  ses init: anInteger.
  ^ses
!!

"Instance method for initialization from above class method"
!SimpleEchoServer methodsFor: 'instance initialization'!

init: anInteger
  ss := (TCP.ServerSocket port: anInteger).
  ^self
!!

"Instance method to handle running the main (infinite) loop"
!SimpleEchoServer methodsFor: 'running'!

run
  | s |
  [
    ss waitForConnection.
    s := (ss accept).
    [self handleSocket: s] fork
  ] repeat
!!

"Instance method to handle each connection"
!SimpleEchoServer methodsFor: 'handling'!

handleSocket: s
  [
    | msg |
    msg := (s nextLine).
    msg displayOn: s.
    (String with: (Character value: 10)) displayOn: s.
    s flush
  ] repeat
!!

"Create an echo server"    
Smalltalk at: #echoServer put: (SimpleEchoServer port: 8000)!

"Run it!"
echoServer run!

That's it. Probably a dozen things horribly wrong, but it gets us started at least. For the next entry, I'm hoping to figure out how to create a package that I can later import to create many similar classes, such as a chat server.

changed September 25, 2007