Client-Server in Java for the Uninitiated

Disclaimer: This is just an informal introduction to get things going. The analogies may be horrendously wrong. I claim no formal knowledge in CN at the time of writing :P

It’s surprisingly simple to write a program (like a mechanized monkey) without knowing what’s going on under the hood. And that is exactly what this post will enable you to do.

The idea is to make processes in different computers talk to each other. We’ll limit ourselves to a very simple scenario here. To start off, let’s talk about the client and server. Both the server and client are machines (can be different or the same). They are identified by an IP address. The server is always on the lookout for some client trying to connect to it. A client connects to a server to get or send some information. You might be buzzing with questions now. Or be blank because the assignment takes higher priority. Either way, looking at code tends to have a calming effect.

All this communication between two machines is achieved through Sockets. A socket can be thought of as a communication channel or box. Creating a server that waits for a connection request from a client is as simple as

//Server.java

const int PortNumber = 5000; // User port numbers are >= 1024
ServerSocket s = new ServerSocket(PortNumber);
Socket skt = s.accept();

Here, the accept method blocks the thread till a connection request arrives. For the lazy testers out there, both the Client and Server programs can be executed with the same driver file with two threads. More on that later. It’s normally a good idea to use try...catch to print the exception trace while debugging/fiddling. Creating a client is again, simple.

// Client.java

const int PortNumber = 5000; // User port numbers are >= 1024
Socket skt = new Socket("localhost", PortNumber);

This throws a connection refused error if the port number is not open in localhost. The server created has the address 0.0.0.0 (a wildcard that says the server is scanning for requests from any address in the network). Printing the ServerSocket or Socket object gives the address and port numbers. Now that we’ve identified the skeleton that connects machines, we’ll move on to actually passing data.

//Server.java

const int PortNumber = 5000; // User port numbers are >= 1024
ServerSocket s = new ServerSocket(PortNumber);
Socket skt = s.accept();

PrintWriter out = new PrintWriter(skt.getOutputStream(),true);
out.print("I don't always start with Hello World.");
out.close();
skt.close();
s.close();

Once the server has received a connection request from a client, the client communicates through a randomly chosen port to the server and the server, through 5000 to the client. Here, the server sends “I don’t always start with Hello World.” to the client through port 5000. Then closes the writer and the socket. Then, it terminates the server. Now, the client has to get this message.

//Client.java

const int PortNumber = 5000; // User port numbers are >= 1024
try{
  Socket skt = new Socket("localhost", PortNumber);
  BufferedReader in = new BufferedReader(new InputStreamReader(skt.getInputStream()));
  while(!in.ready()); // waits till there is some input to read
  System.out.println(in.readLine());
  in.close();
  skt.close();
} catch(Exception e){
  System.out.println(e.toString());
}

Here, the client connects to the server and waits for the server to send something through in.ready(). Once the server sends the client a message, the client prints it and closes the socket. Individually, these two are easy to understand and write. If the aim is to run both the client and server with the same driver instance, separate threads have to be created.

//Driver.java
Thread cli,ser;
cli = new Thread(){
  public void run(){
    Client c;
    c = new Client();
    c.start();
  }
};
ser = new Thread(){
  public void run(){
    Server s;
    s = new Server();
    s.start();
    }
};
cli.start();
ser.start();

If you’ve not read the documentation properly, like me, you’ll realize that calling the run() function is a futile effort in testing the program. start() creates a new thread, run() runs the thread in the thread that calls run(). You might even end up debugging this program for a shameful 30 minutes.

If you notice, the client thread is started before the server thread. Even if it’s the other way around, let’s say due to some crazy incident, the client gets started before the server is started. In that case, in Client.java, the line Socket skt = new Socket("localhost", PortNumber); will happily throw an error and terminate the client thread. The server thread, after slowly starting a server, will wait for a request. And the program will keep waiting. To fix this, we add code in Client.java to periodically check if the port is accepting connections.

//Client.java

const int PortNumber = 5000; // User port numbers are >= 1024
try{
  
  boolean scanning = true;
  Socket skt = new Socket();
  while(scanning){
    try{
      skt = new Socket("localhost", PortNumber);
      scanning = false;
    } catch(Exception e){
      System.out.println("Failed to establish connection. Trying again after 1 second.");
      Thread.sleep(1000);
    }
  }
  
  BufferedReader in = new BufferedReader(new InputStreamReader(skt.getInputStream()));
  while(!in.ready()); // waits till there is some input to read
  System.out.println(in.readLine());
  in.close();
  skt.close();
} catch(Exception e){
  System.out.println(e.toString());
}

The code to do this is quite self explanatory. If the connection is established, scanning is set to false and the program moves to reading the input. Otherwise, the thread running the Client object sleeps for a second and tries again. This works for one Client trying to tlak with the one Server. If there are more Clients, then Server.java needs a simple modification. The accept() method has to be called once for each Client.

//Server.java

const int PortNumber = 5000; // User port numbers are >= 1024
ServerSocket s = new ServerSocket(PortNumber);
Socket skt = new Socket();

while(<condition>){ // for example, no_of_servers_left > 0
  skt = s.accept();
  PrintWriter out = new PrintWriter(skt.getOutputStream(),true);
  out.print("I don't always start with Hello World.");
  out.close();
  skt.close();
  // modify <condition> to break out of loop. Example: no_of_servers_left--;
}

s.close();

Here, the condition can simply be true. For a variable number of Clients, a mechanism to break out of this has to be introduced. One way to do that is to interrupt the ServerSocket’s accept method from another thread. For that, the code needs to be modified. The ServerSocket object must be shared by the thread running accept() and the parent thread which may decide to close the server. To do that, ServerSocket s is declared globally as volatile ServerSocket s and s.close() is called from the parent thread. The volatile keyword makes sure all threads sharing the object see the same values. It reads and writes directly to main memory instead of the CPU cache and disables JVM’s instruction reordering as a part of optimization. There is no need for synchronization here. Another way to achieve this is to set a finite timeout for waiting for a Client to request a connection. This is done by using the setSoTimeout() method.

That’s it! It should be good enough to give you a fuzzy idea about the big picture of what’s going on.

Back