Java offers many ways to exchange information between a client and a server object. Some examples that are available throughout this Web site include:
Examples 1 and 2, using CGI, represent stateless environments. In other words, the client object opens a connection to the server, executes the server object, accepts any results, and disconnects immediately from the server. If another call to the server is required, the client needs to open the connection to the server again and repeat the process.
Example 3, using Remote Method Invocation, represents a persistent environment. In other words, the client object opens a connection to the server and leaves it open until the client or the server chooses to disconnect. The client can continue exchanging information with the server object without needing to reestablish the connection.
Sockets allow Java objects to communicate with each other persistently, similar to RMI and CORBA/DL. The difference lies, however, in how it does this. Let's consider the distributed architecture of a socket-based application:
The Java client object connects to the Java server object through the TCP/IP network using a socket. Once this connection is made, the two can exchange information between themselves.
What exactly is a socket? Consider the following scenario:
Two companies, A and B, are located on separate islands and wish to engage in commerce with each other. There are miles of ocean separating them, so they agree to buy a freight barge and use it to transfer the goods between them. On each island, the companies need to dock the barge at a harbor, so they each build a port.
Company A loads the barge with its product at the port and sends it off to Company B. Company B opens its port to receive the barge and unloads the goods. In return, it loads the barge with its own product for delivery back to Company A.
In this example, the barge serves as the transport vehicle between the two islands, and the port serves as point where the products are loaded and unloaded. Let's extend this example to a computer application:
A Java applet (client) contains a search form, on which the user enters some search criteria. It wishes to send the search criteria to a server application in order to perform a database query and send the results back the applet for display. The client applet represents Company A. The server application represents Company B. The search criteria and query results represent the products exchanged between the client and the server. The TCP/IP network represents the ocean, and the TCP layer is the barge that transports the information.
The server application opens a TCP port, called a server socket, and waits for incoming traffic. The applet opens a network connection, called a client socket, with the server. Once the connection is accepted by the server, the applet sends the search criteria to the server through an outbound TCP stream. The server reads the incoming stream, processes the database query, and sends the results back to the client through the TCP stream. This process continues until either the client or the server closes its respective socket.
Let's start by building a simple socket-based application. As we build the application, we will see how each of the components interacts with the other.
Our first attempt at building a socket-based application will be a simple one:
Let's start with the server application.
The first step in creating the server object is to import the necessary packages. The java.net package will be used to access the socket classes. The java.io package will be used to send messages between the client and server applications.
// Simple Socket Server import java.net.*; import java.io.*; public class SimpleSocketServer { |
As we communicate using sockets, we need to store a reference to the server socket and the client connection.
// define the socket objects static ServerSocket serverSocket = null; // server static Socket clientSocket = null; // client |
The application needs to offer itself as a server through a TCP socket. This is done creating the ServerSocket class, specifying the TCP port number that will be used for communication. (This port number must be one that is not already in use on the server. We will arbitrarily use port 9000.) The attempt to create a server socket may throw an IOException, so we shall trap it and exit the program if this occurs.
public static void main(String args[]) { // open up a server socket on port 9000 try { serverSocket = new ServerSocket(9000); System.err.println("Server socket listening on port 9000"); } catch (IOException e) { System.err.println("Cannot open socket on port: " + 9000); System.err.println(e.getMessage()); System.exit(1); } |
Next, we wait for a client to request a connection with the server on this socket. This is done by calling the server socket's accept() method, which returns a Socket object. This new socket will be used to communicate with the client. The accept() method may throw an IOException, so we shall trap it and exit the program if this occurs.
// listen for client socket request try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Error accepting client socket request."); System.err.println(e.getMessage()); System.exit(1); } |
Now that we have started a connection with the client, we need to open the input and output streams in order to exchange messages with it. We will use object streams in our example. (This technique is known as object serialization.) The attempts to open these streams may throw an IOException, so we shall trap it and exit the program if this occurs.
// Use object streams as the TCP socket transport ObjectInputStream is = null; ObjectOutputStream os = null; // open the message streams try { is = new ObjectInputStream(clientSocket.getInputStream()); os = new ObjectOutputStream(clientSocket.getOutputStream()); } catch (IOException e) { System.err.println("Error opening object streams."); System.err.println(e.getMessage()); System.exit(1); } |
Recall that our client will send us any string, and we will return to it the same string with its characters in reverse order. Through the socket's input stream, let's read the incoming String object using the object input stream. We'll reverse the string and send the result back using the object output stream.
The read attempt may throw an IOException or ClassNotFoundException, and the write attempt may throw an IOException. Let's trap both exceptions through the generic Exception class.
// String areas for client and server messages String inmsg = null; String outmsg = null; // process the message streams try { StringBuffer sb; // read the incoming string object inmsg = (String)is.readObject(); // reverse the string sb = new StringBuffer(inmsg); outmsg = new String(sb.reverse()); // send the new string back to the client os.writeObject(outmsg); os.flush(); } catch (IOException e) { System.err.println("Error streaming the message."); System.err.println(e.getMessage()); System.exit(1); } |
We've completed our server process. Let's close the client and server sockets and terminate the program.
// close the sockets when done try { clientSocket.close(); serverSocket.close(); } catch (IOException e) { System.err.println("Could not close sockets."); System.err.println(e.getMessage()); } } |
Our server application can be compiled like any other Java class. To run it, open up a shell or window and execute the server application in the background:
javac SimpleSocketServer.java |
Windows 95/NT:
start java SimpleSocketServer |
Solaris:
java SimpleSocketServer & |
Now that we have build the simple socket server, let's build the associated client. Recall that our socket-based application will do the following:
The first step in creating the client object is to import the necessary packages. The java.net package will be used to access the socket classes. The java.io package will be used to send messages between the client and server applications.
// Simple Socket Server import java.net.*; import java.io.*; public class SimpleSocketClient { |
The client application requires a socket connection object that will be used to connect to the server.As we communicate using sockets, we need to store a reference to the server socket and the client connection. We will declare the main() routine to throw any exceptions that are generated within the application.
public static void main(String args[]) throws Exception { // client socket connection Socket socket = null; |
Similar to the server application, we will use object streams to exchange messages with the server. Let's declare them now, along with the String objects representing the incoming and outgoing messages.
// socket streams ObjectInputStream is = null; ObjectOutputStream os = null; // message strings String inmsg = null; String outmsg = null; // initialize an outgoing message outmsg = "The quick, brown fox jumps over the lazy dog's back."; |
The client will attempt to connect to the server socket now by creating a Socket object, passing the server application's host name and port number. In our example, we will use localhost as the host name, and 9000 as the port number.(If the connection attempt fails, an UnknownHostException or IOException may occur.)
// open socket connection with server socket = new Socket("localhost", 9000); |
Now that we have connected to the server, we need to open the input and output streams in order to exchange messages with it. We will use object streams that we declared above. (The attempts to open these streams may throw an IOException.)
// open socket streams os = new ObjectOutputStream(socket.getOutputStream()); is = new ObjectInputStream(socket.getInputStream()); |
Recall that our client will send a string to the server and receive it back in reverse order. Let's do so now using the object streams. The final result will be printed on the default system display. (The read and write attempts may throw an IOException or ClassNotFoundException,.)
// send outbound message to server and flush the buffer os.writeObject(outmsg); os.flush(); // accept inbound message from server inmsg = (String)is.readObject(); System.out.println(inmsg); |
We've completed our client process. Let's close the streams and socket connection and terminate the program.
// close streams and socket connection os.close(); is.close(); socket.close(); } } |
Our client application can be compiled and executed like any other Java application:
javac SimpleSocketServer.java |
java SimpleSocketClient |
Response:
.kcab s'god yzal eht revo spmuj xof nworb ,kciuq ehT |
Traditional client-server models illustrate a client application accessing a relational database running on a remote server. Two-tier designs place the database access within the client. Three- and n-tier designs distribute the database access to one or more middle layers running on another machine. The Common Gateway Interface provides one method for delivering Java applications across the network.
The RMI and CORBA/IDL sections of this Web site demonstrate how the traditional client-server model can be extended to a distributed object architecture. The RMI operates in a pure Java environment, whereas the IDL supports a heterogeneous environment using CORBA standards.
We can use TCP sockets to build a distributed object-based application and will do so in the following pages. First, let's compare the use of sockets to some of the other distributed computing models:
Similarities:
Differences:
*If you use object streams to exchange messages between the client and server sockets, you must use a browser that supports JDK 1.1. If you wish to make the socket-based system compatible with JDK 1.0, you must use data streams.
In order to illustrate a distributed computing model using sockets, as well as its ability to provide persistency in Internet computing, we will build a simple application using the JDBC. If you are new to the JDBC, then refer to the Java Database Connection section of this site for a quick overview. (This same application is shown in the RMI and CORBA/IDL sections for comparison purposes.)
In our example, the applet will appear and function as follows*:
1, Nancy Davolio, Sales Representative 2, Andrew Fuller, Vice President, Sales 3, Janet Leverling, Sales Representative 4, Margaret Peacock, Sales Representative 5, Steven Buchanan, Sales Manager 6, Michael Suyama, Sales Representative 7, Robert King, Sales Representative 8, Laura Callahan, Inside Sales Coordinator 9, Anne Dodsworth, Sales Representative |
*Note: This applet simulates the process that will be used to provide
JDBC access through sockets. It is included here only to demonstrate the
concepts and does not contain the full implementation.
Upon initial load, the applet will connect to the Microsoft Access 7.0 sample database, Northwind. When the "Begin Search" button is pressed, it will perform a search that returns and displays all employee names and titles from the Employee table. When the "Clear Data" button is pressed, it will clear the search results from the applet. The database connection remains open until the applet is closed and terminated (i.e. shut down).
Key success factors of the process include:
To build our socket-based system, we will create three components:
Similar to the RMI and CORBA/IDL examples, a socket connection allows an applet to communicate with a remote server object. Because we need to provide multiuser database support, we will incorporate threads into the server object as well. We will refer to this as our Server Thread.
Let's begin building our socket process by starting with the Server Thread.
The first step we will take in building our socket application is in the creation of a threaded object for multiuser access. (This is similar to the JDBC/RMI Connection Object contained within the RMI section of this site.)
Before we begin developing the socket thread, let's define how we wish to exchange messages between the client and the server:
The following table illustrates the various commands and return values that we can have:
Client Command | Action Taken by the Server | Return Values |
---|---|---|
1. null (occurs when the client connection is closed) | Close the client socket | None |
2. open | Open the database connection | ok error: Exception |
3. srch | Perform a database search (query) | ok error: Exception |
4. fetch | Fetch a row from the database (following a query) | Query results null (if no more rows found) error: Exception |
5. close | Close the database connection | ok error: Exception |
6. All other commands | Generate an error (invalid command) | Invalid command |
Let's call our socket thread JS, for Java Socket.
We begin by importing the appropriate Java packages:
import java.net.*; import java.io.*; import java.sql.*; public class JS extends Thread { |
We will pass a client socket object to the thread's constructor. The socket connection will be established prior to creating the thread. Note that we plan to use object streams as we exchange messages between the client and server.
private Socket clientSocket; // Client socket object private Connection con; // Database connection object private Statement stmt; // SQL statement object private ResultSet rs; // SQL query results private ObjectInputStream is; // Input stream private ObjectOutputStream os; // Output stream public JS (Socket clientSocket) { this.clientSocket = clientSocket; } |
The following input/output methods will be used to exchange messages between the client and the server. First, readCommand() will accept and process a command string from the client. Next, send() will return a message back to the client. Finally, sendError() will send a pre-formatted error message to the client.
/** Receive and process incoming command from client socket */ public boolean readCommand() { String s = null; |
try { s = (String)is.readObject(); } catch (Exception e) { s = null; } if (s == null) { closeSocket(); return false; } // invoke the appropriate function based on the command if (s.equals("open")) { openDatabase(); } else if (s.equals("srch")) { performSearch(); } else if (s.equals("fetch")) { getNextRow(); } else if (s.equals("close")) { closeDatabase(true); } else { sendError("Invalid command -> " + s); } return true; } /** Send a message back through the client socket */ public void send(Object o) { try { os.writeObject(o); os.flush(); } catch (Exception ex) { ex.printStackTrace(); } } /** Send a pre-formatted error message to the client */ public void sendError(String msg) { send("error:" + msg); } |
Let's build the data access methods that are referenced by our incoming command strings. We'll trap all possible exceptions within these methods and send messages back to the client when they are caught.
/** Open a database connection */ public void openDatabase() { try { // Load the JDBC-ODBC bridge driver Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver"); // Connect to the database con = DriverManager.getConnection("jdbc:odbc:Northwind", "admin", ""); send("ok"); } catch (Exception ex) { sendError(ex.toString()); } } /** Issue a SQL query */ public void performSearch() { String query; // SQL select string // build the query command query = "SELECT EmployeeID, LastName, FirstName, Title " + "FROM Employees "; try { stmt = con.createStatement(); rs = stmt.executeQuery(query); send("ok"); } catch (Exception ex) { sendError(ex.toString()); } } /** Fetch the next row in the result set, returning null when done */ public void getNextRow() { String s = null; try { if (rs.next()) { s = rs.getInt("EmployeeId") + ", " + rs.getString("FirstName") + " " + rs.getString("LastName") + ", " + rs.getString("Title"); } else { rs.close(); rs = null; stmt.close(); stmt = null; s = null; } send(s); } catch (Exception ex) { sendError(ex.toString()); } } /** Close the database connection ack = true if we want to send an acknowledgement back to the client */ public void closeDatabase(boolean ack) { try { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (con != null) con.close(); rs = null; stmt = null; con = null; } catch (Exception ex) { sendError(ex.toString()); } } |
Finally, the thread will contain two more methods: run() and closeSocket(). The run() method is where the thread's activities execute once the thread is created. When it completes, the thread stops processing. The closeSocket() method will close the necessary sessions when the client disconnects itself from the server.
/** Thread execution method */ public void run() { // initialize the database objects rs = null; stmt = null; con = null; try { // open the socket streams is = new ObjectInputStream(clientSocket.getInputStream()); os = new ObjectOutputStream(clientSocket.getOutputStream()); // continue processing until no more commands while (readCommand()) { } } catch (IOException e) { e.printStackTrace(); } } /** Close the client socket */ public void closeSocket() { try { // close all sessions and streams closeDatabase(false); os.close(); is.close(); clientSocket.close(); } catch (Exception ex) { System.err.println(ex.toString()); } } |
javac JS.java |
Now that we have created our Socket Thread, let's build the server application that will use it. The server application will perform the simple task of opening a socket connection with the client, after which it launches a thread that will do the rest of the work. It is this application that allows multiuser access to the database.
The name of our server application will be JSStart. The comments placed throughout the source code will explain each step in the application. (We'll call our server JSStart.)
// import the necessary packages import java.net.*; import java.io.*; public class JSStart { public static void main(String args[]) { // create a server socket on port 9000 ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(9000); System.out.println("JSStart listening on port 9000"); } catch (IOException e) { System.out.println("Cannot listen on port: " + 9000 + ", " + e); System.exit(1); } // endless loop to process client connections while (true) { // wait for a client request Socket clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.out.println("Accept failed: " + 9000 + ", " + e); break; } // create a new thread for the client JS js = new JS(clientSocket); // can't open a thread, so return an error to the client if (js == null) { try { ObjectOutputStream os = new ObjectOutputStream(clientSocket.getOutputStream()); os.writeObject("error: Cannot open socket thread"); os.flush(); os.close(); } catch (Exception ex) { System.out.println("Cannot send error back to client: 9000, " + ex); } } else { // transfer control over to the thread now js.start(); } } // we should never get this far because of the while loop above try { System.out.println("Closing server socket."); serverSocket.close(); } catch (IOException e) { System.err.println("Could not close server socket." + e.getMessage()); } } } |
javac JSStart.java |
The server socket application that we just created will be executed as a background process running on the Web server. It represents the "server" half of a "client-server" process. The "client" half can be built as a Java application or as an applet. Although we will illustrate this using an applet, let's quickly explore the security aspects required by socket clients.
Standard Java security measures apply to socket-based applications and applets. For example, an applet may only connect to a server socket on the host from which it was originally loaded. An application may connect to any other server socket on the network. (This is unlike the RMI client, which requires an explicit security manager for client applications.)
The sample code below will only contain the logic necessary for the applet to use sockets. The complete set of source code for the applet is available in The Complete JDBC/Socket Example later on.
The applet begins by importing the appropriate packages and creating an reference to the socket streams. Next, it opens the socket connection with the server and begins processing. Follow each of the methods contained in the source below, referring to the inline comments for further explanation. Pay particular attention to how the client communicates with the server, using the send() and receive() methods.
import java.net.*; import java.io.*; public class JSApplet extends Applet implements ActionListener { Socket socket = null; ObjectOutputStream os = null; ObjectInputStream is = null; /** Initialize the applet and connect to the server */ public void init() { ... // apple display goes here if (connectToServer()) { openDatabase(); } else { showStatus("Cannot open socket connection."); } } /** Open a socket connection with the server */ public boolean connectToServer() { try { socket = new Socket(getCodeBase().getHost(), 9000); // open the input and output object streams os = new ObjectOutputStream(socket.getOutputStream()); is = new ObjectInputStream(socket.getInputStream()); } catch (Exception ex) { showStatus(ex.toString()); return false; // error connecting to server } return true; // successful connection } /** Open the database connection through the socket */ public void openDatabase() { String ack; send("open"); ack = (String)receive(); if (ack != null) showStatus(ack); } /** Send a message to the server */ public void send(Object o) { try { os.writeObject(o); os.flush(); } catch (Exception ex) { showStatus(ex.toString()); } } /** Receive a message from the server */ public Object receive() { Object o = null; try { o = is.readObject(); } catch (Exception ex) { showStatus(ex.toString()); } return o; } /** Close the sockets when the applet is destroyed */ public void finalize() { try { send("close"); os.close(); is.close(); socket.close(); } catch (Exception ex) {} super.destroy(); } /** Issue a SQL query */ public void performSearch() throws Exception { Object result; // Row retrieved from query send("srch"); result = receive(); if (result.equals("ok")) { processResults(); } else { showStatus((String)result); } } /** Process the query results */ public void processResults() { Object result; send("fetch"); result = receive(); if (result == null) { showStatus("No rows found using specified search criteria."); return; } // keep fetching until end of result set while (result != null) { ... // applet display goes here send("fetch"); result = receive(); } } } |
javac JSApplet.java |
At this point we have created the following classes:
To run the applet, simply load it from within an HTML document using a browser or the appletviewer. Since we are using some JDK 1.1 features within our applet, we'll must use a browser that supports this release of the JDK. Let's use the appletviewer.
The socket server application must run as a background process on the Web server. (If you specified a socket port number that is less than 1024, you may also need special administrative privileges to run the application.)
Windows 95/NT:
start /min java JSStart
Solaris:
java JSStart &
When the server starts, you should see the following message displayed on the screen:
JSStart listening on port 9000
Create an HTML document that loads our applet:
<APPLET CODE="JSApplet.class" HEIGHT=200 WIDTH=600> </APPLET> |
Run this document using the appletviewer (the HTML document name is jsapplet.htm):
appletviewer jsapplet.htm
Congratulations! You are now running a multithreaded JDBC application using Java sockets.
The complete source code for each of the objects created in our JDBC/Socket example is available through the links listed below. You may save each of these objects or copy/paste the code into files on your hard disk and experiment with them. Good luck with your own socket-based applications!
The socket thread.
The socket application.
The socket client.