Spectranet: Tutorial 2

From Spectrum
Jump to navigation Jump to search

For the first practical session, we'll start with a TCP server. We'll start with this because it's easy to play with: all you need for the other end of the connection (on, for example, a PC) to play with it is the telnet command. Even better than telnet is netcat (the 'nc' command) which comes by default with most Unixy type operating systems (Linux, BSD etc.), but if you don't have netcat, telnet suffices. Whether you run Windows, Mac OS X or something else, you'll have a telnet client. Telnet is also tremendously useful for simple debugging of various networked services, so it's as well to introduce it at this point.

This tutorial will be concerned with writing a simple TCP server which can handle one connection; the 'hello world' of network server programs.

What does a server program need to do?

All server programs on a TCP socket will do the following:

  • Open a socket.
  • Bind it to a port.
  • Tell the socket to start listening.
  • Accept any incoming connections.
  • Transfer some data.
  • Close the connection when done.

Opening a socket is pretty self explanatory - it allocates the resources associated with what you eventually want to be a connection to a remote machine. It's the first step of any networked program, whether it's for TCP, UDP, a server or a client. When you successfully open a socket, you get something back called a file descriptor or socket handle. If you're a Unix programmer, you'll know them as file descriptors. Windows programmers will know them as socket handles. It doesn't matter - they are the same thing - it's basically an identifier for the system to the socket you've just opened. However, an open socket on its own isn't a lot of use - you have to tell the system what you want to do with it. The bind operation associates a socket with a local port, for example, port 80 if it was a web server. But even then, the socket doesn't do much - because it doesn't yet know whether you want to connect to a remote host with it, or listen with it. In the case of a server the next step is to tell it to listen.

Once you've done open, bind, and listen, the socket is finally in a state where it will do something! A remote machine can now try to connect. There's still one more step though - when a machine connects, the server needs to accept the connection.

Accept does something very interesting. It doesn't merely tell the TCP stack to do all the necessary stuff to get the socket into the established state - it actually creates a completely new socket which is a clone of the one you opened, with a brand new file descriptor. This new socket is actually the one you'll send and receive data on. The original socket continues to listen for further connections. So when you come to sending data you won't be using the original file descriptor. When you close the connection that you accepted, it doesn't affect the state of the original listening socket.

The code

The following code snippets show how all the above is done, in both assembly language and C. To see the code in context, you should open the following link in a new tab or browser window - the example assembly code for this tutorial is here: [1]. The example C code is here: [2]

If you're using assembly language, your code should include the file spectranet.asm, available here: [3] . This file has all the symbols you need to call the ROM code. C users, using the Z88DK should include the relevant includes - normally at least <sys/socket.h> and <sys/types.h>, and link with the library libsocket.

Using telnet to test the code

Note: Microsoft has removed the 'telnet' command in Windows 7 and newer. Download PuTTY and use this instead. If you're using Linux, sometimes telnet is not installed by default, however netcat (nc) is often installed. Usually installing either telnet or netcat on Linux is very straight forward (apt-get install telnet or yum install telnet, depending on distro).

If you assemble or compile the example code, and load it onto a Spectrum, you can try it out by typing RANDOMIZE USR 32768 on the Spectrum. Then on another machine, such as a PC or Mac, you can try connecting to it as follows. Open a command prompt or shell (in Windows, Start -> Run program, and type 'cmd', in Mac OS X, Linux or BSD, open a terminal window), and then type:

telnet <ip-address-of-your-spectrum> 2000

for example, if your Spectrum is at

telnet 2000

...and you should immediately get the message 'Hello world', and the Spectrum will display 'Connection established.' You can then type a string into telnet, which the Spectrum will display. If you start the string with the letter 'x', the example program on the Spectrum will exit.

Opening the socket

This is a very simple operation. In assembly language, there is only one parameter - the socket type you want to open, which may be SOCK_STREAM or SOCK_DGRAM. The TCP socket is of type SOCK_STREAM. It's passed in the C register:

    ld c, SOCK_STREAM     ; The kind of socket we want - a stream - i.e. TCP
    ld hl, SOCKET         ; The address of the socket() routine
    call HLCALL           ; Call it
    jr c, .error          ; Handle any errors
    ld (v_sockfd), a      ; Save the socket handle somewhere in memory

You'll note a couple of things about this code. Firstly, we don't just 'call SOCKET'. This is because under normal circumstances, the Spectrum ROM will be paged in, and the Spectranet won't be. There are three entry points that cause a page-in. HLCALL and IXCALL cause the Spectranet to page its ROM in, call the address in HL (or IX in the case of IXCALL), and when the call is finished, page out again (so the normal Spectrum ROM is available). The second thing you'll note is that the carry flag is used to indicate whether the call succeeded or not. For all the Spectranet ROM calls that can report an error, the carry flag is set on error, and the A register contains the error code.

For C users, opening the socket is done like this:

  int sockfd;
  sockfd=socket(AF_INET, SOCK_STREAM, 0);

If an error occurs, sockfd is set to a negative value. Nearly all the socket library functions return a negative value on error, and 0 or a positive value on success. The current Spectranet hardware doesn't support anything other than AF_INET sockets - but be sure to set the first parameter, because one day at least AF_INET6 may be supported too. The last parameter should be set to 0 (the default protocol; the Spectranet doesn't yet support anything else).

So now we have a valid socket file descriptor (or socket handle), which we can now do operations on. The ROM code has allocated resources for the socket - poked the various hardware registers to allocate hardware resources, and set a couple of system variables to associate the hardware register set with the socket file descriptor.

Another thing to note at this stage is that since you've allocated these resources, you need to be careful to de-allocate them and not leak socket handles, or you'll run out of hardware resources! Note that sockets don't get automatically closed if your program exits...so be sure to close them in your exit routines. (Pressing the reset button will, however, clear everything out).

Associating the socket with a port

You've probably noticed that programs on the internet don't just have a server address, but a port, too. Ones you may know already are HTTP for the world wide web, on port 80, the secure shell on port 22, or FTP which uses both port 21 and a randomly assigned port. The next step in a server program is to associate the socket with the port it'll listen on with the bind function.

In assembler, the operation is very simple. We'll assume that the A register still contains the socket file descriptor - if not, just retrieve it from memory. There's no return code unless an error occurs, and the port to bind to is passed in the DE register pair, in machine byte order (the BIND function takes care of converting it to network byte order).

     ld de, 2000     ; The port we want to listen on.
     ld hl, BIND     ; Call the BIND function
     call HLCALL     
     jr c, .error    ; Carry flag = error

The C interface is a bit more formal about things. Firstly, there's a structure, struct sockaddr_in, which is a general purpose internet address structure (hence the name sockaddr_in - socket address, internet). This must be set up with the port.

     struct sockaddr_in my_addr;

You'll note the macro htons here. This converts a machine order (little endian) integer into a network order (big endian) value. Except here, it's actually a no-op - because the underlying BIND function in ROM does this for us. You should still use htons - it won't bloat your code (since it's a macro that doesn't do anything) but it's a good habit in case you do network code on other machines where it is not a no-op!

Now call bind:

     if(bind(sockfd, &my_addr, sizeof(my_addr)) < 0)

The first argument to bind() is the file descriptor, the second is the address of the sockaddr_in structure you made earlier, and the last argument is the size of this structure.

If all went well, the socket is now bound to a local address (in our case, that just means it's associated with a port).

Tell the socket to listen

This is a very simple operation. Just pass the file descriptor to the listen function. In assembler, you do it like this:

     ld a, (v_sockfd)     ; get the socket file descriptor
     ld hl, LISTEN        ; for the LISTEN function call
     call HLCALL
     jr c, .error

It's also a very simple operation in C.

     if(listen(sockfd, 1) < 0)

Note the second parameter in the C library call. This is the allowed backlog. The hardware currently doesn't support changing the backlog, but set this to something sensible in case it eventually does.

Wait for a connection, and accept it

Things get a bit more interesting at this point, since there's more than one way of accomplishing this. However, for this tutorial, we'll concentrate on the simple case - just block until someone connects. The accept function will do just this if called before any remote host has tried to connect - it won't return until someone connects, and when it does, it'll return with the file descriptor of the accepted socket.

That last sentence is very important: it's at this point we're actually going to get a new socket created. The accept call does all this for you - it will block until someone connects, and when they do, it'll clone the listening socket, and return a completely new socket file descriptor which you use for communicating with the remote host. The original socket you opened in the first three steps is still open, and the accept function can accept further connections from this socket. We won't worry about that in this tutorial though - this example will be written just to handle a single client.

For the assembly version, we've got another byte in memory for storing this new file descriptor, called v_connfd. It's just another integer.

     ld a, (v_sockfd)      ; The listening socket we did open, bind and listen on.
     ld hl, ACCEPT         ; The accept call.
     call HLCALL           ; Call accept, and block until someone connects to us.
     jr c, .error          ; carry set = error occurred
     ld (v_connfd), a      ; Save the new socket file descriptor

In C, the same operation is done as follows:

     connfd=accept(sockfd, NULL, NULL);
     if(connfd > 0)

The two parameters set to NULL are for a struct sockaddr_in* pointer, and structure length. We won't worry too much about them now, but if they are filled in, accept will fill the structure with the address of the remote host that connected to this program.

Send and receive data

Data is sent and received with the send and recv functions, respectively. You can think of send and recv a bit like the BASIC keywords PRINT and INPUT - send will send the data and return immediately (unless the transmit buffer is full), and recv - like INPUT, will block until there's data ready to be returned to the program. There are some other details, too.

For sending data, we'll assume we've got a buffer somewhere in memory containing the string 'Hello world\n' (i.e. 'Hello world' with a newline), and we want to send this 12-byte chunk of data. In assembler this is what you need to do:

     ld a, (v_connfd)      ; file descriptor of the connection we accepted earlier
     ld de, BUF_hello      ; memory address of the buffer containing 'Hello world\n'
     ld bc, 12             ; which is 12 bytes long
     ld hl, SEND           ; call the SEND function
     call HLCALL
     jr c, .error          ; did it work?

The ROM call for SEND therefore takes three arguments. The socket file descriptor is passed in the A register. DE contains the address of the block of data to send. BC is set to the number of bytes to send. When SEND returns, if all is well, BC is set to the number of bytes actually sent.

The C library call is essentially the same, with just one extra parameter, flags (which currently should be left set to 0).

     char *hstring="Hello world\n";
     bytes=send(connfd, hstring, strlen(hstring), 0);

A negative value is returned in case of an error, but otherwise, send returns the number of bytes sent.

The recv call is very similar - again, it takes the socket on which to receive data, an address of a buffer (in this case, the address of a block of memory to fill), and the size of the buffer. It returns how many bytes were actually received. Note that recv doesn't attempt to fill the entire buffer you give it - the size of the buffer is just the maximum amount of data it will return. If some data that's less than the size of the buffer is received, recv returns this - so it's important to check the number of bytes returned in case you expect more than you actually got! The recv call will block until some data is received. (How to stop your program from stopping until someone sends some data will be covered in the multiplexing tutorial).

In assembler, recv is used like this:

     ld a, (v_connfd)      ; Get the connection file descriptor.
     ld de, BUF_receive    ; Set DE to the address of a buffer to fill with data
     ld bc, 512            ; The buffer pointed to by DE is 512 bytes long.
     ld hl, RECV           ; Call the RECV function.
     call HLCALL           ; ...and BC will contain the number of bytes actually received
     jr c, .error

As with send, the RECV function takes three arguments: A contains the socket file descriptor, the DE register pair contains the address off the start of a buffer in memory to fill, and BC contains the maximum amount of data that RECV should put in this buffer. On return, the BC register pair contains the number of bytes actually received, which in many cases will probably be less than the size of the buffer.

The C call is similar, too:

    char buf[512];         /* allocate a 512 byte buffer */
    bytes=recv(connfd, buf, sizeof(buf), 0);

Again, the flags parameter should be set to zero. If an error occurs, recv returns a negative value.

Closing the connection

Once you're done with a socket, it should be closed. It's important to explicitly do this - while in Unix or Windows, all file descriptors and memory gets cleaned up when a program exits, the Spectrum doesn't really have this concept - so if your program is going to get invoked multiple times, it's important to make sure all sockets are closed when it exits. Many programs will need to service many connections that come and go, for example, a web server will have to open, service and close perhaps dozens of connections a second. Even on a big mainframe, if you didn't clean up the socket file descriptors you could end up running out.

The close routine does this. If your server is going to be servicing more connections, the original socket should be left open, since this is the one you're listening on, and just the connection sockets should be closed when they are done with (except, of course, when your program exits, at which point you'll want to close your listening socket, too). The close routine is very simple:

     ld a, (v_connfd)       ; get the connection's file descriptor
     ld hl, CLOSE
     call HLCALL
     jr c, .error

So what errors can happen on close? If your program has a bug, you might end up trying to close a socket file descriptor that doesn't actually exist.

The C close routine is called sockclose. This is because the Z88DK doesn't yet have a truly generalized fcntl, so the z88dk close() routine doesn't know how to close a socket. The sockclose routine, therefore, only works on socket file descriptors and not, say, a file descriptor for a file on disc. This may well change; the z88dk developers are working on a more generalized fcntl right now.

In C, closing a socket is as simple as:

     if(sockclose(connfd) < 0)

In summary

As you've seen, a simple TCP server consists of creating the socket with the socket routine, binding it to a port with bind, telling it to listen with listen, accepting an incoming connection with accept, then sending and receiving data with send and recv. Once the connection's done, it's closed with close, or sockclose if you're using the z88dk. Both accept and recv block until they get a connection and data respectively, but there are ways to avoid blocking - which will be discussed in a later tutorial.

In the next tutorial, we'll cover writing a TCP client.

Spectranet: Tutorial 3

Further reading

Practical code: The simple loader tool, from the utility ROM's NMI menu, which is a simple server that receives data on port 2000 and puts this data into the Spectrum's RAM: [4] - see the F_loader routine.

Reference manuals for the functions used: