Spectranet: Tutorial 3
As you go into this second practical example - one thing should stand out - there's not all that much difference between writing a simple server and a client! You open the socket the same way, you close it the same way, and you read and write to it the same way. The differences are that you may convert a hostname (such as 'spectrum.alioth.net') into an IP address (or a dotted decimal string into the system representation of an IP address), and instead of using bind, listen and accept, you just call connect to make the connection to the remote host.
So, this is generally what a TCP client must do:
- Look up the remote host using gethostbyname.
- Open a socket, using the socket routine.
- Connect to the IP address, using the connect routine.
- Send and receive data, using send and recv.
- Close the socket using close (or sockclose, when using the z88dk's C compiler).
In this example, we'll connect to the host 'spectrum.alioth.net' on port 80, and send some data that will hopefully elicit a reply. Port 80 is the port for web traffic, so the data we'll send will be 'GET / HTTP/1.0\r\n\r\n' (\r\n being the carriage return/newline sequence).
The code
The code snippets are both in C and assembler. [TODO - links to example code]
f 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 - for clients, this will be sys/socket.h, sys/types.h and netdb.h, and link with the library libsocket.
If you jumped straight in here, rather than starting with Tutorial 2, you'll want to read the bit about HLCALL and IXCALL, which explains how the ROM routines are invoked, in Tutorial 2.
Looking up the remote host
The first thing your client will normally need to do is turn a name such as 'spectrum.alioth.net', or perhaps a dotted decimal IP address string such as '70.85.70.131' into a 4-byte big endian representation of the IP address. The routine that does this is called gethostbyname, and for C users, it's defined in <netdb.h>. It's best to look up the host before opening sockets when using the Spectranet because it's a small system, and you can only open up to four sockets at once. The gethostbyname function opens a socket to talk to your nearest DNS server, and then closes it once it's got a response. If your client needs to open four sockets at once to remote hosts, if you try to do the lookup after opening the sockets, you'll find that the lookup fails. So it's good practise to get the gethostbyname call out of the way before creating the socket.
At this stage, something should be pointed out about any strings you might pass to the Spectranet ROM.
The Spectranet ROM routines all handle C strings. If you're a C programmer you'll immediately know what this implies - but if you're not, a C string is a set of characters that is null terminated - i.e. the string is ended by a byte set to zero. So, for instance, the string "Hello world" is represented as "Hello world" with the byte after the 'd' in world being set to zero. With sjasmplus, you can define C strings very easily, as follows:
remote_host defb "spectrum.alioth.net",0
The ,0 puts the null terminator on the end. Other assemblers may accept null terminated strings in this way, too.
In assembler you use gethostbyname as follows:
ld hl, remote_host ; address of the string containing the hostname ld de, ip_buffer ; pointer to 4 bytes of memory to hold the IP address ld ix, GETHOSTBYNAME ; call the gethostbyname function call IXCALL jr c, .error ; If carry was set, there was an error.
As in the functions covered in tutorial 2, gethostbyname will return with the carry flag reset when it succeeds. If it fails, it returns with carry set, and the A register contains the error code. The HL register pair points to the string containing the hostname we want to look up, or the dotted decimal representation of the IP address. If you pass a dotted decimal string, gethostbyname doesn't actually do any lookup - it just converts it to the system representation, a 4 byte big endian value. The register pair DE should contain the address of a 4 byte buffer for this value.
The C example follows the BSD socket library in form, and it's used like this:
struct hostent *he; ... he=gethostbyname("spectrum.alioth.net"); if(!he) { printk("Could not look up host!\n"); return; }
On success, the C gethostbyname() call returns a pointer to a hostent structure, containing the address. Note that the pointer returned is to some statically allocated storage - so if you're going to make multiple calls to gethosbyname, be sure to copy the values you want to keep! This is true of gethostbyname() on Unix and Windows systems too.
If the C gethostbyname() call fails to find a host, it returns a null pointer.
Using the data returned by gethostbyname() will be covered below, as we cover connect.
Open a socket
Just as in the server example, a socket needs to be opened. The operation is exactly the same as for a server - but here's the code snippet again - in assembler:
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
And in C:
int sockfd; sockfd=socket(AF_INET, SOCK_STREAM, 0);
Just as in the server example, all this has done is allocate the resources associated with the socket - the socket can't yet be used for communication. For that, we need to use connect.
Connect to the remote host
The assembly version of connect takes three parameters - as you may expect by now, the socket file descriptor that was returned by the socket call, the IP address to connect to and the port number to connect to. All connections to a remote host need both an address and port. In this example, we'll be connecting to whatever address gethostbyname resolved for the string "spectrum.alioth.net", and port 80 - to connect to the HTTP server at that address.
Connect can fail for several reasons. The most common are:
- Connection refused - this is generally because nothing was listening on that port.
- Timeout - The remote host didn't respond within a timely period - it may be down, or the port may be blocked by a firewall, or a myriad of other reasons.
If connect returns successfully, you now have a TCP stream established to the remote host, and you can send and receive data on the socket - it's ready for use. Connect is the only call you need to make a socket useful for a client program.
In assembler, you call connect like this:
ld a, (v_sockfd) ; get the socket file descriptor into the A register ld de, ip_buffer ; The 4 byte big endian IP address that gethostbyname returned ld bc, 80 ; port 80 ld hl, CONNECT ; and call connect call HLCALL jr c, .error
The arguments passed are the socket's file descriptor as returned earlier by the socket call, in A. DE contains the memory address of where the IP address is stored. The BC register pair is set to the port that we want to connect to. If connect fails, the carry flag is set and A contains the error code.
If you're using C, you use connect() as follows:
struct sockaddr_in remoteaddr; .... remoteaddr.sin_port=htons(80); /* connect to port 80 */ remoteaddr.sin_addr.s_addr=he->h_addr; /* IP address returned by gethostbyname */ if(connect(sockfd, &remoteaddr, sizeof(sockaddr_in)) < 0) { sockclose(sockfd); /* close the socket */ printk("Connect failed!\n"); return; }
As you can see, connect takes a sockaddr_in structure - i.e. an internet address structure. Two members need to be set - the port, and the IP address. The htons() macro you see is actually a no-op, but it's good practise to include it (it won't bloat your code, it's just a macro). The htons() macro is used to convert a 16 bit number from machine byte order (little endian) into network byte order (big endian), but the underlying ROM code actually does this. The IP address is stored in the struct hostent's h_addr member. If connect() fails, it returns a negative value. Don't forget to close the socket when cleaning up from an error!
Sending and receiving data
This is exactly the same as in the server example, but to recap - the send and recv routines are used. In assembler, here's the snippet from the example simple client program:
; Now send some data - GET / HTTP/1.0 which will elicit a response. ld a, (v_sockfd) ; Get the socket file descriptor ld de, STR_send ; String to send ld bc, 18 ; which is 18 bytes long ld hl, SEND ; to be passed to the ROM SEND routine call HLCALL jr c, ERROR ..... ld a, (v_sockfd) ld de, resp_buffer ; the buffer we want to fill. ld bc, 1024 ; up to 1K at a time ld hl, RECV ; and use the ROM's RECV routine call HLCALL jr c, ERROR
For brevity, both send and recv are shown in use here. Our simple client example sends the string 'GET / HTTP/1.0\r\n\r\n' then immediately calls recv to wait for the result, returning up to 1024 bytes of the response.
The C code to do the same looks like this:
/* Send 'GET / HTTP/1.0\r\n\r\n' to the remote host */ bytes=send(sockfd, txdata, strlen(txdata), 0); if(bytes < 0) { printk("Send failed\n"); sockclose(sockfd); return; } /* Get the response - use 1 byte less than the buffer so we can guarantee to be able to null terminate it for printing */ bytes=recv(sockfd, rxdata, sizeof(rxdata)-1, 0); if(bytes < 0) { printk("recv failed\n"); sockclose(sockfd); return; }
If you compare this with the server code, you'll notice it's really no different - one a TCP socket is open, it works the same way whether you are the server or the client.
Close the socket
Just as in the server example, you need to close the socket when you're done with it, or before your program exits - otherwise it won't get closed until the machine is reset. To recap, in assembler:
ld a, (v_sockfd) ; Get the socket fd ld hl, CLOSE ; and close it. call HLCALL
And in C:
sockclose(sockfd);
In summary
Client communication is very simple - you need to look up the IP address of the remote host you want to connect to with gethostbyname, open a socket, and use connect to make contact with the remote host. Data is then sent in the normal way, using send and recv. Once the socket is no longer needed, it's closed with close (or sockclose, if you are using C).
Next, we'll take a look at UDP.
Spectranet: Tutorial 4 - Datagram communication.
Further reading
The reference manual for the functions used by this tutorial: