Chapter 20: Network Programming

20.1: IP Address and Port Number

Each machine which is on-line has an IP (Internet Protocol) address. One which is connected to the Internet through LAN has a fixed IP address, while one using dial-up ISP (Internet Service Provider) is assigned a temporary IP address each time dialing up.

There are two formats to represent an IP. One is DNS (domain name service) format such as "monash.edu.au", the other is the dotted quad format such as "130.194.227.34". Through a standard algorithm these two formats can be uniquely converted between one and another.

Class java.net.InetAddress is used to represent an IP address. There is no constructor of this class, the only way to create an InetAddress object is to call its static method getByName and provide it with one of the two formats of IP address. This class also encapsulates the standard algorithm, so that once you have an InetAddress object you can get from it any format of the IP address.

   
   import java.net.*;
   import java.io.*;
   
   public class Test {
   
       public static void main(String [] argu)
       {
           InetAddress ia1 = null;
           try {
               ia1 = InetAddress.getByName("CA_BUS_LT201_19");
               System.out.println("This machine's IP is: " + ia1.getHostName()
                              + " or " + ia1.getHostAddress());
           }
           catch(UnknownHostException e) {}
       }
   }

Output will be:

   
   This machine's IP is: CA_BUS_LT201_19 or 130.194.227.34

Especially, IP address "localhost" or "127.0.0.1" represents the local machine on which the code is running. A local host InetAddress object can also be created with

   
   InetAddress ia1 = InetAddress.getByName(null);

One IP address is used to locate one on-line machine, but one machine may have several programs such as servers and clients running. Each program is assigned an unique port number in this machine. When you want to locate a specific program on a specific machine you must provide both the IP address of the machine and the port number of the program.

20.2: Internet Protocols - TCP/IP & UDP

TCP (Transmission Control Protocol) are also known as stream-based sockets, because communication are achieved through a stable and constant network connection. Correct data transmission is guaranteed. It allows resending of lost data, and bytes are received in the same order that they are sent - because the connection is like a stream.

Class java.net.Socket is used to setup a network connection with TCP/IP.

TCP/IP's reliability pays. It has a high overhead. Compared with it, UDP (User Datagram Protocol) is less reliable but quicker. A big block of data may be broken into packets. Class java.net.DatagramSocket is used to send and receive data, and java.net.DatagramPacket is used to send and receive data in packets.

20.3: TCP/IP: ServerSocket & Socket

* Server

A server is a service provider. It does not need to know where its clients are, instead it assumes that clients know its address. Once a client find a server, the client will first leave its own address to the server, so that a two-way connection can be set up and data can be sent back and forth. When the service is finished, the connection is disconnected.

In Java, Socket is used on both server and client end for the stream connection. ServerSocket is used to set up the server. You only need to provide the port number to its constructor. If creation of the ServerSocket fails, an IOException will be thrown.

Once created, ServerSocket's accept method keeps listening on the net for clients, just like the switch board of a company. The thread blocks here until a client contact is received.

* Client

As a client, a Socket object is created straight-away with the server's IP address and port number. It is automatically assigned the first available port number of that machine. Every time you start the client program, its port number will increment by 1, until the computer is restarted.

The socket will try to make contact with the server. If the creation of connection fails, an UnknownHostException or an IOException will be thrown. If it succeeds, it will leave its IP address and port number to the server, and the ServerSocket's accept method will use them to create a new Socket object. Then on both ends there are two same sockets connected by one connection.

An InputStream and an OutputStream can be returned by Socket's getInputStream and getOutputStream method. Then a program can read from or write into the socket just like a file.

A simple server program:

   
   import java.net.*;
   import java.io.*;
   
   public class Test1 {
       public static void main(String [] argu) throws IOException
       {
           ServerSocket serverSocket = new ServerSocket(12000);
           try {
           Socket socket = serverSocket. accept();
               try {
                   PrintWriter pw = new PrintWriter(
                         new OutputStreamWriter(socket.getOutputStream()), true);
                   BufferedReader br = new BufferedReader(
                   new InputStreamReader(socket. getInputStream()));
   
                   System.out.println("Server: From client: " + br. readLine());
                   pw.println("Greeting from Server!");
               }
               catch(IOException e)
               {   socket.close(); }
           }
           catch(IOException e)
           {   serverSocket.close(); }
       }
   }

A simple client program:

   
   import java.net.*;
   import java.io.*;
   
   public class Test2 {
       public static void main(String [] argu) throws IOException
       {
           Socket socket = new Socket("localhost", 12000);
           try {
               PrintWriter pw = new PrintWriter(
                     new OutputStreamWriter(socket.getOutputStream()), true);
               BufferedReader br = new BufferedReader(
                     new InputStreamReader(socket.getInputStream()));
   
               pw.println("Greeting from Clinet!");
               System.out.println("Clinet: From server: " + br.readLine());
           }
           catch(IOException e)
           {   socket.close();  }
       }
   }

Once connection set up, the client will first write a line into the socket and the server will display it, and then it will write a line into its socket and the server will display it.

20.4: Will Blocked IO Block the Connection?

One important concept to notice: if a read from the socket blocks waiting for the other end to write something into the socket, it will not block the connection between the two sockets. Another thread can still write into the same connection.

In the following example, the server first creates a separate writing thread which keeps writing into the socket, then blocks at readLine. The client first creates a separate reading thread which keeps reading from the socket, then wait for a keyboard input to write a line into the socket.

When running the two processes you can see that before you provide the keyboard input to the client so that it can write a line to the server, the server's main thread blocks at the readLine, but it does not prevent the server's writing thread to write lines periodically to the client.

Server program:

   
   import java.net.*;
   import java.io.*;
   
   public class Test3 {
       static PrintWriter pw = null;
       static BufferedReader br = null;
   
       public static void main(String [] argu)
       {
           Test3 t1 = new Test3();
           try {  t1.startRun(); }
           catch(IOException e)
           {   System.out.println("ServerSocket can not be created!"); }
       }
   
       public void startRun() throws IOException
       {
           ServerSocket serverSocket = new ServerSocket(12000);
   
           try {
               Socket socket = serverSocket.accept();
   
               try {
                   pw = new PrintWriter(
                        new OutputStreamWriter(socket.getOutputStream()), true);
                   br = new BufferedReader(
                        new InputStreamReader(socket.getInputStream()));
   
                   new WriteThread().start();
                   System.out.println("Server: From client: " + br.readLine());
                   //new WriteThread().start();
               }
               catch(IOException e)
               {   
                   socket.close();  
                   serverSocket.close();
               }
           }
           catch(IOException e)
           {   serverSocket.close();  }
       }
   
       static class WriteThread extends Thread {
           public void run()
           {
               while(true)
               {
                   pw.println("From server's thread!");
                   try { sleep(1500); }
                   catch(InterruptedException e) {}
               }
           }
       }
   }
   

Client program:

   
   import java.net.*;
   import java.io.*;
   
   public class Test4 {
       static PrintWriter pw = null;
       static BufferedReader br = null;
   
       public static void main(String [] argu)
       {
           Test4 t1 = new Test4();
           try {  t1.startRun(); }
           catch(IOException e)
           {   System.out.println("Socket can not be created!");  }
       }
   
       public void startRun() throws IOException
       {
           InetAddress ia = InetAddress.getByName("localhost");
           Socket socket = new Socket(ia, 12000);
   
           try {
               pw = new PrintWriter(
                    new OutputStreamWriter(socket.getOutputStream()), true);
               br = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
   
               new ReadThread().start();
   
               //The following two lines are just to create a pause
               DataInputStream dis = new DataInputStream(System.in);
               dis.read();
   
               pw.println("Greeting from Clinet!");
           }
           catch(IOException e)
           {   socket.close();  }
       }
   
       static class ReadThread extends Thread {
           public void run()
           {
               while(true)
               {
                   try {
                       System.out.println("Clinet: From server: " + br.readLine());
                       sleep(200);
                   }
                   catch(IOException e) {}
                   catch(InterruptedException e) {}
               }
           }
       }
   }

20.5: Clean up and Exception Handling for ServerSocket & Socket

There are three kinds of possible errors for a server program, which will all throw IOException:

  1. The creation of ServerSocket fails
  2. The creation of Socket fails
  3. The creation of InputStream or OutputStream fails

For a client program, there are only No 2 and 3.

Sockets should be carefully cleaned up, because they use important non-memory resource.

A finally block instead of a catch block may be used to close the socket whether there is an exception or the program ends normally. However, if there are multiple threads sharing the same socket, you can not use finally block to clean up the socket, otherwise one thread may close the socket before the rest finish.

Generally speaking, if a program has multiple threads, clean up work should be clearly planed and assigned to the correct thread.

20.6: Serving Multiple Clients: Multiple Threads

To enable a server to serve multiple clients, you create one ServerSocket, include its accept method in a loop, and for each client connection a new Socket is created and passed to a new thread.

Server program:

   
   import java.net.*;
   import java.io.*;
   
   class ServeMany {
       public ServeMany() throws IOException
       {
           ServerSocket serverSocket = new ServerSocket(12000);
           Socket socket = null;
           while(true)
           {
               try {
                   socket = serverSocket.accept();
                   new ServerOne(socket);
               }
               catch(IOException e)
               {   serverSocket.close(); }
           }
       }
   
       private class ServerOne extends Thread {
           private Socket socket = null;
           PrintWriter pw = null;
   
           public ServerOne(Socket socket) throws IOException
           {
               this.socket = socket;
               try {
                   pw = new PrintWriter(
                         new OutputStreamWriter(socket.getOutputStream()), true);
                   start();
               }
               catch(IOException e)
               {   socket.close(); } // if pw fails, this socket will be closed,
           }                         // but ServerSocket will not.
   
           public void run()
           {
               while(true)
               {
                   pw.println("Greetings from server!");
                   try {  sleep(1500); }
                   catch(InterruptedException e) {}
               }
           }
       }
   }
   
   public class Test {
       public static void main(String [] argu)
       {
           try {  ServeMany sm1 = new ServeMany(); }
           catch(IOException e)
           {   System.out.println("Server can not be created!"); }
       }
   }
   

Client program:

   
   import java.net.*;
   import java.io.*;
   
   public class Test2 {
       static Socket socket = null;
       static BufferedReader br = null;
   
       public static void main(String [] argu)
       {
           try {
               socket = new Socket("localhost", 12000);
   
               try {
                   br = new BufferedReader(
                         new InputStreamReader(socket.getInputStream()));
   
                   while(true)
                       System.out.println("Clinet: From server: " + br.readLine());
               }
               catch(IOException e)
               {   socket.close(); }
           }
           catch(IOException e){}
       }
   }

20.7: UDP: DatagramSocket & DatagramPacket

Under TCP/IP, a constant connection is kept between the server and the client until the communication is finished. Under UDP, there is no connection. Data are sent as packets, just like mail packages. Every site uses the same DatagramSocket to send and receive DatagramPackets.

When you construct a DatagramPacket, if it is for receiving data, which will be passed to DatagramSocket's receive method, you only need to pass a byte [] array and the size of the array to its constructor. The maximum size of the array is slightly less than 64Kb.

If the DatagramPacket is used to send data, which will be passed to DatagramSocket's send method, you should pass to its constructor a byte [] array, a length, an InetAddress and a port number. The length can be shorter than the size of the array, indicating that you only want to send part of the array.

A packet on its way contains the IP address and port number of both the sender and the receiver. When you create a packet to be sent you already specify the receiver's IP address and port number, and when your DatagramSocket sends the packet, it automatically adds your own address and port number to the packet.

You will never need to extract the receiver's address of a packet, because the packet is on your hand, you must be the receiver. To extract the sender's address and port number, DatagramPacket has two methods: getPort and getAddress. To extract the byte [] array from the packet, use method getData.

The following two DatagramSockets send packets to each other. The first one has an infinite loop, which receives a packet, add something, then send it back to its sender. The second one sends a packet, then receives the feedback and displays it.

   
   import java.net.*;
   import java.io.*;
   
   public class Test1 {
       public static void main(String [] args) throws SocketException
       {
           DatagramSocket ds = new DatagramSocket(12000);
           byte [] array1 = new byte[100];
           DatagramPacket receive = new DatagramPacket(array1, 100);
           try {
               while(true)
               {
                   ds. receive(receive);
                   InetAddress ad = receive. getAddress();
                   int port = receive. getPort();
                   String s1 = new String(receive. getData());
                   s1 = "\"" + s1.trim() + "\" Sounds good!";
                   byte [] array2 = s1.getBytes();
                   DatagramPacket send = 
                        new DatagramPacket(array2, array2.length, ad, port);
                   ds. send(send);
               }
           }
           catch(IOException e)
           {   ds. close();  }
       }
   }
   
   
   import java.net.*;
   import java.io.*;
   
   public class Test2 {
       public static void main(String [] args) 
            throws SocketException, UnknownHostException
       {
           DatagramSocket ds = new DatagramSocket(13000);
           byte [] array1 = "Hi, Frank, how about dinner tonight?". getBytes();
           InetAddress ad = InetAddress.getByName("localhost");
           DatagramPacket send = 
                  new DatagramPacket(array1, array1.length, ad, 12000);
           try {
               ds. send(send);
   
               byte [] array2 = new byte[100];
               DatagramPacket receive = new DatagramPacket(array2, 100);
               ds. receive(receive);
               String s = new String(receive. getData());
               System.out.println("Feedback is: \n" + s);
           }
           catch(IOException e)
           {   ds. close();  }
       }
   }

Notice that unlike the communication pattern under TCP/IP where there is a distinct difference between the server and the client, here the two parties are exactly the same. Two DatagramSockets are assigned two different port numbers, and their IP address are both "localhost".

For the client program, you can leave out the port number "13000" when you create the DatagramSocket, because it does not need a fixed port number, and the computer will automatically assign one to it.