@DATABASE AmigaMail @NODE MAIN "VIII-31: A Shared Socket Library Server and Client" @TOC "Cats_CD:Amiga_Mail/Table_of_Contents/VIII" by John Wiederhirn and John Orr Wednesday is gyro day in CATS, so we have to go out to lunch and eat gyros. Because David the Engineer used to work in CATS, we are morally obligated to bring him along. Like the rest of the engineers, David is usually working very hard and doesn't notice what time it is, so we have to remind him that it's time to leave for lunch. Unfortunately, David can't hear us yelling to him in the Engineering department, so we needed an alternate way to tell David that is was time for lunch. The only logical conclusion was to write a program that uses the Shared Socket Library to make a requester pop up on David's Workbench screen telling him it is lunch time. To write a network application for the Shared Socket Library you will need two things: 1) An understand the material in the article "@{"Developing Network Applications for the Amiga" link Cats_CD:Amiga_Mail/VIII-17/NetApps.txt/MAIN}" from the January/February 1992 issue of Amiga Mail. 2) The Shared Socket Library include files and Autodocs (which are on the Network Developer's Disk and the @{"Denver/Milano" link cats_cd:devcon_disks/devcon_91/contents_91.2/Network!Socket} 1991 Devcon disks) This application has to be broken into two pieces, a client and a server. The client program will send out the notes. On Amiga A, the user runs the client (SendNote), passing it a note string (like "David, it's time to eat") and a machine address for Amiga B. The client then sends a note request off to the note server (ShowNote) on Amiga B. When the note server gets the note request, it pops up an EasyRequest containing the note on Amiga B. The server waits for the user to click an "OK" gadget, then sends back an acknowledgement to the client. There are a couple of decisions to make about the application before coding anything. First, there are two transport protocols to choose from: TCP and UDP. To make things easier, this application uses TCP because it is a reliable protocol, so the application doesn't have to worry about making sure data makes it across the network. The application uses a client/server model, but there are two of those models to choose from: iterative and concurrent. Because this application does not need to handle more than one request at the same time, the iterative server is a better choice. This also makes coding easier. @{" The Application Protocol " link VIII-31-1} @{" The ShowNote Server Application " link VIII-31-2} @{" Listening for Network and Amiga Events " link VIII-31-3} @{" Identifying Network Events and Talking to the Client " link VIII-31-4} @{" Starting the ShowNote Server " link VIII-31-5} @{" The SendNote Client Application " link VIII-31-6} @{" Resolving the Target (Host) Address " link VIII-31-7} @{" Locating the Server Port and Connecting to It " link VIII-31-8} @{" Starting the SendNote Client " link VIII-31-9} @{" Note.h " link Cats_CD:Amiga_Mail/VIII-31/Note.h/MAIN} @{" SendNote.c " link Cats_CD:Amiga_Mail/VIII-31/SendNote.c/MAIN} @{" ShowNote.c " link Cats_CD:Amiga_Mail/VIII-31/ShowNote.c/MAIN} @ENDNODE @NODE VIII-31-1 "The Application Protocol" The note program now needs an application protocol for sending information between the client and server. When the client sends a request message to the server, it needs to send the note string. It also would be nice to supply the text for the buttons that will pop up in the requester. On the return trip from ShowNote to the SendNote client, the protocol only needs to define some return codes so the client can tell if the request worked and which button on the requester the remote user clicked. Because the application needs to send different types of packets (one type containing a message request, the other type containing the response from the server), the protocol needs to have a way to specify a packet type. The following structure is the packet that ShowNote and SendNote send back and forth to each other: struct NetNote { int nn_Code; int nn_Retval; char nn_Text[200], nn_Button[40]; }; On the trip from client to server, the nn_Code field is the message request packet type (NN_MSG), nn_Text is the note for the remote user, and nn_button is the text for the EasyRequest buttons. On the return trip, nn_Code is either NN_ACK, if there was no error, or NN_ERR, if there was an error. If there was no error, nn_Retval contains the number of the EasyRequest button that the user selected. In this application, the same packet is passed back and forth for both legs of the trip (client to server and vice versa). If the application required sending large chunks of data in one direction and small chunks of data in the other direction, it would be a good idea to use packets of different sizes. Otherwise, if the application used only one packet size, the size of the packet would be huge compared to the size of the small chunk of data, which would unnecessarily loading the network. @ENDNODE @NODE VIII-31-2 "The ShowNote Server Application" Although the Amiga's Shared Socket Library is meant to be compatible with the Unix socket implementation, there are a couple of Amiga-specific quirks that an Amiga networked application has to take care of. First, an Amiga application has to open the Shared Socket Library (socket.library). Before calling any other functions in socket.library, the program has to call the socket.library function, setup_sockets(). @{"ShowNote.c" link Cats_CD:Amiga_Mail/VIII-31/ShowNote.c/MAIN} code has almost all of the network intialization material in the SS_Init() function. /* ** Attempt to open socket library and initialize socket environment. ** If this fails, bail out to the non-returning AppPanic() routine. */ if (SockBase = OpenLibrary("inet:libs/socket.library",0L)) { setup_sockets( 3, &errno ); } else { AppPanic("Can't open socket.library!",0); } Unlike most libraries, socket.library does not go in LIBS:. Instead, it goes with the network-specific support files, in INET:, in a LIBS directory. This location needs to be hardcoded into the application. Next, create a socket: /* ** Open the initial socket on which incoming messages will queue for ** handling. While the server is iterative, I do it this way so that ** SIGBREAKF_CTRL_C will continue to function. */ int snum; if ((snum = socket( AF_INET, SOCK_STREAM, 0 )) == -1) { AppPanic("Socket Creation:",errno); } and figure out what the well-known port number of the server is. For testing purposes, SendNote and ShowNote use a hard-coded port number (8769). If the application was installed on a network, each machine that ran the server would need an entry for the "note" service in the inet:db/services file. In that case, both the server and the client would have to use getservbyname() to find the port number of the "note" service. The server then needs to build a sockaddr_in structure describing itself. The sockaddr_in contains all the information needed to map an initialized socket to a specific transport address on the Internet (thus the _in suffix, other network protocols have different suffixes). Before the socket can be assigned an Internet transport address, it needs to be initialized: struct sockaddr_in sockaddr; memset( &sockaddr, 0, len ); /* clear sockaddr */ sockaddr.sin_family = AF_INET; sockaddr.sin_port = 8769; sockaddr.sin_addr.s_addr = INADDR_ANY; Next, bind the socket to the well-known address in sockaddr. if ( bind( snum, (struct sockaddr *)&sockaddr, len ) < 0 ) { AppPanic("Socket Binding:",errno); } /* ** Okay, the socket is as ready as it gets. Now all we need to do is to ** tell the system that the socket is open for business. */ listen( snum, 5 ); The call to bind() takes the socket identifier and sockaddr_in structure and assigns a unique network identity (a transport address) to the socket, making it visible to the network. The listen() call tells socket.library the socket is ready to receive incoming messages. The '5' in the listen() call tells the system the size of the connection request queue. A connection request that a client sends to the server's socket goes into this queue and waits for the server to process it. If requests arrive at the server's socket faster than the server can process them, the queue fills to capacity. While the queue is full, the system rejects any other incoming connection requests. ShowNote uses 5 because that is the maximum allowed by Berkeley Sockets. @ENDNODE @NODE VIII-31-3 "Listening for Network and Amiga Events" Once back in the main() routine, the application is almost ready to start processing network events. The only thing remaining is to decide what kind of events the server needs to hear about. Most applications will need to be aware of both network and local Amiga events, which means separate masks need to be set up for both types of events. On the network side, the mask needs to contain information on which sockets need responses. /* First, prepare the various masks for signal processing */ fd_set sockmask; FD_ZERO( &sockmask ); FD_SET( socket, &sockmask ); The sockmask variable will be used as a template to indicate what network events the application notices. Since sockmask came off the stack and contains garbage, the FD_ZERO() call clears all its network signal bits. The FD_SET() call sets the mask to listen for events relating to the socket that the server created earlier. Everything is prepared, so the next step is entering the event loop itself. long umask; fd_set mask; while(1) { /* ** Reset the mask values for another pass */ mask = sockmask; umask = SIGBREAKF_CTRL_C; /* ** selectwait is a combo network and Amiga Wait() rolled into ** a single call. It allows the app to respond to both Amiga ** signals (CTRL-C in this case) and to network events. ** ** Here, if the selectwait event is the SIGBREAK signal, we ** bail and AppPanic() but otherwise its a network event. */ if (selectwait( 2, &mask, NULL, NULL, NULL, &umask ) == -1 ) { AppPanic("CTRL-C:\nProgram terminating!",0); } Before an event occurs, the mask variable tells selectwait() which network events the server wants to receive. After an event occurs, the mask variable indicates which socket triggered the network event. The sockmask variable is used to reset mask back to its original mask value at the top of each pass through the event loop. In addition to waiting on network events, selectwait() also waits on Exec signals for the current task. For this example, the only Amiga event the server cares about is a Ctrl-C break (SIGBREAKF_CTRL_C). The selectwait() function has a simple purpose, but due to the wide scope of network events, the function has a myriad of parameters and configurations. This example uses the bare minimum of what's possible using selectwait(), and many network-friendly applications will be able to get by using only a small subset of selectwait()'s potential. In this case, the network event set is passed in mask, and the Amiga event mask is passed in umask. If selectwait() returns with a value of -1, it means the Amiga event mask was the trigger. Otherwise, a network event caused the function to return. This example is simple enough that a Ctrl-C interrupt can be handled rather easily. @ENDNODE @NODE VIII-31-4 "Identifying Network Events and Talking to the Client" If selectwait() returned as a result of a network event, then a bit more detective work is needed to determine which socket caused the event. The example only has to watch a single socket, so if that socket wasn't the cause, an error occurred. In more complex servers, the server might have to check each of several sockets using the FD_ISSET() macro to determine which one caused the event. There is also the possibility that more than a single event came in at the same time, so an application with many active sockets needs to take into account the possibility of multiple true results from FD_ISSET(). if (FD_ISSET( socket, &mask )) { HandleMsg( socket ); } else { AppPanic("Network Signal Error!",0); } The FD_ISSET() macro checks if there was activity on a socket by checking if the socket's bit is set in the socket mask passed to selectwait(). In this code, there is only a single active socket, so if that socket isn't the trigger then there was an error. When a client tries to talk with a server, it first attempts to get a connection between itself and the server. In this example, the server application returns from its selectwait() with that socket identified in the mask variable. The server has to accept the connection: sockadd_in saddr; int len; if (!(nsock = accept( sock, (struct sockaddr *)&saddr, &len ))) { AppPanic("Accept:",errno); } When the server calls accept(), the function attempts to form a connection with the client. If it can do so, it returns a new socket identifier. The new socket identifier corresponds to a new socket where the all future communication between the client and server will occur. This allows concurrent servers to use an existing socket to establish new connections while carrying on other private client-server conversations. The accept() function also passes back a sockaddr_in structure describing the client. In this example, the server uses this address to figure out the client's host name: struct hostname *hent; struct in_addr sad; char *dd_addr, *hname, rname[80]; /* ** Get the internet address out of the sockaddr_in structure and then ** create a dotted-decimal format string from it. */ sad = saddr.sin_addr; dd_addr = inet_ntoa(sad.s_addr); /* ** Use the internet address to find out the machine's name */ if ( !( hent = gethostbyaddr( (char *) &sad.s_addr, sizeof(struct in_addr), AF_INET ))) { AppPanic("Client resolution:\nAddress not in hosts db!", 0 ); } hname = hent->h_name; Right now, if the server cannot identify the client's machine, it terminates with an error. While this may seem a bit drastic, it does prevent anonymous messages from being sent across the system. Coding a secure client and server requires more complex protocols and involves many other issues which are beyond the scope of this article. After doing a little formatting of the address and name information, the HandleMsg() function begins the actual process of communicating with the client. int nsock; struct NetNote in; /* ** Okay, now the waiting packet needs to be removed from the connected ** socket that accept() gave back to us. Verify its of type NN_MSG and ** if not, set return type to NN_ERR. If it is, then display it and ** return an NN_ACK message. */ recv( nsock, (char *)&in, sizeof(struct NetNote), 0 ); if (in.nn_Code == NN_MSG) { DisplayBeep(NULL); /* DisplayBeep() to get the user's attention */ DisplayBeep(NULL); retv = DoER( rname, (char *)&in.nn_Text, (char *)&in.nn_Button ); in.nn_Code = NN_ACK; in.nn_Retval = retv; } else { in.nn_Code = NN_ERR; } The recv() function removes a struct NetNote sized amount of data from the socket nsock and places that information in a buffer. Once the message packet is in the buffer, the server verifies it is a proper packet by checking the nn_Code. The checking isn't really necessary since the example uses a reliable protocol (TCP) and there is only one type of packet the server can receive. After checking the packet, the server prepared the nn_Code field for the return trip to the client. If there was something wrong with the packet, the server sets nn_Code to NN_ERR, otherwise the server sets nn_Code to NN_ACK. If there was no error, the server extracts the message and button text from the NetNote packet. The server passes them plus name and address information for the client to the DoER() routine. The DoER() routine creates a system EasyRequest, displays it, and returns a value which corresponds to the button which was pressed. The return information on which button was pressed is encoded into the nn_Retval field of the NetNote packet, which is then ready to be sent back to the client. The prepared packet is sent back to the client using the send() function: /* ** Having dealt with the message one way or the other, send the message ** back at the remote, then disconnect from the remote and return. */ send( nsock, (char *)&in, sizeof(struct NetNote), 0 ); s_close( nsock ); The HandleMsg() function then closes nsock using s_close(). The packet protocol between the client and server in this application is defined so there are no cases where the client would send another message, so it can close the socket and break the connection. The acknowledgement/error packet will arrive at the client regardless of whether the connection is still active or not, at least under TCP/IP. HandleMsg() returns to the main() routine and event loop. Once back in the main event loop, the server will continue connecting and responding to client messages until it receives a Ctrl-C interrupt or an error condition occurs. @ENDNODE @NODE VIII-31-5 "Starting the ShowNote Server" The ShowNote server should run as a backround CLI process. To start it, type the following line at a CLI prompt: run >nil: shownote You can start up the server when you boot your Amiga by adding that line to s:user-startup. @ENDNODE @NODE VIII-31-6 "The SendNote Client Application" The client application, SendNote, sends a message for a server to pop up on its display. While the server is an Intuition program in that it uses a requester to display the messages and get responses, the client (SendNote) is strictly a CLI-based application. SendNote parses its command line using the AmigaDOS 2.0 ReadArgs() call. For more information on ReadArgs(), see the AmigaDOS Manual, 3rd Edition from Bantam, the article "@{"Standard Command Line Parsing" link Cats_CD:Amiga_Mail/II-27/readargs.txt/MAIN}" from the May/June 1991 issue of Amiga Mail, or the DOS library includes and Autodocs. There is very little difference in the application-wide setup of sockets between the client and server. Both must open socket.library and call setup_sockets() to initialize the socket environment. One minor difference is the number of sockets the client initializes. The client only needs a single socket to establish a connection to the server, where the server requires at least two (one for receiving connection requests and one for talking to a client). The FinalExit() routine is both the client's error handler and its shutdown routine. Since the client doesn't need to maintain so much operating information, the shutdown routine is rather simple, and can be used for both errors and normal termination. @ENDNODE @NODE VIII-31-7 "Resolving the Target (Host) Address" The client gets a string back from the ReadArgs() call which contains the hostname in either dotted-decimal notation or ASCII form. The clint needs to convert that string to a usable form. struct sockaddr_in serv; struct hostent *host; char *hostnam, *text, *button; /* ** First we need to try and resolve the host machine as an IP/Internet address. ** If that fails, fall back to seaching the hosts file for it. Later versions of ** gethostbyname() may use DNS to find a host name, rather than searching the hosts file. */ bzero( &serv, sizeof(struct sockaddr_in) ); if ( (serv.sin_addr.s_addr = inet_addr(hostnam)) == INADDR_NONE ) { /* ** Okay, the program wasnt handed a dotted decimal address, ** so we check and see if it was handed a machine name. */ if ( (host = gethostbyname(hostnam)) == NULL ) { printf("Host not found: %s\n",host); FinalExit( RETURN_ERROR ); } /* ** It does indeed have a name, so copy the addr field from the ** hostent structure into the sockaddr structure. */ bcopy( host->h_addr, (char *)&serv.sin_addr, host->h_length ); } After clearing out the serv sockadd_in structure, the client tries to convert the host name string (hostnam) it got from its command line from dotted-decimal to an IP address block using the inet_addr() function. If this fails, the server treats the string hostnam as an ASCII string containing a host name, and tries to get a normal IP address using gethostbyname(). This will search the hosts file (inet:db/hosts) for a matching entry. Future versions of gethostbyname() may use DNS (domain name system), which allows gethostbyname() to ask a server for host information rather than looking it up in a hosts file. If it is successful, gethostbyname() returns a pointer to a hostent structure. It requires a little work to to convert this hostent structure to a sockaddr_in (IP socket address) structure. There is a sockaddr structure embedded inside the hostent structure which can be used as a sockaddr string in this case. The call to bcopy() copies that embedded sockaddr structure into the client's sockaddr_in buffer. @ENDNODE @NODE VIII-31-8 "Locating the Server Port and Connecting to It" The next step is to find the server's port number. In the case of this example, the port name is hardcoded into the server and client. A real networked application should have an entry for the "note" service in the inet:db/services file. The code below finds the port number in the client machine's inet:db/services file. struct servent *servptr; char servnam[] = "note"; if ((servptr = getservbyname( servnam, "tcp" )) == NULL) { printf("%s not in inet:db/services list!",servnam); FinalExit( RETURN_ERROR ); } serv.sin_port = servptr->s_port; /* ** This tells the system the socket in question is an Internet socket */ serv.sin_family = AF_INET; Since the client and server are running on top of an IP (Internet Protocol) system, the client needs to specify AF_INET in its call to socket() just as the server did. /* ** Initialize the socket */ if ( (sock = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) { printf("socket gen: %s\n", strerror(errno)); FinalExit( RETURN_ERROR ); } The client socket is initialized and ready. Because the client knows the IP address of the server's machine and the service number of the "note" service, the client knows the well-known transport address so it can attempt to establish a connection with the server: /* ** Connect the socket to the remote socket, which belongs to the ** server, and which will "wake up" the server. */ if ( connect( sock, (struct sockaddr *) &serv, sizeof(struct sockaddr) ) < 0 ) { printf("connect: %s\n", strerror(errno)); s_close( sock ); FinalExit( RETURN_ERROR ); } The connect() call contacts the server across the network and attempts to form a connection. There are many things which can go wrong at this point, and any return less than zero indicates an error condition. If an error does occur, the socket.library function strerror() converts the error number in errno into a readable error message, which is then displayed by the client. In case of an error, the client already has a socket open, so it then must close the socket and terminate using the application's error handler, FinalExit(). Once the connection is established, the client needs to prepare the note request packet. Since the call to ReadArgs() has already parsed everything, the client only has to copy the strings into a NetNote structure: struct NetNote out; out.nn_Code = NN_MSG; strcpy( (char *)&out.nn_Text, text ); strcpy( (char *)&out.nn_Button, button ); The client has filled in all the relevant fields of the NetNote structure, so it is ready to send. The server will fill in the nn_Retval field before passing it back to the client, so the client doesn't need to fill it in for the client-to-server leg of the trip. All that remains is to transfer the packet across the network: send( sock, (char *)&out, sizeof(struct NetNote), 0 ); printf("\nMessage sent to %s...waiting for answer...\n", hostnam ); /* ** Wait for either acknowlegde or error. */ recv( sock, (char *)&out, sizeof(struct NetNote), 0 ); Now the client has to wait for the server to respond. This is one of the few points where the client can hang forever. If the server receives the message, and never replies back, the client will never stop waiting for a reply. A real application would time-out if the server didn't respond within a certain time interval. One way to do this is using a selectwait() which breaks when triggered by a timer.device event. We'll leave that as an exercise. The call to recv() waits until the server sends back a reply. Once the client receives the reply, the client has to check the NetNote structure's nn_Code field. If it's NN_ERR, an error occurred, and the client terminates through the FinalExit() handler. If an NN_ACK packet comes back from the server, the nn_Retval field contains the number of the button the user pressed on the ShowNote requester. The server gets the button number directly from the EasyRequest() function that the server uses to pop up the requester. The buttons on the requester are numbered from 1, increasing left to right, except for the rightmost button, which is button zero. If there is only one button, that button will return a zero (it's the furthest right). After the client gets the packet back from the server, the client's task is complete. It only has to clean up after itself. @ENDNODE @NODE VIII-31-9 "Starting the SendNote Client" The SendNote command has the following template, and uses the AmigaDOS 2.0 ReadArgs() call to parse the CLI command line: SENDNOTE HOST/A,TEXT,BUTTON The HOST argument is the address of the server machine. It is either a dotted-decimal notation address or its ASCII host name from the inet:db/hosts file. The TEXT argument is the string the server will pop up in its requester. The BUTTON argument is the ASCII string that will appear in the requester button. For example: sendnote 123.123.123.1 "I've fallen and I can't get up!" "Help me!" displays a requester on the machine whose address is 123.123.123.1, with the text of the requester saying "I've fallen and I can't get up!" and a button labeled "Help me!". If that machine is not running the server, then SendNote just displays an error message and terminates. If the inet:db/hosts file on the machine running SendNote has an entry that gives 123.123.123.1 the name "foo", then sendnote foo "I've fallen and I can't get up!" "Help me!" will have the exact same effect as the previous form of the command. There are also default values for both the TEXT and BUTTON arguments. If the user doesn't provide an argument for BUTTON, then ShowNote defaults to "OK". If both TEXT and BUTTON are missing, the TEXT argument defaults to "==PING!==". To give the user on the server machine several buttons to choose from, put several strings in the BUTTON argument delimited by `|` characters, like this: sendnote foo "Is it time yet?" "yes|no|maybe" That's all there is to the client application. Because the socket.library routines take care of so much of the network "nitty gritty", the application code can deal with networks in a straight-forward and simple manner. Two applications (client and server), each around 8K bytes in size, are able to implement a complete and working intranetwork communication system (albeit a very simplistic one). If you are interested in doing more serious development using the AS225 software (and thus IP and TCP/UDP), you should take a look at more advanced texts. A good start is Unix Network Programming by W. R. Stevens (Prentice-Hall, ISBN 0-13-949876-1). It covers many aspects of network protocol and application design, as well as explaining quite a bit about "Berkeley Sockets" which socket.library implements. @ENDNODE