Inspired by words from Ryan Tomayko and Jacob Kaplan-Moss, I set out to see how difficult it would be to implement a tiny UNIX echo server in scsh. It turns out to be fairly easy. In this post, we’ll cover:
- How the UNIX socket programming model works at a high level
- How to write a basic echo server in scsh
Unlike the echo servers of Tomayko and Kaplan-Moss, this one doesn’t use
fork (yet). We will add forking to the server in a follow-up post. Hopefully this will make it easier for less experienced UNIX programmers (like me) to get started with.
The UNIX socket programming model for dummies
At a high level, the steps to open a server-side socket are:
- create a socket “thing”
- toggle some settings on it so the OS knows it’s for interweb use
- bind it to an address
- listen for connections on it
- start accepting those connections
scsh does its part to make this easy by providing a high-level Scheme procedure,
BIND-LISTEN-ACCEPT-LOOP, which handles this stuff for you, in addition to some nifty error handling and other bookkeeping chores. But we’re going to ignore that for now and write everything by hand to see how it’s done.
You should be able to enter all of the code in the next section directly in the scsh top-level. You don’t need to open any additional packages; this is just plain old scsh.
Our echo server
Our server takes two arguments: the
PROC is a procedure that causes the server to actually do something; we’ll write in a minute. The
PORT is the port you want to serve on, e.g., 49995:
(define (server proc port) (let* ((sock (create-socket protocol-family/internet socket-type/stream)) (addr (internet-address->socket-address internet-address/any port))) (set-socket-option sock level/socket socket/reuse-address #t) (bind-socket sock addr) (listen-socket sock 5) (let loop () (lambda () (accept-connection sock)) proc (loop))))
The first thing you’ll notice is that it’s pretty sequential and quite simple really. We just go through the socket opening dance we described earlier: open, configure, bind, listen, and accept.
Since this is an echo server, we’ll just, like, echo stuff:
(define (echo socket address) (let* ((in (socket:inport socket)) (out (socket:outport socket)) (message (read-line in))) (format out "~A~%" message) (force-output out)))
As you can see, our
ECHO procedure takes a socket and an address. (We don’t do anything with the address in this example, but our procedure needs to “implement this interface”, as they say, in order to work. You can see this for yourself in the scsh 0.6.7 tarball; it’s in
LET*-binding we create some convenient locally bound names for the socket structure’s input and output ports, and then we read a line from the socket’s input port.
Since this is echo server, we just write the same data back out with
FORMAT. We call
FORCE-OUTPUT to flush output to the terminal immediately. Otherwise Scheme will wait for the operating system’s output buffer to fill before writing out, and it will appear to the user that nothing is happening.
Trying it out
Let’s start it up and see if it works. Assuming you’ve loaded the procedures above in your scsh image somehow, enter this at the REPL:
> (server echo 49995)
The REPL should appear to “hang” while the server is running. Now go to your terminal and connect to the echo server. There are several ways to do it; here’s what I used:
~ $ telnet localhost 49995 Trying ::1... telnet: connect to address ::1: Connection refused Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. HELLO HELLO who's there? who's there? i don't know i don't know could you stop that? could you stop that? fine then fine then goodbye goodbye
Hopefully that was enough to get you started playing with sockets in scsh. I’ll write a followup post where we talk about the UNIX forking process model and update this example to make it a “preforking” server.