A Gentle Guide to Socket in Java13 min read

What is Socket?

A socket is an interface to send and receive data in two programs on the network in a bidirectional manner. In other words, a socket represent one endpoint of two-way communication between two programs. A socket is determined by an address (hostname, machine name), for example 127.0.0.1 and a port number, range from 0 to 65535, so, a socket may have the following address: 127.0.0.1:12345.

Because the socket is an interface that facilitates the communication of 2 different programs on the network, hence, the concepts of client and server usually go along with it. On the server-side, the socket is utilized to bind a service to a specific port number, and as you know, on network communication, the ports from 0 to 1023 are classified as well-known ports, each of them may bind to a different service, for example, port 80 is for HTTP, port 25 is for SMTP (Simple Mail Transfer Protocol), and so on. Depends on your need, you might bind your server to a corresponding port. However, if you just start out working with Socket, it’s recommended to use a port greater than 1023.

To start the communication between client and server, once server on a computer has a socket that is bound to a specific port, the server now just waits and listen for incoming request from the client. On the client-side, it needs to know the hostname (i.e. the address) of the computer on which the server is running, and of course, the port in which the server listens to the incoming requests.

When the client sends a request to the server if the server accepts this request, the server then creates a socket to initialize the connection with the client, from now on, client and server can interact and send data to each other using their own socket. And of course, one server can serve multiple clients, not just one.

A typical example of socket usage is an instant messager. A client might send a message to the server, and upon the request from the client, the server can reply by sending a message back to the client.

Sockets in Java

This is what socket brings to the table from now, and it’s enough theory, let’s get our hands dirty. Let’s create some examples that illustrate how to use Sockets to create a server serving one client, then we learn to write Socket code for a server and a long connected client. And finally, we will examine how a single server can interact with multiple clients flexibly using the combinations between Socket classes and Thread.

Java Class Library offers us several classes to create interaction between two programs using sockets:

  • Socket class: represent one side of a two-way connection between a program and another program on the network (e.g. between client and server)
  • ServerSocket class: this can only be used on the server-side, which implements a socket that the server can use to wait, listen, and accept a connection request from the client.

Both Socket and ServerSocket classes are located on java.net package.

So, a client can create an interaction with the server using the Socket class, and on the server-side, it utilizes the ServerSocket class to wait and accept a connection from the client, once the connection between client and server has been established, the server will spawn a Socket class to interact with the client.

Depending on the server’s protocol, the code for creating interaction between client and server might slightly different. However, regardless of how simple or complicated the socket is, the basic outline for creating working with Sockets in Java is:

  • Open the socket
  • Open input stream and output stream to a socket
  • Read from and write to the stream
  • Close the streams
  • Close the socket

Single server with a single client request

For the sake of simplicity, in the first example of using a Socket, we will create an echo server that listens to a request from a client. A client sends a message to the server and then the server echo back the message was sent. First, open your favorite IDE and create 2 separated programs, one for client and one for server. Both containing the main method.

Write the server’s side code

The purpose of a server as we already know is to response requests from clients. For the server-side code, we first need to create a server socket in which it will listen and accept a connection from the client:

final int PORT = 7777;
final String ADDRESS = "localhost";
ServerSocket serverSocket = new ServerSocket(PORT, 50, InetAddress.getByName(address));
Socket socket = serverSocket.accept();

Let’s break down what’s happening here:

  • The interesting part here is that we create a ServerSocket instance that takes 3 arguments in its constructor. The first argument is the port number in which the server will listen to request from the client. The second argument is the number of backlog, which is the maximum length of queue for incoming connections. The third argument determines the IP address of the host, the InetAddress.getByName() method translates the textual representation of a IP address to its address, in our case, localhost will be convert to 127.0.0.1.
  • Next, the server waits infinitely until it receives a connection from the client and accept it by using the accept() method on a ServerSocket instance. This method returns a Socket instance which server will use to interact with the client later on.

We have bind our server to a specific address and port number, listening and accept a connection from the client. The next step is to create streams to send and retrieve data for the server. With that in mind, we need to use instances of OutputStream and InputStream:

DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());

On DataInputStream instance, we can use the readUTF() method read the string from the client. Accordingly, the writeUTF(str) on DataOutputStream instance will write to client a response string:

String msg = inputStream.readUTF(); // read data sent from the client
outputStream.writeUTF("The string: " + msg + " was sent to the server!"); // write the respond back to the client.

Here is the complete code for our server:

package server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.*;
/**
 * @author namvdo
 */
public class Server {
    private static final int PORT = 7777;
    private static final String ADDRESS = "localhost";

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT, 50, InetAddress.getByName(ADDRESS));
             Socket socket = serverSocket.accept();
             DataInputStream inputStream = new DataInputStream(socket.getInputStream());
             DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
        ) {
            String msg = inputStream.readUTF();
            outputStream.writeUTF("The message " + msg + " was sent to the server!");
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

The code might look a little bit different from what was represented but its function is still the same, here we use try-with-resources syntax to automatically close socket and streams to avoid resource leaks.

The readUTF() and writeUTF(str) methods on DataInputStream and DataOutputStream classes are not the only ways to read and write data from the stream. In essence, if you’re working with audio, movie, or an intensive amount of data you might want to work with some other class such as BufferedReader and PrintWriter classes.

Write the client side code

So the server needs at least one client to initialize the connection. For the client-side code, it will simply send a message to a sever and receive back a response. The primary thing we need to do is to create a socket for the client, this Socket instance will have the same IP address and port number as the server’s socket:

final int PORT = 7777;
final String ADDRESS = "localhost";
Socket socket = new Socket(InetAddress.getByName(ADDRESS), PORT);

The reason why need to use the same IP address and port number is that, needless to say, the server already has an address, bound its service to a specific port and listening for incoming requests from the client. The client need to specific exact these things in order to interact with the server.

Next, we again use OutputStream and InputStream instances to send and retrieve data from the server by using the writeUTF(str) and readUTF() method, respectively:

DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());

Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();

outputStream.writeUTF(msg); // sends data to the server
String receivedResponse = inputStream.readUTF(); // read data sent from the server
System.out.println(receivedResponse);

As we see, the procedure for reading and writing data from the stream on the client-side looks identical to what we did in the server code. We read a line from the standard input and send it to the server. Here’s the complete code of our client:

package client;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author namvdo
 */
public class Client {
    private static final int PORT = 7777;
    private static final String ADDRESS = "localhost";

    public static void main(String[] args) {
        try (Socket socket = new Socket(ADDRESS, PORT);
             DataInputStream inputStream = new DataInputStream(socket.getInputStream());
             DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
        ) {
            Scanner scanner = new Scanner(System.in);
            String msg = scanner.nextLine();
            outputStream.writeUTF(msg);
            String receivedResponse = inputStream.readUTF();
            System.out.println(receivedResponse);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Ideally, in real case scenarios, you might get the address of the server on another computer, so it’s good practice if you create an isolated configuration file or passing the address through command-line arguments.

OK, now let’s test it out. The server code must be run first, and then you can start the client program. Once the server starts, we run the client program and send a Hello World message to the server:

As can be observed, when the client sends a message to a server, the server will instantly send a message back to the client and then the program finished. Notice that Client and Server programs were both running.

Single server with multiple requests from the same client

In the previous example, the server serves one request from the client and then both client and server programs stop. But it doesn’t have to be this way, you can have one client and send multiple requests to the server and instantly get responses from the server. For example, the client sends 5 messages to the server, we just need to modify our previous code a bit.

For the server code:

for(int i = 0; i < 5; i++) {
    String msg = inputStream.readUTF();
    outputStream.writeUTF("The message " + msg + " was sent to the server!");
}

And for the client:

for(int i = 0; i < 5; i++) {
    Scanner scanner = new Scanner(System.in);
    String msg = scanner.nextLine();
    outputStream.writeUTF(msg);
    String receivedResponse = inputStream.readUTF();
    System.out.println(receivedResponse);
}

Here is the output of our modified program:

Single server with multiple clients

Now it comes to our appealing part, how can we use sockets to create a server that interacts with multiple clients? Of course, we can create multiple client programs and each of them will send some messages to the same server. However, our server runs on a single thread, everything is processed in a sequential manner, we cannot obtain responsiveness since the server needs to process all request from a client before moving on to the next one, our server cannot simultaneously interact with multiple clients.

To make our server more malleable and resilient, on our Server program, we can make use of the Thread class to create multiple threads. The main thread will listen and accept incoming requests from the clients. While our custom thread will serve requests from our accepted clients. In other words, our server still accepts new connections from the client sequentially, however, the server can handle their requests simultaneously, one thread per each client connection.

package server;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.*;
import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.Date;

/**
 * @author namvdo
 */
public class Server {
    private static final int PORT = 7777;
    private static final String ADDRESS = "localhost";

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT, 50, InetAddress.getByName(ADDRESS));
        ) {
            while (true) {
                ResponseThread response = new ResponseThread(serverSocket.accept());
                response.start(); // starts respond to the client on another thread
            }

        } catch(IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }

}

class ResponseThread extends Thread {
    private final Socket socket;
    public ResponseThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
            DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());){
            String msg;
            while(!(msg = inputStream.readUTF()).equals("exit")) {
                outputStream.writeUTF("The message: " + msg + " was sent to the server!");
                System.out.println("The message: " + msg + " was sent to the server!");
            }
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Inside the main method of our Server class, the server loops forever and sequentially listens and accepts new connections from the clients, when a new connection request from the client comes in, our Server accepts this connection. Next, upon the creation of ResponseThread instance, we call start() method on it which causes another thread to start to service the client message requests. For each client connection, a new thread will spawn to serve this client request.

Notice here that we don’t put serverSocket.accept() inside try-with-resources because this can cause Socket created in one thread and close in another one. This is really error-prone when a thread wants to use the socket while it is already be closed.

For now, if you want to try it out, create another program named Client2 which looks the same as our Client class. Start the Server program, then Client and Client2 programs. Here is the output that you might expected:

From Server:

From Client:

From Client2:

Conclusion

In this article, we have learned a socket is one endpoint of two-way communication between two programs over the network, next we have learned how to use Sockets in Java to create simple programs between client and server using ServerSocket and Socket classes, at the same time, we’ve demonstrated how DataInputStream, and DataOutputStream can be used to read data from and write data to a program. In the last session, we’ve also spent a little effort to apprehend how Thread class can help us to make a server simultaneously serves multiple client requests.

Previous Article

Subscribe to my newsletter to get weekly updates

Categories

Archives