This series, The Object-Oriented Thought Process, is intended for someone just learning an object-oriented language and who wants to understand the basic concepts before jumping into the code, or someone who wants to understand the infrastructure behind an object-oriented language he or she is already using. These concepts are part of the foundation that any programmer will need to make the paradigm shift from procedural programming to object-oriented programming.
Click here to start at the beginning of the series.
In keeping with the code examples used in the previous articles, Java will be the language used to implement the concepts in code. One of the reasons that I like to use Java is because you can download the Java compiler for personal use at the Sun Microsystems Web site http://java.sun.com/. You can download the standard edition, J2SE 5.0, at http://java.sun.com/j2se/1.5.0/download.jsp to compile and execute these applications. I often reference the Java J2SE 5.0 API documentation and I recommend that you explore the Java API further. Code listings are provided for all examples in this article as well as figures and output (when appropriate). See the first article in this series for detailed descriptions for compiling and running all the code examples.
In the previous column, you covered several advanced techniques about connecting to a database using Java Database Connectivity (JDBC). Storing data to a database is just one of many ways to save data—a concept called data persistence. In this article, you will explore another aspect of object persistence/serialization, specifically how do you move an object across a client-server connection? You may want to store an object on a machine that is connected to your application via a network connection. You will create a very simple client/server application and transfer an object from the client to the server and back.
Designing the Employee Class
Before you delve into the code to implement the client/server application, you must design a class to be sent across the client/server wire. In short, you will instantiate an object that will be marshaled between the client and the server. Remember that when you say marshaled you mean moving the object between two points. Specifically, the object must be decomposed into a form that can be transported across a wire and then rebuilt on the opposite side of the wire.
In this example, you create a class called Employee. The employee is built from the following specifications:
Two attributes:
- private int employeeNumber;
- private String employeeName;
Constructor (set the attributes):
- Employee(int num, String name)
Methods (getters and setters for the attributes):
- public int getEmployeeNumber()
- public void setEmployeeNumber(int num)
- public String getEmployeeName()
- public void setEmployeeName(String name)
This class is not going to do a whole lot. It is really only here so you have something to send across the network. What you want to do with your client/server system is this:
- Instantiate objects of type Employee
- Set the attributes to some values
- Send the object from the client to the server
- Have the server change some of the values
- Send the object from the server back to the client
- Verify that the client has the updated information
The complete code for this simple Employee class is shown in Listing 1.
import java.io.*; import java.util.*; public class Employee implements Serializable { private int employeeNumber; private String employeeName; Employee(int num, String name) { employeeNumber = num; employeeName= name; } public int getEmployeeNumber() { return employeeNumber ; } public void setEmployeeNumber(int num) { employeeNumber = num; } public String getEmployeeName() { return employeeName ; } public void setEmployeeName(String name) { employeeName = name; } }
Listing 1: The Employee Object
An important point worthy of interest here is the way the class is defined:
public class Employee implements Serializable
Note the use of the keyword Serializable. This interface identifies that the class can be serialized. The Serializable interface does not actually contain any methods; it is used as a sort of tag that marks the class as Serializable. In the words of the Java 2 API specification: “The serialization interface has no methods or fields and serves only to identify the semantics of being Serializable.” At this point, this is all you need to know about serialization; however, you will investigate some interesting issues that pertain to Serializable in a future article.
The sole purpose of the Employee class is to contain the employeeNumber and employeeName. Thus, you can create an instance of this class to demonstrate the specifics of sending it across a client/server network. These attributes, along with their getters/setters, are all you need for this purpose.
The Client
You will first take a look at the Client end of the system. You could just as easily start by developing the Server. In this system, the Client will do the work of creating the Employee object. Thus, it makes sense to start with the Client.
Here are the steps needed to properly demonstrate the features of your client.
- Create an Employee object
- Echo the Employee information to confirm the initial values
- Create a socket
- Create an ObjectOutputStream
- Create an InputOutputStream
- Write the Employee object to the ObjectOutputStream
- This where the object is sent to the Server
- Retrieve the updated Employee object from the InputOutputStream
- After the Server changes it
- Echo the Employee information to verify the change in values
- Close the ObjectOutputStream
- Close the InputOutputStream
Creating the Employee Object
The instantiation of the Employee object is the simplest part of the process. To make things as clean as possible, you create a single object so you don’t have to worry about any synchronization. The complete Client.java file can be seen in Listing 2.
For this specific instance, you create an Employee object called joe.
Employee joe = new Employee(150, "Joe");
Make sure that the Client.java file is in the same directory as the Employee.java file. If you decide to use packages, you will have to make sure that your import statements reflect this. You will keep things simple and use the default packaging.
Using the parameters passed to the constructor, the object joe now has an employeeNumber of 150 and an employeeName of “Joe”. This is the object that you will send from the Client application to the Server. Note that both the Client and the Server are separate applications and thus they both have a main() method. To get the client/server system up and running, you will have to create a process for both.
Before you start looking at the networking code, the Client first echoes the values of the new joe object, just so you feel confident that the object was created properly.
System.out.println("employeeNumber= " + joe .getEmployeeNumber()); System.out.println("employeeName= " + joe .getEmployeeName());
Creating the Socket
The first part in connecting to the network is to create a socket using the Socket class. The SDK documentation describes a socket as “an endpoint for communication between two machines”.
To use the Socket class, you must import the java.net package.
import java.net.*;
you create an object called socketConnection using the Socket class. The single line of code is as follows.
Socket socketConnection = new Socket ("127.0.0.1", 11111);
This version of the Socket constructor creates a stream socket and connects it to the specified port (11111) number at the specified IP address (“127.0.0.1”). The SDK API states, “you can specify a port number, or 0 to use any free port”.
Even though there is not much code needed to create a Socket, two important parameters are passed to the Socket constructor. The first parameter is the URL (actually class InetAddress) of the Server you intend to connect to. This is a String, and the specific URL used here is called a loop-back. The loop-back URL (“127.0.0.1”) literally points to the same machine. This is a great technique for testing. With this feature, you can create the Client and the Server on the same machine so that you do not need to have an active network connection to test your applications.
In the next column, I will provide a different URL so that you can test this system over the Internet. In any event, having the option to test the system on a single machine really makes development a lot easier.
The second parameter is an integer and represents a virtual socket that you will associate with this specific client/server application. There is some flexibility with this number; you can vary it. However, the Client and the Server must use the same socket number for this system to work properly.
Creating the ObjectStreams
After the socket connection is made, you need to create a couple of object streams, at least in this example. Again, you will keep it simple so you don’t have to worry about synchronization. You create an ObjectOutputStream, called clientOutputStream. This allows the Client to write the Employee object to clientOutputStream.
Taking a look at the Sun documentation regarding the ObjectOutputStream is helpful: “an ObjectOutputStream writes primitive data types and graphs of Java objects to an OutputStream. The objects can be read (reconstituted) using an ObjectInputStream. Persistent storage of objects can be accomplished by using a file for the stream. If the stream is a network socket stream, the objects can be reconstituted on another host or in another process”.
In essence, you are stuffing the Employee object, in this case joe, into a “pipe” that is connected to a server (that you will construct next). First, you need to associate the object streams with the socketConnection object you created earlier. The code for this is as follows.
ObjectOutputStream clientOutputStream = new ObjectOutputStream(socketConnection.getOutputStream()); ObjectInputStream clientInputStream = new ObjectInputStream(socketConnection.getInputStream());
This is a good example of a Java filter. Java I/O uses filters extensively and this is one of those places. By associating the object streams with the socket you created, actual socket information is “filtered” and, in effect, hidden from you. All you have to do from now on is to read and write to and from the object streams; we don’t have to worry about the socket. This keeps you focused on the layer that you interact with directly. It also allows you to change things like the URL and virtual socket without forcing you to change a lot of code.
Writing the Employee Object to the Network
Once the object streams are created, you can write the Employee object joe to the network. This is accomplished with the following line of code.
clientOutputStream.writeObject(joe);
Notice how simple this code is. All you are doing is using the writeObject() method of the OutputStream class with joe as a parameter. You don’t have to worry about the socket, the URL, or anything about the network connection. The SDK API states that “the writeObject() method is responsible for writing the state of the object for its particular class so that the corresponding readObject() method can restore it”.
At this point, the joe object is loaded into the “pipe” that connects the Client to the Server. If the Server connection is not present, an exception will be generated. In fact, if the Server is not running, exceptions will be generated earlier in the process; you will explore many of these exception conditions.
The joe object is now “pipe” and ready to be retrieved by the Server, which will read it, update it, and then send it back. Once the Server is done with it, the Client can take the object back off the “pipe.”
Retrieving the updated Employee Object from the Network
Remember that this application was designed with simplicity in mind. Thus, once the Client sends the joe object to the Server, it simply waits for the object to come back. This is accomplished by using the following line of code.
joe= (Employee)clientInputStream.readObject();
You are using two separate object streams to do the job. The Client and the Server will share the connections (pipes). Obviously, the output stream for the client will be the input stream for the Server and visa versa. Thus, the Client will put the initial joe object on its output stream. The Server then will take that object off its input stream.
The SDK API states that “the method readObject() of the input stream is used to read an object from the stream. Java’s safe casting should be used to get the desired type.” In Java, strings and arrays are objects and are treated as objects during serialization. When read, they need to be cast to the expected type.
After altering the joe object, the Server puts joe in its output stream. Then, the Client retrieves joe from its input stream. This may seem confusing at first; however, once you run the code it will become clearer. You can think of it as a two-lane highway from one city to another. There is an eastbound and a westbound side—and each can only go in one direction.
Once the Client retrieves the joe object, you print out the attributes of the object so that you can verify that the Server actually did get possession of the object and altered the attributes employeeNumber and employeeName.
System.out.println("employeeNumber= " + joe .getEmployeeNumber()); System.out.println("employeeName= " + joe .getEmployeeName());
Closing the Object Streams
Finally, you need to do some housekeeping. In this case, you will close both the input and output streams with the following lines.
clientOutputStream.close(); clientInputStream.close();
This completes the code for the Client. The only other issue that you should pay attention to is that the code in the Client is incorporated inside a try/catch block. This is required for compilation. As stated earlier, I will cover the various exceptions and how we handle them in later articles. Listing 2 contains the complete code for the Client.
import java.io.*; import java.net.*; public class Client { public static void main(String[] arg) { try { Employee joe = new Employee(150, "Joe"); System.out.println("employeeNumber= " + joe .getEmployeeNumber()); System.out.println("employeeName= " + joe .getEmployeeName()); Socket socketConnection = new Socket("127.0.0.1", 11111); ObjectOutputStream clientOutputStream = new ObjectOutputStream(socketConnection.getOutputStream()); ObjectInputStream clientInputStream = new ObjectInputStream(socketConnection.getInputStream()); clientOutputStream.writeObject(joe); joe= (Employee)clientInputStream.readObject(); System.out.println("employeeNumber= " + joe .getEmployeeNumber()); System.out.println("employeeName= " + joe .getEmployeeName()); clientOutputStream.close(); clientInputStream.close(); } catch (Exception e) {System.out.println(e); } } }
Listing 2: The Client
The Server
With the Client code complete, you now turn your attention to the Server. For obvious reasons, the Client cannot function without the Server. So, before you can demonstrate the Client code, you have to get the Server up and running. It is something like the chicken and egg dilemma—which comes first, the Client or the Server? In reality, you need both for the system to work. However, one thing is for certain; the Server must be started before you can process any Client activities. In fact, the Server can handle multiple Clients, a scenario that you will investigate later.
Here are the steps that need to be completed to properly create the Server.
- Create a reference to the Employee object
- Create a server socket connection
- Create a socket to accept an object from the “pipe”
- Create an ObjectOutputStream
- Create an InputOutputStream
- Read an Employee object
- Change the Employee number
- Change the Employee name
- Write the revised employee object to the output stream
- Close the object streams
Remember that in this system, the Server never actually instantiates an Employee object. It uses the one sent to it by the Client.
Create a Reference to the Employee Object
In the Server, you are going to use an Employee object sent from the Client. To do this, the Server must create a reference to an Employee object. When the object is received from the Client, the reference then “points” to that object. The following code created the reference.
Employee employee = null;
At the time this reference is defined, you set it to null.
Creating the Socket
There are actually two steps to creating the Socket connection for the Server. First, you create an instance of the class ServerSocket.
The SKD API states, “this class implements server sockets. A server socket waits for requests to come in over the network and then creates a server socket, bound to the specified port.”
ServerSocket socketConnection = new ServerSocket(11111);
The difference here is that you do not have to specify a URL, only the virtual socket. In the Client example, you needed to “locate” the Server by using the URL. In the case of the Server, you will read the object directly off the virtual port. The port number must be the same for the Server and the Client.
Once the connection to the virtual port is in place, the Server will wait for an object to “show up” at the port. Before you wait, you write a message to the console indicating the Server is up and running.
System.out.println("Server Waiting");
However, it is not the connection to the virtual port that initiates the “Waiting” state. The following line of code accomplishes that task.
Socket pipe = socketConnection.accept();
According to the SDK API, “the accept() method listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.”
You create a Socket object that you call pipe. Using the accept() method, you now simply wait for something to appear at the virtual socket. That something is the object sent from the Client.
Creating the ObjectStreams
As with the Client, you create two object streams, one for the input and one for output. These are the complements of the streams created in the Client. The output stream of the Client connects to the input stream of the Server and visa versa.
ObjectInputStream serverInputStream = new ObjectInputStream(pipe.getInputStream()); ObjectOutputStream serverOutputStream = new ObjectOutputStream(pipe.getOutputStream());
Reading the Employee Object and Writing it Back
Now, the infrastructure is in place to read the Employee object off of the “pipe.” Once you execute the accept() method, the Server will wait until it is notified that an object is ready to be read. When this happens, the Server continues and the following code is executed.
employee = (Employee )serverInputStream.readObject();
You use the readObject() method to read the object off of the “pipe.” This method returns the reference to the Employee object that was sent from the Client. You use the reference that was created earlier. Note that you cast the object to the reference. The readObject() method returns an Object off the “pipe;” you must cast it to the specific object that you want.
Once you retrieve the Employee object from the connection, you can make a couple of simple changes. you do this so that when you send it back to the Client, you can verify that the Server actually changed it.
employee .setEmployeeNumber(256); employee .setEmployeeName("John");
After you change the object, you send it back to the Client using the output stream with the following code:
serverOutputStream.writeObject(employee);
Closing the Object Streams
Finally, you need to do some housekeeping. In this case, you will close both the input and output streams with the following lines.
serverOutputStream.close(); serverInputStream.close();
This completes the code for the Server. As with the Client, the code in the Server is incorporated inside a try/catch block. This is required for compilation. As stated earlier, you will cover the various exceptions and how to handle them in later articles. Listing 3 contains the complete code for the Server.
import java.io.*; import java.net.*; public class Server { public static void main(String[] arg) { Employee employee = null; try { ServerSocket socketConnection = new ServerSocket(11111); System.out.println("Server Waiting"); Socket pipe = socketConnection.accept(); ObjectInputStream serverInputStream = new ObjectInputStream(pipe.getInputStream()); ObjectOutputStream serverOutputStream = new ObjectOutputStream(pipe.getOutputStream()); employee = (Employee )serverInputStream.readObject(); employee .setEmployeeNumber(256); employee .setEmployeeName("John"); serverOutputStream.writeObject(employee); serverInputStream.close(); serverOutputStream.close(); } catch(Exception e) {System.out.println(e); } } }
Listing 3: The Server
Running the System
In this article, you simply set up the infrastructure for a working Client/Server model. There are many fascinating points that you will explore in future articles. However, you should get this basic model up and running so that you can verify that the system is in working order.
Compiling the Code
I compile the code using a DOS Shell. Eventually, you will need two of these DOS Shells—one to run the Server and one for the Client. You can open a DOS Shell in the Programs->Accessories option.
Type the following code at the command prompt to compile all three of the files.
"C:Program FilesJavajdk1.5.0_06binjavac" Employee.java "C:Program FilesJavajdk1.5.0_06binjavac" Client.java "C:Program FilesJavajdk1.5.0_06binjavac" Server.java
Figure 1 shows the screen shot of how this is accomplished.
Figure 1: Compiling the Code
Starting the Server
In one of the DOS Shells, type in the following line at the command prompt.
"C:Program FilesJavajdk1.5.0_06binjava" Server
Figure 2 shows what happens in the DOS Shell.
Figure 2: Starting the Server
If everything is working properly, the “Server Waiting” message is displayed. At this point, you can start the Client.
Starting the Client
In a separate DOS Shell, start the Client with the following line:
"C:Program FilesJavajdk1.5.0_06binjava" Client
The result is shown in Figure 3.
Figure 3: Starting the Client
If all is well, you will see that the employeeNumber and the employeeName both were changed. You can put some specific identification in the print statements to provide further assurance.
With the circuit complete, the Server should exit cleanly, as shown in Figure 4.
Figure 4: Completing the System
Conclusion
In this article, you covered the basic concepts involved with creating a simple Client/Server model. The code is a complete and functional model. There are many interesting aspects of Client/Server models that you have yet to explore.
Moving an object from one place to another is often a tricky proposition. Using modern object-oriented languages makes life a lot easier for today’s programmers. It is very interesting to look at code from earlier programming languages pertaining to network programming. In years past, basic network programming required the programmer to develop code that was sometimes quite complex or purchase third-party software that was often hard to use. Languages such as Java and .NET have much of this functionality built right into the language.
As stated earlier, although the example of this article is complete and useful, it is quite basic. There are many more fascinating topics to explore regarding client/server applications. Next month, you will investigate several of these interesting topics.
References
Tyma, Paul, Gabriel Torok and Troy Downing: Java Primer Plus. The Waite Group, 1996.
About the Author
Matt Weisfeld is a faculty member at Cuyahoga Community College (Tri-C) in Cleveland, Ohio. Matt is a member of the Information Technology department, teaching programming languages such as C++, Java, and C# .NET as well as various web technologies. Prior to joining Tri-C, Matt spent 20 years in the information technology industry gaining experience in software development, project management, business development, corporate training, and part-time teaching. Matt holds an MS in computer science and an MBA in project management. Besides The Object-Oriented Thought Process
, which is now in it’s second edition, Matt has published two other computer books, and more than a dozen articles in magazines and journals such as Dr. Dobb’s Journal, The C/C++ Users Journal, Software Development Magazine, Java Report, and the international journal Project Management. Matt has presented at conferences throughout the United States and Canada.