Spectranet: Tutorial 2

From Spectrum
Revision as of 23:28, 28 June 2008 by Winston (talk | contribs) (New page: 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 P...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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. Links to the source code for a complete, runnable example are at the end of the page for you to download, assemble/compile and then run. If you're using assembly language, your code should include the file spectranet.asm, available here: [1] . 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.

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;
     ....
     my_addr.sin_family=AF_INET;
     my_addr.sin_port=htons(2000);

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)
     {
        handle_error();
        return;
     }

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)
     {
        handle_error();
        return;
     }

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.