| < Day Day Up > |
|
As discussed in the previous chapter, sockets provide the basic communication layer required for two applications running in different address spaces to communicate with each other, thus forming the foundation necessary for distributed application development. Even though sockets also permit two modules running in the same address space (or belonging to a single application) to communicate with each other, it is usually not necessary to implement sockets in such applications because the objects are accessible directly. While the TCP sockets provide a reliable point-to-point communication channel serving the majority of client-server applications in the industry, the UDP sockets are useful for implementing message broadcasting-type applications where the sequence of delivery of data packets is not a concern. The java.net package comes with all the necessary classes and interfaces for socket-based application development. The server modules that incorporate server sockets to provide service to multiple clients also need to implement multi-threading concepts in order to support several clients at the same time. Having discussed the concepts of sockets and multi-threaded applications in prior chapters, this chapter will combine these two concepts and demonstrate implementation of sockets-based client and server applications. Because the server and client modules run in different address spaces, a server application developed in Java can service a client application written in any platform or language. In fact, the server does not know (and does not care) what the client application is and where it is running. Figure 7.1 displays a typical scenario, where the server module is written in Java and the client modules are written in different environments—such as Delphi, C++, and Java—and on different operating systems.
Figure 7.1: A Java server module serving clients in different environments.
In order to create a TCP stream-based server socket, the server module must create an instance of the java.net.ServerSocket class, which listens to a specific port for the incoming client connection requests. The maximum number of simultaneous client connections that the server can accept may also be specified while creating the server socket object. In that case, the server will make sure that the simultaneous client connections will not exceed this number. It does this by rejecting all additional client connection requests once the limit is reached. The ServerSocket class has three constructors with different input arguments; all the constructors require the port number to be specified (as the first argument), at which the server will be listening for client connections. If a value of 0 is specified, any available free port is used. The first constructor does not need any more arguments, the second and third constructors take an integer value indicating the maximum number of simultaneous connections as second argument, and the third constructor takes the IP address to which the server socket should be bound as an additional argument. The third constructor is used to specify an IP address on a system having multiple network interfaces (or serving on multiple IP addresses). Every time a new request for a fresh connection is received from a client, the server creates a server-end point of the socket connection—identified by an object of the java.net.Socket class—if the server accepts the client connection. It is this object through which the server communicates with the client. For every client connection, there is one Socket object created when the accept method is executed on the server socket. Even the client application will create its own Socket object through which it connects to the server-side Socket object. The Socket class provides a pair of input and output stream objects on either side to perform the data transfer from one end point to the other; these stream objects—InputStream and OutputStream—can be used to read from and write to the data as individual bytes. However, it is often more convenient to create ObjectInputStream and ObjectOutputStream, using the raw streams to perform effective transfer of Java objects across the network. Because the communication between sockets is performed using input and output streams, object serialization can be coupled with sockets to build a powerful custom data marshalling software. Listing 7.4 displays a simple TCP server module, which keeps listening at the server port. For every new client connection request, a new server thread is spawned that further communicates with the client. The server thread program is displayed in Listing 7.5. The server thread program waits in an infinite loop to receive client requests. It should be noted that every message between the client and the server is preceded with a message code and an equal mark ‘=’; the client and the server know what the other side is sending. This type of message coding is typical of any custom data-marshalling software. In this example, the client program first sends its name with the message code ‘NAME’. The server responds to this message by sending a return ‘Hello’ message. When the client knows that it received a response for the ‘NAME’ message, it sends the next message with code ‘TIME’ to query the current time on the server. This time, the server responds to this message by sending a string representation of the current server time. Once the client receives a response to this message, it displays the message to the console and sends the ‘BYE’ message, which is interpreted by the server as a request to terminate the client connection. Listing 7.6 displays the client module. This is a very rudimentary multi-threaded client server model and provides the basic structure. More features can be added to make it a real-world working model. Listings 7.4, 7.5, and 7.6 are available on the accompanying CD-ROM.
Listing 7.4: TCPServer.java
package tcpsockets; import java.io.*; import java.util.*; import java.net.*; public class TCPServer { ServerSocket serverSock; Socket clientConnection; int port; public TCPServer() { port = 6800; try { serverSock = new ServerSocket(port); System.out.println("Started server module..."); while (true) { System.out.println("Waiting for client connection requests..."); // For every client connection accepted by the server, spawn a // separate thread to work with the client. The thread closes // when the client explicitly disconnects from the connection // or sends a BYE message. clientConnection = serverSock.accept(); int clientPort = clientConnection.getPort(); String clientIP = clientConnection.getInetAddress().toString(); System.out.println("Connection request accepted from client at IP address "+clientIP+" and port "+clientPort); ServerThread newConnThread = new ServerThread(clientConnection); newConnThread.run(); } } catch (Exception ex) { ex.printStackTrace(); } } public static void main(String[] args) { System.out.println("Attempting to start the TCP server..."); TCPServer TCPServer1 = new TCPServer(); } }
Listing 7.5: ServerThread.java
package tcpsockets; import java.io.*; import java.util.*; import java.net.*; public class ServerThread extends Thread { Socket sock; ObjectOutputStream ostr; ObjectInputStream istr; int clientPort; String clientIP; public ServerThread(Socket newSocket) { sock = newSocket; try { // get the input and output streams from the socket object // to perform read and write operations respectively ostr = new ObjectOutputStream(sock.getOutputStream()); istr = new ObjectInputStream(sock.getInputStream()); clientPort = sock.getPort(); clientIP = sock.getInetAddress().toString(); System.out.println("Ready to communicate with client at IP address "+clientIP+" and port "+clientPort); } catch (Exception ex) { ex.printStackTrace(); } } public void run() { // It is this run method of the individual thread object that // performs the actual communication with the client socket. String inMsg = ""; do { try { inMsg = (String) istr.readObject(); StringTokenizer inStrTok = new StringTokenizer(inMsg, "=", false); String msgCode = inStrTok.nextToken(); if (msgCode.equals("NAME")) { String clientName = inStrTok.nextToken(); System.out.println("The client name is "+clientName); ostr.writeObject("NAME=Hello "+clientName); } if (msgCode.equals("TIME")) { Calendar currTime = Calendar.getInstance(); ostr.writeObject("TIME="+currTime.getTime()); } } catch (ClassNotFoundException ex) { ex.printStackTrace(); System.exit(-1); } catch (IOException ex) { System.out.println("Error with I/O streams for client at IP address " +clientIP+" and port "+clientPort); System.out.println("Terminating client connection..."); System.exit(-1); } } while (!inMsg.equals("BYE")); System.out.println("Client requested termination from IP address " +clientIP+" and port "+clientPort); try { sock.shutdownInput(); sock.shutdownOutput(); sock.close(); } catch (IOException ex) { ex.printStackTrace(); } } }
Listing 7.6: TCPClient.java
package tcpsockets; import java.io.*; import java.util.*; import java.net.*; public class TCPClient { Socket sock; InetAddress serverInetAddr; String clientName; public TCPClient(String clName) { try { clientName = clName; serverInetAddr = InetAddress.getByName("127.0.0.1"); sock = new Socket(serverInetAddr, 6800); System.out.println("Connected to server on " +serverInetAddr.getHostName()+ " listening to port "+sock.getPort()); // get the input and output streams from the socket object // to perform read and write operations respectively ObjectOutputStream ostr = new ObjectOutputStream(sock.getOutputStream()); ObjectInputStream istr = new ObjectInputStream(sock.getInputStream()); ostr.writeObject("NAME="+clientName); String inMsg = ""; while (true) { if (sock.isClosed()) { System.out.println("Server closed the connection..."); break; } if (sock.isInputShutdown()) { System.out.println("Input stream is down..."); break; } if (sock.isOutputShutdown()) { System.out.println("Output stream is down ..."); break; } try { inMsg = (String) istr.readObject(); StringTokenizer inStrTok = new StringTokenizer(inMsg, "=", false); String msgCode = inStrTok.nextToken(); if (msgCode.equals("NAME")) { ostr.writeObject("TIME=?"); } if (msgCode.equals("TIME")) { String currTime = inStrTok.nextToken(); System.out.println("Current time on the server is " +currTime); ostr.writeObject("BYE"); } } catch (ClassNotFoundException ex) { ex.printStackTrace(); } catch (IOException ex) { System.out.println("The socket is closed..."); System.out.println("Terminating server connection..."); System.exit( -1); } }; } catch (Exception ex) { ex.printStackTrace(); } } public static void main(String[] args) { if (args.length != 1) { System.out.println("Client must specify a name..."); System.exit(-1); } String clientName = new String(args[0]); System.out.println("Attempting to connet to the TCP server..."); TCPClient TCPClient1 = new TCPClient(clientName); } }
As seen in these examples, the TCP sockets are stream based and establish a point-to-point connection between the two ends of communication, and therefore form the foundation of client server applications. The other type of sockets is the UDP (or user datagram protocol) sockets, which are also known to communicate using connectionless protocol. The java.net.DatagramSocket class provides the UDP-based socket functionality and is used to both send and receive messages over a UDP port. The datagram sockets are used to broadcast messages through a port on the host machine where the server is running, very similar to a mail delivery system. A large message may be broken into individual data packets and sent. When the client receives these packets, they may or may not reach in the order they were sent, and sometimes may not reach at all. Therefore, it is a good idea to embed a sequence number within the data packets that the client can use to interpret the packet sequence and reconstruct the complete message from these packets before trying to use them. The missing sequence numbers in the received packets inform the client about the missing packets, and the client may in turn request the server to resubmit them. The DatagramSocket has a variety of constructors, including one that does not take any arguments. To use the constructors that take arguments, at least the port of the IP address of the local host must be specified. If the port is not specified, one of the available ports is used, and if the IP address is not specified, the local host IP address is used. If the DatagramSocket object is created using the no-argument constructor, then the port and IP address are both selected automatically for the local computer. It is always recommended to specify the host computer IP address and port to make it easy for the remote client to be prepared to receive the messages, unless the application requires that the socket be created without IP address, or port, or both. The java.net.DatagramPacket class is used to create objects of real data packets sent using the UDP protocol. The DatagramPacket class has a variety of constructors with different options while constructing the data packets. These include combinations of data buffer, length of data buffer, Internet address, and port of the remote computer participating in the communication (either receiver or sender), among others. The methods of the class enable accessing the buffer, setting the buffer, Internet address, and port of the recipient or sender, and so on. Typically, the sender of datagram packets sends to a specific port on the computer where the recipient is running, whereas the recipient is bound to a specific socket on its computer listening for incoming messages. This means that the sender specifies the recipient’s port number in the constructor of the DatagramPacket class along with the recipient’s IP address. When the recipient receives the message, the incoming datagram packet contains the IP address and port of the computer from where the message was sent. Listing 7.7 displays a simple UDP socket, that spawns a separate thread that broadcasts messages to the client, as shown in the UDP server thread program of Listing 7.8. The server thread program sleeps for 200 milliseconds after sending 50 messages. As discussed in an earlier chapter, while working with multi-threaded applications, the threads should be designed to be unselfish, which means they should provide an opportunity for other threads to get the CPU resources. In conjunction with the operating system, every programming language provides a way for the threads to yield to other threads. The length of time the current thread might sleep to yield to other threads is a design parameter of the application and is determined by the importance and priority of several threads (belonging to the same multi-threaded application and also threads belonging to other applications) running in parallel during the runtime of the multi-threaded application. Listing 7.9 displays the client application, which receives the messages broadcast from the server and displays them at the console. To test the application, compile the programs and start the server and client in separate consoles or terminals. If the output from the client console is observed continuously for some time, it can be noticed that some of the packets are received out of order; this is typical of UDP sockets. As mentioned earlier, this is why a sequence number should be embedded in the message. Listings 7.7, 7.8, and 7.9 are available on the accompanying CD-ROM.
Listing 7.7: UDPServer.java
package udpsockets; import java.io.*; import java.net.*; import java.util.*; public class UDPServer { public UDPServer() { } public void StartServer() { UDPServerThread serverThread = new UDPServerThread(); serverThread.run(); } public static void main(String[] args) { UDPServer UDPServer1 = new UDPServer(); UDPServer1.StartServer(); } }
Listing 7.8: UDPServerThread.java
package udpsockets; import java.io.*; import java.util.*; import java.net.*; public class UDPServerThread extends Thread { int remotePort = 6040; public UDPServerThread() { } public void run() { try { InetAddress inetAddr = InetAddress.getByName("localhost"); DatagramSocket dgSocket = new DatagramSocket(); System.out.println("Message transmission started... "); int messageId = 1; int messageIdRpt = 1; while (true) { Calendar currTime = Calendar.getInstance(); String message = "Message Seq Number "+messageId; message += " - current time : "; message += currTime.getTime(); try { DatagramPacket dgPacket = new DatagramPacket(message. getBytes(), message.length(), inetAddr, remotePort); dgSocket.send(dgPacket); } catch (IOException iox) { System.out.println("IO Exception ..."); System.exit(-1); } if (messageIdRpt == 50) { System.out.println("50 messages sent since last time reporting..."); messageIdRpt = 1; sleep(200); } else messageIdRpt++; messageId++; } } catch (InterruptedException iox) { System.out.println("Server is interrupted to stop ..."); System.out.println("Hence stopping the UDP broadcast server ..."); System.exit(-1); } catch (UnknownHostException sx) { System.out.println("Host unknown ..."); System.exit(-1); } catch (SocketException sx) { System.out.println("Socket Exception ..."); System.exit(-1); } } }
Listing 7.9: UDPClient.java
package udpsockets; import java.io.*; import java.net.*; import java.util.*; public class UDPClient { int port = 6040; public UDPClient() { } public void StartClient() { try { InetAddress inetAddr = InetAddress.getByName("localhost"); DatagramSocket dgSocket = new DatagramSocket(port); System.out.println("Message reception started... "); byte buff[] = new byte[1000]; String message; try { DatagramPacket dgPacket = new DatagramPacket(buff, buff.length); dgSocket.receive(dgPacket); while (dgPacket.getData().length > 0) { message = new String(dgPacket.getData(), 0, dgPacket.getLength()); System.out.println(message); dgSocket.receive(dgPacket); } } catch (IOException iox) { System.out.println("IO Exception ..."); System.exit(-1); } } catch (UnknownHostException sx) { System.out.println("Host unknown ..."); System.exit(-1); } catch (SocketException sx) { System.out.println("Socket Exception ..."); System.exit(-1); } } public static void main(String[] args) { UDPClient UDPClient1 = new UDPClient(); UDPClient1.StartClient(); } }
| < Day Day Up > |
|