Plugging Into OS/2 Socket Programming - Part 3/3
Written by Edward Boykin
Welcome to my third article on OS/2 socket programming. We are finally going to look at a more advanced client-server setup. This month, I will discuss only the client part and treat the server only as a 'black box' so that you may concentrate on one half of the set up. I have included only the client source and executables, but in order to make the client usable I wrote it so it would work with an internet "chat" type server (not IRC) that already exists. The server to be connected with is hard-coded in the client source. Next month I will update the client so you can connect to the server I am writing.
For some of you who have been unable to work with sockets because of the cost of the IBM TCP/IP toolkit, I have some good news: it has come to my attention that the Warp Connect CD-ROM contains all the headers, libraries and DLLs of the toolkit; however, it does not contain documentation. If you own this CD then do a search for the "toolkit" directory and you will find this zip file there. (Those of you who do not have Warp Connect are still out of luck, unfortunately.)
On to the show...
The Select() Function
In my study of socket programming, I found that only one function really ever confused me and that was the select() function. There are two versions of select() in the OS/2 socket SDK: one version is the same as the BSD socket call which uses bitmasks and such to set up the function; the other is IBM's version of the function with the same name. I found the former to be too difficult to use so I opted to use the latter instead. The select() function is used to check one or many socket descriptors for readability, writability, and exceptions. The function looks like this:
int select ( int *socks, int noreads, int nowrites, int noexcept, int timeout );Figure 1: IBM's version of the select() function.
The first parameter is an array which contains the socket descriptors you wish to perform the select call on. When creating this array, place all of the sockets you wish to check for readability first, then for writability, and finally for exceptions. So, you could theoretically have an array of three integers with the same integer, or socket descriptor, in all three places, meaning that you want to see if you can read from and write to the socket and check for any socket exceptions. The next three parameters tell select how many sockets in that array are for reading, writing and exception checking. The final parameter is a timeout value. This value is in seconds and determines how long the select call will block if none of the sockets check out. If you make this value 0 select will not block, and if you make it -1 select will not timeout.
The Anatomy of a Client
The client I have provided for this article is a very simple, PM based TCP packet client. In a nutshell, it connects to a server and sends and receives packet to and from the server. The server allows multiple client connections and when one client sends a message the server relays that message to all of the other connected clients. This is what allows the network "chatting" to take place.
A good client application should have at least the following capabilities. Of course, it needs to be able to send and receive messages quickly with these message being of some standardized protocol. It should also be able to connect to any server which supports the particular chat protocol. (This ability will be available in the next article.) The client should have an easy to use user interface. And, finally, the client should be easily enhanced.
The User Interface
I mentioned before that the client was to be PM based. I did this to make it easier to handle user input and server output. The interface for the sample client is a simple one. It contains two buttons, a read-only multiple line editor, and an entry field. One button is for connecting to the server and the other for disconnecting while the MLE is for server output and the entry field is for user input. It is a crude, but effective, interface for our needs.
A more advanced client interface might allow for drag-n-drop font changes or dynamic font changing based on the dimensions of the main window, etc. These things, however, are for the future.
The Packet Protocol
The packet protocol I have selected is one which is used by the "Internet Citizens Band", or ICB, servers for years. A single packet to or from the client contains three parts and has the form: "l p m", where l is a single byte which specifies the total length of the packet including itself, p is the packet type indicator, and m is the packet data. The current client only supports the login packet, a, and the open message packet, b, for the packet type.
Even though we have a limited number of packet types right now, by including this byte of information we are not limited to just these packet types - the "Internet Citizens Band" server actually supports many other packet types. I have limited this client only for simplicity's sake. The single byte of the length does pose one restriction, however, and that is that the total length of message can only be 253 bytes since the length byte includes the protocol type and itself in that length.
Sending packets is very simple. I have defined a user message called SM_SEND which, when sent to the send/receive thread, tells the thread that there is an outgoing message. The message contains a pointer to the already prepared packet in mp1.
Receiving packets is accomplished in almost the same way as sending them. When the send/receive thread loop receives a packet, it is broken down by removing the l (size) and p (packet type) from the string and then runs through a case statement to determine the packet type. Since the client only supports the b (open message) packet type, this is the only case we look for. We also define a default which takes any of the other packets from the server and ignores them. Once it is determined that a complete open message packet has been received the thread then sends another user message, SM_RECV, to the user interface thread. Once again mp1 contains a pointer to the message string.
Enhancing the Client
It should be obvious how the client is easily enhanced in the last few sections. I have used a packet protocol which allows for a byte to specify a different kind of packet. This could include private messages, specially formatted open messages, or just about anything else you can think of. We could create a broken up message which could be any packet type that was over 253 bytes. The packet could then be broken into 253 byte chunks and sent separately. The possibilities are almost endless here. Imagination is the only limit.
The Operation of CLIENT.EXE
Now that I have described what goes into the client program I will begin to put it all together. As I said before, the client contains two threads. I will call this the user interface thread and the send/receive thread. The user interface thread handles the user input and server output and the connection of the client to the server. The send/receive thread handles the sending and receiving of message packets to and from the server. The two threads will talk to each other via user defined messages.
Take a look at Figure 2. I have diagrammed the basic execution layout of the client program there. I will start with the top of the diagram in the user interface thread and work may way over.
Figure 2: Execution path of client.
Initializing the Client Application
Upon initial client startup the main window is displayed. You must supply a nickname on the command line when you start CLIENT.EXE. At this point the user should press the "Connect" button. The connect button send a WM_COMMAND message with the button id, ID_CONNECT, as one of its parameters which the window procedure handles by calling a function setupSockets(). SetupSockets() will initialize the socket environment and create the socket that the client will use to communicate with the server. If this all occurs normally then setupSockets() will return the socket descriptor that was created. If all does not go well then it will return 0 or a negative number. If it is a negative number then this is the error code multiplied by -1.
Assuming we had a good socket returned then WinPostMsg() is used to send a SM_SOCKETGO message. The basic structure is in the code to add some kind of error notification to the user but for times sake I did not include anything to that effect.
Connecting to the Server
The SM_SOCKETGO message is used to tell the client that the socket environment is all set up ready to go and we need to connect to the server now. The message will be handles by calling the connectToServer() function. We send to this function our socket descriptor, the server name, and the server port. I have defined, in client.h, the server to be icb.sjsu.edu and the port to be 7326. Leave these be until next month when I will have the server ready for you.
ConnectToServer() does just that - it connects to the server. It sets up the necessary hostent struct which is used by the socket function connect() to determine the place we want to connect to. Notice we are using the gethostbyname() function in order to determine icb.sjsu.edu's interment IP address. Upon a successful connection the function loginToServer() is called. The parameters to this function are the socket descriptor and your chosen nickname.
LoginToServer() gives us our first look at sending and receiving a message packet. This is the only time that message will be sent or received outside of the send/receive thread. The comments at the beginning of the function describe the format of the login packet. Notice that fields are separated by a ^A or \001 character. This is part of the standard message packet protocol so if you were to create some new message packet type that used fields you would want to use that character to separate them. If you follow through the first couple lines of actual code you will see how I go about assembling the message packet for our login.
The first communication activity that takes place is the reception of the protocol packet from the server. This is the server saying "hi there". Using a bit of string and pointer acrobatics, I take the two fields of this packet and create the connect message to be displayed on the MLE. The last thing to do is to send that login packet that was put together. If all goes well the function will return TRUE; else it will return FALSE to the caller, the connectToServer() function. A TRUE return code will cause a SM_CONNECTED message to be posted; otherwise the message "Unable to Connect to Server" is displayed on the MLE.
The SM_CONNECTED message tells the client that all is ready for normal communications with the server. The message is handled by calling _beginthread() to start up the send/receive thread.
The Send/Receive Thread Loop
We now come to the meat of the client application. It is here that all further communication between the client and the server will take place. The thread basically executes an continual loop until either we shut down or the server shuts us down. This loop has two sections:
The SM_KILL message can also be received. The user defined message is used to tell the send/receive thread to shutdown the socket connection and end the thread. To do this soclose() is called and the Connected variable is set to FALSE.
If there are no messages to read or once all of the messages have been read and processed and no SM_KILL messages were received then the socket will be checked for incoming packets from the server. To do this, select() is called again. I included a timeout value of 5 seconds just to give the receive section a chance to get some data in if there is none at the moment.
If select() returns a ready socket then the first byte is read to determine the length of the incoming packet. Another read is then done to read in just that amount of data specified in the first byte. After checking to be sure we got the correct amount of data in, the packet type is checked in a switch() statement. If the packet type is b, open message, then the packet message is sent to the user interface thread via the SM_RECV thread. If a packet type is received which is not supported then the memory that contains it is freed.
This loop continues indefinitely until the SM_KILL is received or the program is terminated.
The User Interface Thread
The user interface thread, as I said before, is responsible for handling the user input and server output. The whole thread runs off of a standard client window procedure.
The user input part is fairly simple. The message loop captured the WM_CHAR message and looks for the enter or return key to have been pressed. It then queries the text in the WND_INPUT window and if there is text in there it processes the string.
To process the string, the string, open message packet type, and packet length byte is inserted into a 256 byte character string. A null terminator is added to specify the end of the created string. The message packet is then sent to the send/receive thread via the SM_SEND message. The final thing done is to show the user his own message on the output window.
The server output is handled a bit differently. Once the SM_RECV message is received another series of string acrobatics is done to break the fields of the packet up. When we receive an open message packet from the server it contains 2 fields. The first field is the nickname of who sent the message and the seconds is the message itself. Once the packet is broken up it is then displayed on the output window.
Wrap Up and Next Time
Well, we are almost finished. This month I have use a simple client application which allows a chat over TCP/IP networks. Please examine the code and try to understand how I have integrated the socket functions into a PM program and how things like the select() call work. If you have any questions I can always be reached at firstname.lastname@example.org.
Next time I will include a server which you can set up yourself and allow anyone on your network to chat. I will also change the client code so you can specify what server you want to use. Thanks for you time. I hope you have learned a lot.