So you think client-server programming is only for large applications?
Client-server applications are everywhere. Client-server can be defined as a process which provides services to other processes. The client and server can be run on the same machine or on different machines on opposite sides of the world. A non-programming example of a client-server situation is the telephone system. You are the client (or customer) and the central office is the server. By having a telephone connected to the system (and your bill current!) you are subscribing to the services that the central office provides. Requests are made of the server (central office) to place and receive calls. The server also does accounting on each call made or received and handles emergency (911) requests. In this article I will present a simple CB (citizen's band) radio simulator which was written for a class project. The server is written in C and the client is written in Java. I will assume that the reader understands what sockets are and has a rough idea of how they are used.
The specifications for the project were loosely:
Provide a server which can accept multiple simultaneous connections. The server should have a basic command set that will open and close the connection, change the channel and provide a list of current clients on a specific channel.
Provide an “emergency channel” (9) that will broadcast all traffic to all currently connected clients regardless of which channel they are subscribed to.
Client should “come up” on channel 19. This is the channel where CBers meet. They then agree which channel to move to in order to continue their conversation.
The client should display all traffic on the current channel with the handle of the person who sent the message.
The server can be either a single process concurrent or multiple process server (more on this later).
Besides getting extra credit for doing a graphical user interface, I chose Java in order to simplify programming of the client. Java is used specifically for the following reasons:
Portability. I was developing code at home on my Linux system and running the code on Suns at school.
Functionality. Not just for web page animations, Java is a very useful programming language. Java is object-oriented and very similar to C and C++. A simple user interface is relatively easy to write.
Threads. Java allows multiple threads (like a background process) of execution. A thread can be launched that will listen for incoming messages. When a message comes in, it is automatically sent to output. We start this thread and forget about it.
Can be run remotely from Netscape (or similar browser). While I have not currently implemented this feature, conceivably, web surfers looking at your page could talk to each other using the CB simulator. There are many restrictions to this which we will go into later.
Using C for the client would require more programming to accomplish the same results. First, some sort of GUI builder like Motif or X-Forms would need to be used. I'm not knocking any of these, but not every system has them and they can be difficult to learn and use. C does not have threads, so all I/O would have to be polled. User input as well as incoming messages would have to be polled and processed accordingly. Without a GUI, some type of command codes would have to be developed for the user to control the client and server. This would probably be very cryptic and difficult to use—not to mention difficult to implement.
I developed the server first, from the specification in Table 1. Messages are fixed length and must not vary from the given format. C handles transmissions well through the use of structures and pointers; basically, you just call a write or read routine, passing a pointer to the data structure, and the bytes come or go without much of a problem. This works fine for C; Java is another story.
Sockets work almost the same in Java as their counterparts in C. Since Java is object-oriented, you must create an instance of the socket object. This is done by a simple line of code:
Socket s = new Socket(hostName,portNumber);
where s is the instance of type Socket and hostName,portNumber are the name of the host and port to connect to. But a socket by itself is not very useful without a data input and data output stream. The code segment below sets up a data input and output stream to talk to the socket:
dis= new DataInputStream(s.getInputStream()); dos= new DataOutputStream(new BufferedOutputStream(s.getOutputStream()));
The output stream is created as a buffered output stream. Data will not be written across the socket unless either Java feels that there is enough data to write, or you force a write—using a flush by using something like: dos.flush(); this calls the flush method on the data output stream. On the reading side of the socket, we can simply go into an infinite loop and poll for data from the server, since the listener is running as a separate thread.
Java has most of the same basic data types as C, with a few exceptions. Java has no unsigned integers, but it does have true booleans. To construct the data packet, use a combination of Integers and an array of 120 bytes for the handle and message fields. The data output and input streams have methods for reading and writing integers and bytes. For example, dos.writeInt(1); would write the integer “1” to the data output stream. Conversely, for (int i=0;i<120;i++) dos.writeByte(buffer[i]); (or dos.readByte(buffer[i]) to read) would write the entire buffer to the socket; dos.flush() will make sure that the data is written now and not delayed. It is important to note that we must write or read all of the data (command, channel, handle and message) to or from the server even if all we want to do is change the channel. The server expects this; otherwise it will hang, waiting for all of the bytes to come or go.
One more obstacle remains. How to get the handle and message data into the proper position in the byte array? In the event handler we create string objects for the message and handle, then call the getBytes() method on the string objects. message.getBytes(0,message.length(),buffer,20); will copy message.length() bytes from the string object message starting at position 0 in the string to the byte array buffer starting at position 20. One thing that is missing in my program is error checking. It would be absolutely necessary in a production program to check and recheck to make sure that you don't overflow the buffer by writing more bytes than the buffer can hold.
The server is a simple single-process concurrent server. Simply put, the server polls each connection, and processes requests in order. An alternative would be to fork a new process for each incoming connection. In this situation the single process server is far simpler (and, after all, the computer can only really do one thing at a time). The basic order of things is:
Create the master socket on the well-known port.
Bind the socket so that incoming requests are directed to the proper place.
Listen for connections.
Accept incoming connections.
The “well-known port” is a port which is known to all clients. All clients can't connect to the same port, so the server “hands off” connection requests to a different port. This is done by the TCP/IP layer, and we don't need to concern ourselves about how. This process is analogous to that of making a phone call to a large corporation's toll free number. Suppose that you wish to call 1-800-257-1234. You are asking the server for a connection on that port (phone) number. This company probably has hundreds of lines, but you would not want to try each of them until you finally got through, so the corporation has set up a rotary on their lines to put connections through to the next available phone line.
TCP/IP sockets work the same way. When a connection is accepted on a socket, a new file descriptor is created. The file descriptor is used as an index to an array of structures. Every client has exactly one unique file descriptor and a slot in the client array. Each array position holds a structure which contains the handle and current channel number. When the server receives a message packet, it looks through the entire array and retransmits the message to all clients who are subscribed to the channel number that the message came from.
Currently supported server commands are:
CB_ON sets the client's channel to 19 and sends a welcome message. It also stores the handle in the client array.
CB_OFF closes the socket and clears the client info from the client array.
SET_CHAN changes the channel of the client in the client array.
WHO_CHAN sends a message containing the handles of all connected clients subscribed to the current channel.
SEND-MESSAGE sends the message contained in the message field of the data packet to each client subscribed to the same channel as the originator. Emergency traffic on channel 9 is also sent to all connected clients regardless of what channel they are subscribed to. As stated earlier, the server must have all bytes in the data packet sent or received at once. It is not possible (with this implementation) to send part of a packet.
My original goal was to make a client that looked like a CB radio panel. This turned out to be too difficult to do with Java; while Java is a good portable programming language, creating a complex user interface is very difficult. I adapted my client from an example in Java in a Nutshell by David Flanagan, O'Reilly & Associates (an excellent book—great for reference). The CB client user interface is very simple. A Connect menu is at the top. From here, the user can quit or ask the server who is on the current channel. The middle window is the message area. Here all messages from other users and the server are listed. The client will print the handle and message from the data packet. The server is responsible for the data in those packets as it will put “System: WHO” in the handle for a WHO request. The bottom field is for entering a new channel. When Java detects activity in the menu bar or channel field, it will call the event handler routine. From here, it determines where the event came from and performs the appropriate processing. The user interface is not much—more a “proof of concept” than anything else—but it does provide much more functionality in fewer lines of code than would be required by an equivalent program written in C.
The big vs. little endian debate has been the topic of many flame wars on the Internet. But what is it? Big and little endian refers to the order of bytes. When moving data around, some systems start with the most significant byte (MSB) and some start with the least significant byte (LSB). Imagine an array of 4 bytes. How do you store or send this array? Would you start at the LSB (little endian) or would you start at the MSB (big endian)? Some hardware does it one way, and some does it the other. Why do we care? If you are writing a client and server in C to run on the same type of hardware, the endian problem doesn't pop up. But if you are using a different language, like Java, to talk to a server written in C, there could be a big problem. Endian problems crop up only when multiple byte data types like integers are sent across the network. Java automatically converts its data to and from network byte order when it sends data through a socket. C, on the other hand, does only as it is told. There are two C system calls, ntohl() and htonl(), which convert data to and from network byte order. Read the man pages for these calls and use them in your C-based servers and clients to avoid endian problems.
Java has some strict security restrictions. An applet can only open a socket to the server on which it was loaded. Applications, on the other hand, are allowed to open sockets to any machine. My client is written as a stand-alone application for this reason. (I don't have access to a web server that will allow me to run my CB server.) There are very few major differences between an applet and an application. An applet extends the class applet and an application extends the class frame. Refer to a book on Java for more specific details.
This project was my first real attempt at client-server programming. I'm hooked! With the basic server written, it is possible to extend the code to do many things. I would like to eventually redesign the user interface to make it look better and be easier to use. Having Linux at home has made the program development process much easier. I was able to use the same tools on both my home system and the Sun workstations at school so a simple recompilation was all that was necessary for the server to run on a Sparc 5. My hope is that someone else will find this work useful. No references could be found in any Java book (I have three) to address this specific application. While client-server applications were available in all of these books, all of the servers were written in Java. Java works well for writing servers, but is not as fast and requires more system resources to run. Every language has its place and Java is no exception. Java is very useful as a client programming language; it's here to stay.
Take a look at the listings:
Java in a Nutshell
David Flanagan (1996, O'Reilly & Associates, Inc)
Java Programming Explorer
Neil Bartlett, Alex Leslie, and Steve Simkin (1996, Coriolis Group Books)
Teach Yourself Java in 21 Days
Lemay and Perkins (1996 Sams.net publishing)
Internetworking with TCP/IP vol III
Comer and Stevens (1993, Prentice-Hall, Inc)
The C Programming Language, second edition
Kernighan and Ritchie (1988, 1978, Prentice Hall)
Various Linux man pages