Spectranet: Tutorial 4

From Spectrum
Jump to navigation Jump to search

In the last two tutorials, we dealt with TCP, which provides a reliable, dependable stream of data - almost like having a direct connection between the two systems at each end of the socket. Completely transparently to the programmer, TCP handles things like dropped packets, mis-ordered packets, and delays caused by congestion. When you are using TCP sockets, from the programmer's point of view, you have a straightforward reliable stream. Bytes go in at one end, and come out at the other.

UDP is much different. You don't have a stream as in TCP, but you send and receive messages, called datagrams. There's no stream of bytes - you're sending a chunk of bytes in a single packet each time you call sendto(). You receive a datagram with recvfrom(). UDP provides no frills - there's no retransmit mechanism built into the protocol, there's no accounting for dropped or mis-ordered packets (indeed, the very idea of a mis-ordered packet simply doesn't make sense in the context of UDP itself - each message must fit in a single packet - although it may make sense to your application). If you expand the abbreviation, you find that UDP means User Datagram Protocol. The 'User' in this case is the programmer. The programmer defines what the datagram actually means to the application.

So UDP is a very simple protocol, where you just put some data in a packet and send it - unlike TCP, which gives you a byte stream between the program at each end of the connection.

Why would you want to use UDP, when TCP provides all the goodness of a reliable, properly ordered data stream?

Sometimes, TCP is not the best tool for the job. For many applications, a mis-ordered packet may as well be lost altogether. For other applications, a delayed packet is as good as lost. Some applications have very simple requirements, and don't need the overhead of TCP - such as DHCP and DNS. For both DNS and DHCP the entire message fits in a single datagram - so why bother with all the overhead of creating and tearing down a TCP connection? A very common usage of UDP is for multiplayer games. For a game, a misordered or delayed packet may as well just be discarded - using TCP would actually impede the playability of many multi user games - so UDP is used instead. In the context of the Spectranet, you can handle communications with far more remote systems with UDP - since you can communicate with many remote systems using just one socket.

There are other uses for UDP too - where you want to define your own method of making a reliable data stream, based on the requirements of a specific application. These are fairly rare. Generally, if you find yourself building in retransmit mechanisms into a UDP based program, you probably ought to consider using TCP.

Subtleties of UDP programming

When you're writing programs that use UDP, there's a few differences from programs that use TCP. Firstly, the line between client and server is much more blurred. Unlike TCP, you don't have the listen/accept sequence - the concept simply doesn't exist for datagram communication. Secondly, the sendto() and recvfrom() routines tend to get used rather than send() and recv(). These calls allow you to specify the address details for each call.

The UDP client is considered the program that sends the first message. Since there's no explicit listening is done when using UDP, and no concept of a connection, the server program cannot know any details of the client until the client actually sends a message. When using TCP, from the programmer's point of view, as soon as you accept the connection on the server - you can start sending data, because quite a lot has happened to set up the TCP connection so each end knows exactly what it's doing. But with UDP, you know nothing until you get a message from the client - at which point, you find out what port it's using, as well as its IP address. Similarly, there's no stream to close either. The server program will have no idea that a client has gone away - it just won't see any more messages. This contrasts with TCP, where connections get explicitly closed, and there's quite a bit of protocol activity to shut down the stream. TCP server programs usually find out when the program at the other end has quit. But with UDP, it just goes very quiet. So many UDP programs have some sort of handshaking messages, so the server knows when it can de-allocate resources that were associated with clients. Many UDP servers will have a timeout mechanism, too, to ensure that a client that got switched off, crashed, or lost its internet connection, doesn't leave resources reserved on the server.

The code

The code snippets are both in C and assembler, and you can get a full listing of the UDP server in C here - [1] and in assembly language here - (TO DO). For the client, the code is available in C here - [2] and in assembly language here - [3]. It's a good idea to have them open in another browser window to see the code in context as it's discussed.

As discussed in earlier tutorials, if you are writing a program in assembler you will need to include the spectranet.asm file which can be found here [4]. C programs should include the relevant files, <sys/socket.h> and <sys/types.h>, and possibly <netdb.h> if remote hosts need to be looked up.

Writing a server

The basics of a UDP server consist of the following steps:

  • Open a socket.
  • Call bind() to set the port you want to use.
  • Wait for clients to send datagrams, using recvfrom() to receive them, and service them appropriately, sending responses to the client with sendto().
  • Close the socket when finished.

That's really all that's to it! There may be some refinements, for example, you may want to write the program such that it doesn't block while waiting for data.

Opening a socket should be familiar to you by now. But note the difference between the following example, and the TCP examples covered in the previous tutorials. To open a socket for UDP, you open it like this:

In C:

   int sockfd;
   ...
   sockfd=socket(AF_INET, SOCK_DGRAM, 0);

And in assembly language:

   ld c, SOCK_DGRAM
   ld hl, SOCKET
   call HLCALL

The C function returns a negative value if the call to socket() failed, and the assembly language call returns with the carry flag set and the error number in A. If all goes well, you get back a socket descriptor.

Note the parameter SOCK_DGRAM. This tells the socket library that you require a datagram socket. For the socket family AF_INET, this means you want to use UDP.

Just as in the TCP server example, now we associate this socket with a local port, with the bind() call. This is done in exactly the same way as for a TCP server. To receive datagrams on UDP port 2000, in a C program you do this:

   struct sockaddr_in my_addr;
   ...
   my_addr.sin_family=AF_INET;
   my_addr.sin_port=htons(2000);   /* port 2000 */
   if(bind(sockfd, &my_addr, sizeof(my_addr)) < 0)
   {
      ...handle error...
   }

In assembler, you just put the port you want to bind into the DE register pair:

   ld de, 2000          ; port 2000
   ld hl, BIND
   call HLCALL
   jr c, .error

Once you've done this, all that is remaining is to wait for datagrams to arrive from clients. You can use poll to do this (see the next tutorial which covers polling), or just use recvfrom to wait for and receive the datagram as it arrives. In the example code, we use recvfrom. In C:

   struct sockaddr_in their_addr;
   int addrsz;
   char recvbuf[128];
   ....
   rc=recvfrom(sockfd, recvbuf, sizeof(recvbuf)-1, 0, &their_addr, &addrsz);

Note that recvfrom() takes a pointer to a struct sockaddr_in. When a datagram arrives, recvfrom() not only fills the buffer that you provide, but it also returns the address of the client by filling the struct sockaddr_in that you provide. You need this so you can send datagrams to the client. This structure will contain the IP address of the client, as well as the port that the client is using.

When recvfrom receives data, the number of bytes received is returned. If recvfrom fails, it returns -1.

The same concept can be seen with the assembly language call:

   ld a, (v_sockfd)          ; The socket to use.
   ld hl, v_sockinfo         ; Address of a socket info block - 8 bytes long
   ld de, resp_buffer        ; The buffer to fill with data
   ld bc, 128                ; The size of thebuffer
   ld ix, RECVFROM           ; The call we are making - RECVFROM
   call IXCALL
   jr c, .error

When this successfully completes, BC contains the number of bytes received. The memory pointed to by the HL register pair is filled with a block of 8 bytes, which looks like this:

   Bytes 0-3 - IP address, in big endian form.
   Bytes 4, 5 - The UDP port that the client is using.
   Bytes 6, 7 - The UDP port that we are using

This block can also be passed to sendto when sending a reply to the client, for example, in C:

   rc=sendto(sockfd, sendbuf, strlen(sendbuf), 0, &their_addr, addrsz);

Note how a pointer to the struct sockaddr_in their_addr is passed to the sendto() function. The recvfrom() function will have filled this with the client's address and client's port.

In assembler, the same thing is done as follows:

   ld a, (v_sockfd)          ; The socket to use
   ld hl, v_sockinfo         ; The same 8 byte block that we used for recvfrom
   ld de, send_buf           ; some data to send
   ld bc, 128                ; how many bytes to send - a block of 128 bytes
   ld ix, SENDTO
   call IXCALL
   jr c, .error

Writing a client

With UDP, what really makes the distinction between client and server, is who sends the first message. A client always sends the first message (because there's no other way a server can know where to send messages to a client, since the server won't know the client even exists until it sends a message).

The socket is opened in the same way as for a server, using the type SOCK_DGRAM. As soon as the socket is open, you can send messages with it, using sendto().

A client may also need to use gethostbyname() to look up the name of the server. You can see how this function is used in tutorial 3, the simple TCP client. It's also worth noting at this stage that gethostbyname() also converts dotted decimal IP addresses to the big endian 4 byte value used by the various socket functions.

Here is a run down of using sendto() in C, with an address looked up by gethostbyname():

   struct sockaddr_in their_addr;
   struct hostent *he;
   ....
   their_addr.family=AF_INET;
   their_addr.sin_addr.s_addr=he->h_addr;   /* the struct he returned by gethostbyname() */
   their_addr.sin_port=htons(2000);         /* port 2000 */
   rc=sendto(sockfd, msg, strlen(msg), 0, &their_addr, sizeof(their_addr));

Note that the socket library will pick the originating port for you. If you need to explicitly set the local port (only a very small number of clients ever need to do this), you can use bind() in exactly the same way as you do for a server. If you imagine an example where you do this, you'll see that the code for a simple client is extremely similar to a server, except that the client uses sendto() first, instead of recvfrom(), and the client may need to use gethostbyname(). Compare this to a TCP client and server, where the way the two work are quite distinct.

In assembler, the sendto library call is used as follows in the client (the call to gethostbyname is also shown so you can see how the sockinfo block is set up):

   ld hl, STR_address         ; Host or dotted decimal IP address, a null terminated string
   ld de, v_ipaddr            ; Pointer to storage where the IP address should be returned
   ld ix, GETHOSTBYNAME       ; Call GETHOSTBYNAME
   call IXCALL
   ....
   ld a, (v_sockfd)
   ld hl, v_sockinfo
   ld de, message_pointer
   ld bc, message_size
   ld ix, SENDTO
   call IXCALL
   jr c, .error
   ....
; Here is some memory reserved for the socket info block
v_sockinfo                    ; address of the 8 byte socket info block
v_ipaddr    defb 0,0,0,0      ; 4 bytes reserved for the IP address.
v_port      defw 2000         ; Remote port
v_localport defw 0            ; Local port - leave at 0 to set automatically

When the client needs to receive a message from a server, recvfrom() is used. In C, the same struct sockaddr_in that was used for the call to sendto() is typically used - you will see this in the example code that accompanies this tutorial.

In summary

UDP programs send and receive datagrams, rather than establishing a stream. A single UDP socket can be used to process datagrams from clients, unlike TCP, where each connection has a socket all to itself. So you can see UDP makes far fewer demands of the TCP/IP stack, since it is such a simple protocol.

The next tutorial will show you how to avoid your program stopping and waiting to receive data, so you can deal with multiple sockets as well as keyboard input.

Spectranet: Tutorial 5 - Avoiding blocking, and handling multiple sockets at once.

Further reading

The Spectranet ROM contains code that uses UDP sockets. The utility ROM contains a DHCP client, which uses broadcast UDP to contact a DHCP server and get information such as the IP address, netmask, gateway and DNS servers to use. You can look at the DHCP client's code here: [5]. One thing to note about the DHCP client is that it uses the indirect table to access socket functions, however, it doesn't use the IXCALL and HLCALL mechanisms. This is because the DHCP client only ever runs when the Spectranet ROM is paged in. After all, the DHCP client is running from this ROM!

The DNS client also uses UDP. You can read the code here: [6]. The DNS client, however, is a core ROM routine, and doesn't use the indirect jump table at all. Don't write your clients like this or they will stop working whenever the ROM is upgraded!

The following reference material is also useful:

  • socket - The socket() function call.
  • bind - Set the local port.
  • sendto - Send a message.
  • recvfrom - Receive a message.
  • gethostbyname - Look up a hostname or convert a dotted decimal to an IP address.
  • close - Close a socket.