Java Network Programming Tutorial : TCP client and Multi-threaded (Concurrent) Server with custom Protocol - Step by Step

Author: Nishant Agarwal, mail: [email protected]


  1. Introduction
  2. TCP client/server state diagram
  3. Client
    1. Declare and intialize resources
      1. Client Socket
      2. Input Stream
      3. Output Stream
    2. Talk according to Protocol
    3. Close Resources
  4. Protocol
    1. Protocol Interface
    2. Protocol Implementation
  5. Concurrent Server
    1. Declare and initialize resources
      1. Server Socket
    2. Ready State, Wait for requests
    3. Spawn thread to service request, and go back to state 2
  6. Thread
    1. Declare and initialize resources
      1. Input Stream
      2. Output Stream
    2. Do work according to protocol
    3. Close resources
  7. The Code
  8. References

  1. Introduction

  2. This tutorial teaches you step by step, network programming in Java using Sockets. We want to design a multi-threaded (concurrent) server and a client, which implement a protocol to add two numbers. The client specifies two numbers to add at command line, the request is sent to server, the server spawns a thread to do the work. Multiple, simultaneous, large volume of reuqests can be sent to this server.

    I assume that you already know Java and are familiar with TCP protocol stack. Prior experience with socket programming in Unix will be helpful. If not, still Java socket programming is very easy to learn and implement. So, lets go for it…

    (Note: All the code can be downloaded from links below or viewed on the same page at the end of the text here.)

    Top


  3. TCP client/Server state diagram:

  4. See this excellent state diagram adapted from the famous Unix Network Programming book by Richard Stevens.

    Top


  5. Client:

  6. The client is coded in the MyClient.java file. It is clean to organize the code in three packages: client, server and protocol. (You can download the client code here: MyClient.java)

    1. Declare and Initialize resources
    2. We need a client socket to communicate with the server, and we need two streams. One input stream which will get responses from the server, and one output stream which will send data to the server.

      1. Client Socket
      2. The Socket class in the java.net package is used for implementing TCP client sockets. The constructor used here takes first argument as the hostname on which server resides, and the second argument takes the well-known port number of the service provided by the server. In my code, the host is my universities server, and the port is 51211. Use this signature to open your own socket.

        Socket sock = new Socket("hostname", portnumber);

        Example 1: Suppose you want to use the echo server, its well-known port number is 7, so you will use 7 as port number. For smtp mail server, its 25 and so on. A list of well-known port numbers i.e. 1 to 1023 can be obtained in a unix system in the /etc/services file.

        Example 2: Suppose you want to use your own machine for experiment. So, you want to give the hostname as localhost. Hence you can get a reference to your machine by :

        InetAddress addr;
        Socket sock = new Socket(addr.getLocalHost(), portnumber);

        This is allowed, because the Socket constructor is overloaded to take an InetAddress object as its first argument.

        You will most of the time use well-known port numbers from 1 to 1023 when instantiating a client socket. But sometimes you might want to connect to your own coded server which provides service at ephemeral port numbers (ports greater than 1023).

        JVM might throw UnknownHostException if it is unable to connect to the server at the specified hostname and/or port. You should print a stack trace or get a message to help debug the problem, at least whenever you catch an exception.

        System.err.println("Could not connect in Client: " + e.getMessage());
        e.printStackTrace();
        System.exit(-1); // because client can do nothing unless a socket is created

        Even if you dont know how to handle the exception, do this minimum, or let the exception propogate upwards. Remember these general guidelines:

        1. Ignoring a problem does not make it go away. If something is possible, someone will eventually do it.
        2. Java follows a "handle or specify" policy. Either handle the exception or specify that the method throws the exception.
        3. Throw exceptions at lower levels and handle them at higher levels of code.

        Watch out: always start the server first, if server is not up and running, and you run the client, UnknownHostException will be thrown.

      3. Input stream
      4. You need to open a stream which can be used to read from the server. Java provides a buffered reader class, BufferedReader. This is a character stream. The Socket class provides a method getInputStream() to get a reference to the input stream associated with the socket. This returned stream is a byte stream, which is converted to a character stream by InputStreamReader. Hence, the code looks something like this, wherein we get a reference to the input stream associated with the socket, convert it to a character stream by wrapping it in InputStreamReader, and convert this into a buffered stream, by wrapping it inside BufferedReader.

        BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream());

      5. Output stream
      6. Similar to above, you need to open a output stream to which the client can write and send data to the server. I use the PrintWriter stream, which is a character output stream. The getOutputStream() method of Socket class provides a reference to the output stream associated with the socket. Instantiate a PrintWriter stream by wrapping this reference inside it. The second argument of the constructor is a Boolean which indicates whether to flush on new line.

        PrintWriter out = new PrintWriter(sock.getOutPutStream(), true);

        JVM might throw an IOException if its unable to open the input or output stream.

    3. Talk according to Protocol
    4. The client-server architecture always requires a definite protocol, based upon which the client communicates with the server. This is a design issue. You might implement a protocol in several ways. See the Protocol section below for more details to see one of the ways to do it. Then read the code sample and explanation below.

      Protocol protocol = new MyProtocol(in, out);
      protocol.clientAddRequest(argv[0], argv[1]);

      I instantiate the MyProtocol object, but refer to it using a refernce of the interface Protocol. I pass the input and output streams associated with the socket to the protocol object. The class MyProtocol implements the interface Protocol. This lets us take advantage of polymorphism to choose protocol at run-time. Here, for simple example, my protocol just adds two integers. But same method can be used to add two floats, or two doubles or two strings, if I provide a class which implements the interface Protocol, and then I refer to instances of those classes by the reference protocol.

      The argv[] arguments indicate the two integers you supply at the command line. The idea is that the client wants to add two numbers, they are given at command-line, and the request is sent to server. The server adds them up and sends the result back to client. If the client fails to provide two arguments, the ArrayIndexOutOfBounds Exception can be thrown by the protocol implementation. I handle this exception, and inform client about the usage.

    5. Close resources
    6. Remember to close the resources you instantiated, i.e. the input stream, the output stream and the socket. Close the socket after closing the input and output streams. Closing the IO streams might throw an IOException. Hence the main method of the Client declares a throws clause for it.

    Top


  7. Protocol:

  8. Seperate interface from implementation.

    We want to have two methods. One is the input method for the client, it takes two arguments, i.e. the two numbers client wants to add, and sends them to the server. The second method is for the server, it adds the two numbers sent by the client and sends result back to client. This simple send receive mechanism is shown belwo:

    Client: Greets server
    Server: Greets client by identifying itself i.e. its thread id
    Client: sends first number
    Server: reads first number
    Client: sends second number
    Server: reads second number
    Server: adds the two numbers and sends result back to client
    Client: prints the sum of the two numbers
    Client: tells Bye to the server
    Client: waits for server to say Bye
    Server: gets the Bye from client
    Server: sends a Bye to the Client
    Client: exits
    Server: this particular thread of the server has done its work, it exits

    (The interface and implementation files can be downloaded from Protocol.java and MyProtocol.java)
    1. Protocol Interface
    2. public interface Protocol {
      void clientAddRequest(String s1, String s2) throws IOException;
      void serverAddReply() throws IOException;
      }

      The Protocol interface declares two methods. The clientAddRequest method requires the client-side implementation of the above described protocol. The serverAddReply method requires the server-side implementation of the above described protocol. Both methods can throw an IOException because they perform read and write operations on I/O streams.

    3. Protocol Implementation
    4. The class MyProtocol implements the Protocol interface, and has input Reader and output Writer streams as its members. So the client and server code instantiates an object of the MyProtocol class by passing it references of their respective socket streams.

      The implementation follows the rules described above in the sample interaction between client and server. The Integer.parseInt method is used at the server to get the integers from the string.

      public void clientAddRequest(String a, String b) throws IOException {
      out.println("Hello Server...");
      System.out.println("Server greeted: " + in.readLine());
      out.println(a);
      out.println(b);
      System.out.println("Server gave the sum of " + a + " and " + b + " as: " + in.readLine());
      out.println("Bye");
      System.out.println(in.readLine());
      }

      public void serverAddReply() throws IOException {
      in.readLine();
      out.println("This is server thread: " + Thread.currentThread().toString());
      String num1 = in.readLine();
      String num2 = in.readLine();
      out.println((Integer.parseInt(num1) + Integer.parseInt(num2)));
      in.readLine();
      out.println("Bye");
      }

      Why do I use an interface? Suppose I want to add two floats in the future instead of two integers. Then I just create a class A which implements the interface Protocol and provides an addServerReply method which uses Float.parseFloat method to get floats from the string and adds them up. All I need to do now in the client and the server code is that the reference protocol will now refer to an instance of this new A class. Neat, isnt it.

    Top


  9. Concurrent Server:

  10. The design of the server code is quite simple as it delegates all the work to a thread.

    (You can download the server code from MyServer.java)

    1. Declare and Initialize resources
      1. Server Socket
      2. The server code just needs one main resource. A refernce to a Server Socket. The ServerSocket class is used to instantiate a server socket. The constructor requires us to give the port number which will be used by the server to provide service, as the argument. If we give an argument of 0, then we are given any arbitrary ephemeral port number. See below for example:

        ServerSocket sock = new ServerSocket(0); System.out.println(sock.getLocalPort()); //prints the port number we got

        If we fail to get a server socket, the UnkownHostException is thrown.

        Another overloaded constructor of the ServerSocket allows you to give a second argument which is an int specifying the maximum requests in the wait queue of the server. Default is 50 which is more than enough. The idea is that while the server is busy servicing a request (even spawning a thread takes finite time), we need to queue up pending requests, if any.

    2. Ready state, Wait for requests
    3. Spawn Thread to service request and go back to state 2
    4. The above two states are shown in this code:

      while(isRunning == true) {
      new MyThread(socket.accept()).t.start();
      }

      Code an infinite loop in which the server keeps listening for requests, upon getting a request, it spawns a thread to handle the request, and gets back to its listening state. This design makes a concurrent server, which can serve multiple, simultaneous requests and its much faster than an iterative server.

      The MyThread class provides the thread which will do the work. The accept() method of the ServerSocket class is a blocking method which returns a reference to a new socket when it enocounters a request. Subsequent communiation between the client and the server takes place between the client socket and the socket returned by accept method. Hence actually, the purpose of the ServerSocket socket is just to listen to requests, the communication is done only using the socket returned by the accept method of the ServerSocket class.

      This single line of code is the heart of the multi-threaded server design. Upon getting a request, a new thread is spawned and it is passed the socket with which to communicate with the client. The start() method of the Thread class creates a seperate thread of execution, which begins executing the public void run() method implemented by the MyThread class.

    Top


  11. Thread:

  12. The run method of the MyThread class is the work-horse of the server. It does all the work, the server just spawns it and delegates it the work. The thread is passed an instance of the socket with which to communicate with the client.

    You can download the thread code at MyThread.java)

    1. Declare and Initalize resources
      1. Input Stream
      2. Output Stream
      3. Similar to the client code, an input stream and output stream associated with the socket are opened. Refer discussion in client code above.

    2. Do Work according to Protocol
    3. Similar to what we did in client code, we instantiate a MyProtocol object, pass it the streams associated with the socket in the thread, and refer to it by a interface reference. We then call the addServerReply method on this protocol object. Refer the Protocol section above to understand how protocol is implemented.

      Protocol protocol = new MyProtocol(in, out);
      protocol.serverAddReply();

    4. Close resources
    5. Close the streams and sockets before terminating the thread.

    Top


  13. The Code

  14. You can browse the code online by clicking: here. Or download the code:

    Top


  15. References

    1. Unix Network Programming, Volume 1, The Sockets API , by W. Richard Stevens
    2. Java Network Programming Trail on Sun Website

    Top


MyClient.java

/*This code written by Nishant Agarwal
  Jul 12th, 2004
  [email protected]
*/

package client; 

import java.io.*;
import java.net.*;
import protocol.*; 

public class MyClient {
    public static void main(String[] argv) throws IOException {
       
        //Resources we need
        Socket sock       = null;
        BufferedReader in = null;
        PrintWriter out   = null; 

        //Now initialize the resources
        try {
            sock = new Socket("nunki.usc.edu", 51211);
            in   = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            out  = new PrintWriter(sock.getOutputStream(), true);
	} catch (UnknownHostException e) {
            System.err.println("Could not connect in Client: " +
                               e.getMessage());
            e.printStackTrace();
            System.exit(-1);
	} catch (IOException e) {
            System.err.println("Could not open streams in Client: " +
                               e.getMessage());
            e.printStackTrace();
            System.exit(-1);
	}
   
        //talk to server according to protocol
        Protocol protocol = new MyProtocol(in, out);
        try {
            protocol.clientAddRequest(argv[0], argv[1]);
        } catch (IOException e) {
            System.err.println("Client stream error: " +
                               e.getMessage());
            e.printStackTrace();
	} catch (ArrayIndexOutOfBoundsException e) {
            System.err.println("Please give two integers as arguments: " +
                               e.getMessage());         
	}

        //Close the resources
        out.close();
        in.close();
        sock.close();
    }
}

Top


Protocol.java

/*This code written by Nishant Agarwal
  Jul 12th, 2004
  [email protected]
*/
package protocol;

import java.io.*;

public interface Protocol {
    void clientAddRequest(String s1, String s2) throws IOException;
    void serverAddReply() throws IOException;
}

Top


MyProtocol.java

/*This code written by Nishant Agarwal
  Jul 12th, 2004
  [email protected]
*/

package protocol;

import java.io.*;
import java.net.*;

public class MyProtocol implements Protocol{
    private BufferedReader in;
    private PrintWriter out;

    public MyProtocol(BufferedReader in, PrintWriter out) {
        this.in  = in;
        this.out = out; 
    }

    public void clientAddRequest(String a, String b) throws IOException {
        out.println("Hello Server...");
        System.out.println("Server greeted: " + in.readLine());
        out.println(a);
        out.println(b);
        System.out.println("Server gave the sum of " + a + " and " + b +
                           " as: " + in.readLine());
        out.println("Bye");
        System.out.println(in.readLine());    
    }

    public void serverAddReply() throws IOException {
        in.readLine();
        out.println("This is server thread: " +
                    Thread.currentThread().toString());
        String num1 = in.readLine();
        String num2 = in.readLine();
        out.println((Integer.parseInt(num1) + Integer.parseInt(num2)));
        in.readLine();
        out.println("Bye");
    }
}

Top


MyServer.java

/*This code written by Nishant Agarwal
  Jul 12th, 2004
  [email protected]
*/

package server;

import java.io.*;
import java.net.*;

public class MyServer {
    public static void main(String[] argv) throws IOException {
        
        //Resources we need
        ServerSocket st = null;
        InetAddress addr = null; 
        boolean isRunning = true; 

        //initialze the resources
        try {
            st = new ServerSocket(51211);
            System.out.println("Server connected on host " +
                               addr.getLocalHost() + " on port " +
                               st.getLocalPort());                       
	} catch (UnknownHostException e) {
            System.err.println("Could not create sockets in server: " +
                               e.getMessage());
            e.printStackTrace();
            System.exit(-1);
	} 

        //now keep waiting for requests, spawn a thread 
        //to do the work whenever you get request
        while(isRunning == true) {
            new MyThread(st.accept()).t.start();
	}
    }
}

Top


MyThread.java

/*This code written by Nishant Agarwal
  Jul 12th, 2004
  [email protected]
*/

package server;

import java.io.*;
import java.net.*;
import protocol.*; 

public class MyThread implements Runnable {
    Thread t;
    Socket sock;

    public MyThread(Socket s) {
        sock = s;
        t = new Thread(this);
    }

    public void run() {
        //declare resources we need
        BufferedReader in = null;
        PrintWriter out = null;
        
        //initialize the resources
        try {
            in  = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            out = new PrintWriter(sock.getOutputStream(), true);
	} catch (IOException e) {
            System.err.println("Could not create streams in server thread: " +
                               e.getMessage());
            e.printStackTrace();
	} 

	//talk according to protocol
        Protocol protocol = new MyProtocol(in, out);
        try {
            protocol.serverAddReply();
	} catch (IOException e) {
            System.err.println("Error in talking to client in server thread: " +
                               Thread.currentThread().toString());
            e.getMessage();
            e.printStackTrace();
	} catch (NumberFormatException e) {
            System.err.println("Client did not give integers: " +
                               e.getMessage());
	}

        //close the resources
        try {
            in.close();
            out.close();
            sock.close();
        } catch (IOException e) {
            System.err.println("Server Thread could not clean up: " +
                               e.getMessage()); 
        }  
    }
}

Top


Counter
Hosted by www.Geocities.ws

1