Implementing Servers in Java

Now that we have implemented a basic network client that receives data from the Internet, let’s program a simple server that can send information to clients.

1. Server Sockets

A server program, when started, waits for a client to attach to its port. For our example program, we chose port number 8189, which is not used by any of the standard services. The ServerSocket class establishes a socket. In our case, the command

var s = new ServerSocket(8189);

establishes a server that monitors port 8189. The command

Socket incoming = s.accept();

tells the program to wait indefinitely until a client connects to that port. Once someone connects to this port by sending the correct request over the network, this method returns a Socket object that represents the connection that was made. You can use this object to get input and output streams, as is shown in the following code:

InputStream inStream = incoming.getInputStream();

OutputStream outStream = incoming.getOutputStream();

Everything that the server sends to the server output stream becomes the input of the client program, and all the output from the client program ends up in the server input stream.

In all the examples in this chapter, we transmit text through sockets. We therefore turn the streams into scanners and writers.

var in = new Scanner(inStream, StandardCharsets.UTF_8);

var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8),

true /* autoFlush */);

Let’s send the client a greeting:

out.println(“Hello! Enter BYE to exit.”);

When you use telnet to connect to this server program at port 8189, you will see this greeting on the terminal screen.

In this simple server, we just read the client’s input, a line at a time, and echo it. This demonstrates that the program receives the input. An actual server would obviously compute and return an answer depending on the input.

String tine = in.nextLine();

out.printtn(“Echo: ” + tine);

if (tine.trim().equats(“BYE”)) done = true;

In the end, we close the incoming socket.

incoming.dose();

That is all there is to it. Every server program, such as an HTTP web server, continues performing this loop:

  1. It receives a command from the client (“get me this information”) through an incoming data stream.
  2. It decodes the client command.
  3. It gathers the information that the client requested.
  4. It sends the information to the client through the outgoing data stream

Listing 4.3 is the complete program.

To try it out, compile and run the program. Then use telnet to connect to the server localhost (or IP address 127.0.0.1) and port 8189.

If you are connected directly to the Internet, anyone in the world can access your echo server, provided they know your IP address and the magic port number.

When you connect to the port, you will see the message shown in Figure 4.4:

Hello! Enter BYE to exit.

Type anything and watch the input echo on your screen. Type BYE (all uppercase letters) to disconnect. The server program will terminate as well.

2. Serving Multiple Clients

There is one problem with the simple server in the preceding example. Sup­pose we want to allow multiple clients to connect to our server at the same time. Typically, a server runs constantly on a server computer, and clients from all over the Internet might want to use it at the same time. Without support for multiple connections, any one client can monopolize the service by connecting to it for a long time. We can do much better through the magic of threads.

Every time we know the program has established a new socket connec­tion—that is, every time the call to accept() returns a socket—we will launch a new thread to take care of the connection between the server and that client. The main program will just go back and wait for the next connection. For this to happen, the main loop of the server should look like this:

white (true)

{

Socket incoming = s.accept();

var r = new ThreadedEchoHandler(incoming);

var t = new Thread(r);

t.start();

}

The ThreadedEchoHandter class implements Runnabte and contains the communication loop with the client in its run method.

class ThreadedEchoHandler implements Runnable

{

public void run()

{

try (InputStream inStream = incoming.getInputStream();

OutputStream outStream = incoming.getOutputStream())

{

Process input and send response

}

catch(IOException e)

{

Handle exception

}

}

}

When each connection starts a new thread, multiple clients can connect to the server at the same time. You can easily check this out.

  1. Compile and run the server program (Listing 4.4).
  2. Open several telnet windows as we have in Figure 4.5.
  3. Switch between windows and type commands. Note that you can communicate through all of them simultaneously.
  4. When done, switch to the window from which you launched the server program and press Ctrl+C to kill it.

3. Half-Close

The half-close allows one end of a socket connection to terminate its output while still receiving data from the other end.

Here is a typical situation. Suppose you transmit data to the server but you don’t know at the outset how much data you have. With a file, you’d just close the file at the end of the data. However, if you close a socket, you immediately disconnect from the server and cannot read the response.

The half-close solves this problem. You can close the output stream of a socket, thereby indicating to the server the end of the requested data, but keep the input stream open.

The client side looks like this:

try (var socket = new Socket(host, port))

{

var in = new Scanner(socket.getInputStream(), StandardCharsets.UTF_8);

var writer = new PrintWriter(socket.getOutputStream());

// send request data writer.print(. . .);

writer.flush();

socket.shutdownOutput();

// now socket is half-closed

// read response data

while (in.hasNextLine() != null)

{

String line = in.nextLine();

}

}

The server side simply reads input until the end of the input stream is reached. Then it sends the response.

Of course, this protocol is only useful for one-shot services such as HTTP where the client connects, issues a request, catches the response, and then disconnects.

4. Interruptible Sockets

When you connect to a socket, the current thread blocks until the connection has been established or a timeout has elapsed. Similarly, when you read data through a socket, the current thread blocks until the operation is successful or has timed out. (There is no timeout for writing data.)

In interactive applications, you would like to give users an option to simply cancel a socket connection that does not appear to produce results. However, if a thread blocks on an unresponsive socket, you cannot unblock it by calling interrupt.

To interrupt a socket operation, use a SocketChannel, a feature of the java.nio package. Open the SocketChannel like this:

SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));

A channel does not have associated streams. Instead, it has read and write methods that make use of Buffer objects. (See Chapter 1 for more informa­tion about NIO buffers.) These methods are declared in the interfaces ReadabteByteChannet and WritabteByteChannet.

If you don’t want to deal with buffers, you can use the Scanner class to read from a SocketChannet because Scanner has a constructor with a ReadabteByteChannet parameter:

var in = new Scanner(channet, StandardCharsets.UTF_8);

To turn a channel into an output stream, use the static Channets.newOutputStream method.

OutputStream outStream = Channets.newOutputStream(channet);

That’s all you need to do. Whenever a thread is interrupted during an open, read, or write operation, the operation does not block, but is terminated with an exception.

The program in Listing 4.5 contrasts interruptible and blocking sockets. A server sends numbers and pretends to be stuck after the tenth number. Click on either button, and a thread is started that connects to the server and prints the output. The first thread uses an interruptible socket; the second thread uses a blocking socket. If you click the Cancel button within the first ten numbers, you can interrupt either thread.

However, after the first ten numbers, you can only interrupt the first thread. The second thread keeps blocking until the server finally closes the connection (see Figure 4.6).

Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *