return to first page linux journal archive
keywordscontents

Java and Client/Server

by Joe Novosel

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:

Why Java?

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:

Why Not C?

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.

A Few Words on Sockets

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

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:

  1. Create the master socket on the well-known port.
  2. Bind the socket so that incoming requests are directed to the proper place.
  3. Listen for connections.
  4. 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:

The Client

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.

Endian Wars

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 and Security

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.

Conclusion

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.

Joe has been an avid computer hobbyist since 1981, when his first computer (Radio Shack Color Computer) had a whopping 4K of memory (including video memory!). He has been using Linux for about two years--since version 1.1.47--and thinks Linux brings back the excitement of his early days in computing. After several years in the electrical trade, Joe decided to return to school and is now a Junior at Georgia Tech, where he pursues a degree in Computer Science. Joe may be reached at jnovosel@cc.gatech.edu.

Take a look at the listings:

References