0% found this document useful (0 votes)
2 views

Socket programming in Java

This document serves as a tutorial on socket programming in Java, covering both the traditional java.io package and the non-blocking I/O (NIO) APIs introduced in Java 1.4 and further expanded in NIO.2. It explains the differences between TCP and UDP protocols, provides examples of client-server communication using sockets, and introduces multithreading for handling multiple requests in a server. Additionally, it discusses the features of NIO and NIO.2, emphasizing their advantages for applications requiring intensive or asynchronous I/O operations.

Uploaded by

williamsayia82
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

Socket programming in Java

This document serves as a tutorial on socket programming in Java, covering both the traditional java.io package and the non-blocking I/O (NIO) APIs introduced in Java 1.4 and further expanded in NIO.2. It explains the differences between TCP and UDP protocols, provides examples of client-server communication using sockets, and introduces multithreading for handling multiple requests in a server. Additionally, it discusses the features of NIO and NIO.2, emphasizing their advantages for applications requiring intensive or asynchronous I/O operations.

Uploaded by

williamsayia82
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 22

Socket programming in Java

From simple I/O to non-blocking asynchronous channels in the Java socket


model
This tutorial is an introduction to socket programming in Java, starting with a simple client-
server example demonstrating the basic features of Java I/O. You'll be introduced to both the
original java.io package and NIO, the non-blocking I/O (java.nio) APIs introduced in
Java 1.4. Finally, you'll see an example that demonstrates Java networking as implemented from
Java 7 forward, in NIO.2.
Socket programming boils down to two systems communicating with one another. Generally,
network communication comes in two flavors: Transport Control Protocol (TCP) and User
Datagram Protocol (UDP). TCP and UDP are used for different purposes and both have unique
constraints:

 TCP is relatively simple and reliable protocol that enables a client to make a connection to a
server and the two systems to communicate. In TCP, each entity knows that its communication
payloads have been received.
 UDP is a connectionless protocol and is good for scenarios where you do not necessarily need
every packet to arrive at its destination, such as media streaming.

To appreciate the difference between TCP and UDP, consider what would happen if you were
streaming video from your favorite website and it dropped frames. Would you prefer that the
client slow down your movie to receive the missing frames or would you prefer that the video
continue playing? Video streaming protocols typically leverage UDP. Because TCP guarantees
delivery, it is the protocol of choice for HTTP, FTP, SMTP, POP3, and so forth.

In this tutorial, I introduce you to socket programming in Java. I present a series of client-server
examples that demonstrate features from the original Java I/O framework, then gradually
advance to using features introduced in NIO.2.

Old-school Java sockets

In implementations prior to NIO, Java TCP client socket code is handled by


the java.net.Socket class. The following code opens a connection to a server:

Socket socket = new Socket( server, port );

Once our socket instance is connected to the server we can start obtaining input and output
streams to the sever. Input streams are used to read data from the server while output streams are
used to write data to the server. We can execute the following methods to obtain input and output
streams:
InputStream in = socket.getInputStream();

OutputStream out = socket.getOutputStream();

Because these are ordinary streams, the same streams that we would use to read from and write
to a file, we can convert them to the form that best serves our use case. For example, we could
wrap the OutputStream with a PrintStream so that we can easily write text with methods
like println(). For another example, we could wrap the InputStream with
a BufferedReader, via an InputStreamReader, in order to easily read text with
methods like readLine().

Java socket client example


Let's work through a short example that executes an HTTP GET against an HTTP server. HTTP
is more sophisticated than our example permits, but we can write client code to handle the
simplest case: request a resource from the server and the server returns the response and closes
the stream. This case requires the following steps:

1. Create a socket to the web server listening on port 80.


2. Obtain a PrintStream to the server and send the request GET PATH HTTP/1.0,
where PATH is the requested resource on the server. For example, if we wanted to open the root of a web
site then the path would be /.
3. Obtain an InputStream to the server, wrap it with a BufferedReader and read the response
line-by-line.

Listing 1 shows the source code for this example.

Listing 1. SimpleSocketClientExample.java

package com.geekcap.javaworld.simplesocketclient;

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.io.PrintStream;

import java.net.Socket;

public class SimpleSocketClientExample

{
public static void main( String[] args )

if( args.length < 2 )

System.out.println( "Usage:
SimpleSocketClientExample <server> <path>" );

System.exit( 0 );

String server = args[ 0 ];

String path = args[ 1 ];

System.out.println( "Loading contents of URL: " + server


);

try

// Connect to the server

Socket socket = new Socket( server, 80 );

// Create input and output streams to read from and


write to the server

PrintStream out = new PrintStream(


socket.getOutputStream() );

BufferedReader in = new BufferedReader( new


InputStreamReader( socket.getInputStream() ) );

// Follow the HTTP protocol of GET <path> HTTP/1.0


followed by an empty line
out.println( "GET " + path + " HTTP/1.0" );

out.println();

// Read data from the server until we finish reading


the document

String line = in.readLine();

while( line != null )

System.out.println( line );

line = in.readLine();

// Close our streams

in.close();

out.close();

socket.close();

catch( Exception e )

e.printStackTrace();

}
Listing 1 accepts two command-line arguments: the server to connect to (assuming that we're
connecting to the server on port 80) and the resource to retrieve. It creates a Socket that points
to the server and explicitly specifies port 80. It then executes the command:

GET PATH HTTP/1.0

For example:

GET / HTTP/1.0

What just happened?

When you retrieve a web page from a web server, such as www.google.com, the HTTP client
uses DNS servers to find the server's address: it starts by asking the top-level domain server for
the com domain where the authoritative domain-name server is for the www.google.com.
Then it asks that domain-name server for the IP address (or addresses) for www.google.com.
Next, it opens a socket to that server on port 80. (Or, if you want to define a different port, you
can do so by adding a colon followed by the port number, for example: :8080.) Finally, the
HTTP client executes the specified HTTP method, such as GET, POST, PUT, DELETE, HEAD,
or OPTI/ONS. Each method has its own syntax. As shown in the above code snips,
the GET method requires a path followed by HTTP/version number and an empty line. If
we wanted to add HTTP headers we could have done so before entering the new line.

In Listing 1, we retrieved an OutputStream and wrapped it in a PrintStream so that we


could more easily execute our text-based commands. Our code obtained an InputStream,
wrapped that in an InputStreamReader, which converted it to a Reader, and then wrapped
that in a BufferedReader. We used the PrintStream to execute our GET method and
then used the BufferedReader to read the response line-by-line until we received
a null response, indicating that the socket had been closed.

Now execute this class and pass it the following arguments:

java
com.geekcap.javaworld.simplesocketclient.SimpleSocketClientExamp
le www.javaworld.com /

You should see output similar to what's below:

Loading contents of URL: www.javaworld.com

HTTP/1.1 200 OK
Date: Sun, 21 Sep 2014 22:20:13 GMT

Server: Apache

X-Gas_TTL: 10

Cache-Control: max-age=10

X-GasHost: gas2.usw

X-Cooking-With: Gasoline-Local

X-Gasoline-Age: 8

Content-Length: 168

Last-Modified: Tue, 24 Jan 2012 00:09:09 GMT

Etag: "60001b-a8-4b73af4bf3340"

Content-Type: text/html

Vary: Accept-Encoding

Connection: close

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8" />

<title>Gasoline Test Page</title>

</head>

<body>

<br><br>

<center>Success</center>

</body>

</html>
This output shows a test page on JavaWorld's website. It replied back that it speaks HTTP
version 1.1 and the response is 200 OK.

Java socket server example


We've covered the client side and fortunately the communication aspect of the server side is just
as easy. From a simplistic perspective, the process is as follows:

1. Create a ServerSocket, specifying a port to listen on.


2. Invoke the ServerSocket's accept() method to listen on the configured port for a client
connection.
3. When a client connects to the server, the accept() method returns a Socket through which the
server can communicate with the client. This is the same Socket class that we used for our client, so
the process is the same: obtain an InputStream to read from the client and
an OutputStream write to the client.
4. If you server needs to be scalable, you will want to pass the Socket to another thread to process so that
your server can continue listening for additional connections.
5. Call the ServerSocket's accept() method again to listen for another connection.

As you'll soon see, NIO's handling of this scenario would be a bit different. For now, though, we
can directly create a ServerSocket by passing it a port to listen on (more
about ServerSocketFactorys in the next section):

ServerSocket serverSocket = new ServerSocket( port );

And now we can accept incoming connections via the accept() method:

Socket socket = serverSocket.accept();

// Handle the connection ...

Multithreaded programming with Java sockets

Listing 2, below, puts all of the server code so far together into a slightly more robust example
that uses threads to handle multiple requests. The server shown is an echo server, meaning that it
echoes back any message it receives.

While the example in Listing 2 isn't complicated it does anticipate some of what's coming up in
the next section on NIO. Pay special attention to the amount of threading code we have to write
in order to build a server that can handle multiple simultaneous requests.

Listing 2. SimpleSocketServer.java

package com.geekcap.javaworld.simplesocketclient;
import java.io.BufferedReader;

import java.io.I/OException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

public class SimpleSocketServer extends Thread

private ServerSocket serverSocket;

private int port;

private boolean running = false;

public SimpleSocketServer( int port )

this.port = port;

public void startServer()

try

serverSocket = new ServerSocket( port );

this.start();
}

catch (I/OException e)

e.printStackTrace();

public void stopServer()

running = false;

this.interrupt();

@Override

public void run()

running = true;

while( running )

try

System.out.println( "Listening for a connection"


);

// Call accept() to receive the next connection


Socket socket = serverSocket.accept();

// Pass the socket to the RequestHandler thread


for processing

RequestHandler requestHandler = new


RequestHandler( socket );

requestHandler.start();

catch (I/OException e)

e.printStackTrace();

public static void main( String[] args )

if( args.length == 0 )

System.out.println( "Usage: SimpleSocketServer


<port>" );

System.exit( 0 );

int port = Integer.parseInt( args[ 0 ] );

System.out.println( "Start server on port: " + port );


SimpleSocketServer server = new SimpleSocketServer( port
);

server.startServer();

// Automatically shutdown in 1 minute

try

Thread.sleep( 60000 );

catch( Exception e )

e.printStackTrace();

server.stopServer();

class RequestHandler extends Thread

private Socket socket;

RequestHandler( Socket socket )

this.socket = socket;

}
@Override

public void run()

try

System.out.println( "Received a connection" );

// Get input and output streams

BufferedReader in = new BufferedReader( new


InputStreamReader( socket.getInputStream() ) );

PrintWriter out = new PrintWriter(


socket.getOutputStream() );

// Write out our header to the client

out.println( "Echo Server 1.0" );

out.flush();

// Echo lines back to the client until the client


closes the connection or we receive an empty line

String line = in.readLine();

while( line != null && line.length() > 0 )

out.println( "Echo: " + line );

out.flush();

line = in.readLine();
}

// Close our connection

in.close();

out.close();

socket.close();

System.out.println( "Connection closed" );

catch( Exception e )

e.printStackTrace();

In Listing 2 we create a new SimpleSocketServer instance and start the server. This is
required because the SimpleSocketServer extends Thread to create a new thread to
handle the blocking accept() call that you see in the read() method. The run() method
sits in a loop accepting client requests and creating RequestHandler threads to process the
request. Again, this is relatively simple code, but also involves a fair amount of threaded
programming.

Note too that the RequestHandler handles the client communication much like the code in
Listing 1 did: it wraps the OutputStream with a PrintStream to facilitate easy writes and,
similarly, wraps the InputStream with a BufferedReader for easy reads. As far as a
server goes, it reads lines from the client and echoes them back to the client. If the client sends
an empty line then the conversation is over and the RequestHandler closes the socket.

NIO and NIO.2


For many applications, the base Java socket programming model that we've just explored is
sufficient. For applications involving more intensive I/O or asynchronous input/output you will
want to be familiar with the non-blocking APIs introduced in Java NIO and NIO.2.
The JDK 1.4 NIO package offers the following key features:

 Channels are designed to support bulk transfers from one NIO buffer to another.
 Buffers represent a contiguous block of memory interfaced by a simple set of operations.
 Non-Blocking Input/Output is a set of classes that expose channels to common I/O sources like files
and sockets.

When programming with NIO, you open a channel to your destination and then read data into
a buffer from the destination, write the data to a buffer, and send that to your destination. We'll
dive into setting up a socket and obtaining a channel to it shortly, but first let's review the process
of using a buffer:

1. Write data into a buffer


2. Call the buffer's flip() method to prepare it for reading
3. Read data from the buffer
4. Call the buffer's clear() or compact() method to prepare it to receive more data

When data is written into the buffer, the buffer knows the amount of data written into it. It
maintains three properties, whose meanings differ if the buffer is in read mode or write mode:

 Position: In write mode, the initial position is 0 and it holds the current position being written to in the
buffer; after you flip a buffer to put it in read mode, it resets the position to 0 and holds the current
position in the buffer being read from,
 Capacity: The fixed size of the buffer
 Limit: In write mode, the limit defines how much data can be written into the buffer; in read mode, the
limit defines how much data can be read from the buffer.

Java I/O demo: Echo server with NIO.2


NIO.2, which was introduced in JDK 7, extends Java's non-blocking I/O libraries to add support
for filesystem tasks, such as the java.nio.file package
and java.nio.file.Path class and exposes a new File System API. With that background
in mind, let's write a new Echo Server using
NIO.2's AsynchronousServerSocketChannel.

More tips for using NIO and NIO.2


"NIO shines when it's used to boost processing performance, but its results are closely tied to the
underlying platform. For instance, you might discover that NIO's ability to accelerate application
performance depends not only on the OS, but on the specific JVM, host virtualization context,
mass storage characteristics, and even data..."
-- From "Five ways to maximize Java NIO and NIO.2"
The AsynchronousServerSocketChannel provides a non-blocking asynchronous
channel for stream-oriented listening sockets. In order to use it, we first execute its
static open() method and then bind() it to a specific port. Next, we'll execute
its accept() method, passing to it a class that implements
the CompletionHandler interface. Most often, you'll find that handler created as an
anonymous inner class.

Listing 3 shows the source code for our new asynchronous Echo Server.

Listing 3. SimpleSocketServer.java

package com.geekcap.javaworld.nio2;

import java.io.I/OException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.AsynchronousServerSocketChannel;

import java.nio.channels.AsynchronousSocketChannel;

import java.nio.channels.CompletionHandler;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.TimeoutException;

public class NioSocketServer

public NioSocketServer()

try

// Create an AsynchronousServerSocketChannel that


will listen on port 5000

final AsynchronousServerSocketChannel listener =


AsynchronousServerSocketChannel.open().bind(new
InetSocketAddress(5000));

// Listen for a new request

listener.accept( null, new


CompletionHandler<AsynchronousSocketChannel,Void>() {

@Override

public void completed(AsynchronousSocketChannel


ch, Void att)

// Accept the next connection

listener.accept( null, this );

// Greet the client

ch.write( ByteBuffer.wrap( "Hello, I am Echo


Server 2020, let's have an engaging conversation!\n".getBytes()
) );

// Allocate a byte buffer (4K) to read from


the client

ByteBuffer byteBuffer = ByteBuffer.allocate(


4096 );

try

// Read the first line

int bytesRead = ch.read( byteBuffer


).get( 20, TimeUnit.SECONDS );
boolean running = true;

while( bytesRead != -1 && running )

System.out.println( "bytes read: " +


bytesRead );

// Make sure that we have data to


read

if( byteBuffer.position() > 2 )

// Make the buffer ready to read

byteBuffer.flip();

// Convert the buffer into a


line

byte[] lineBytes = new byte[


bytesRead ];

byteBuffer.get( lineBytes, 0,
bytesRead );

String line = new String(


lineBytes );

// Debug

System.out.println( "Message: "


+ line );

// Echo back to the caller


ch.write( ByteBuffer.wrap(
line.getBytes() ) );

// Make the buffer ready to


write

byteBuffer.clear();

// Read the next line

bytesRead = ch.read( byteBuffer


).get( 20, TimeUnit.SECONDS );

else

// An empty line signifies the


end of the conversation in our protocol

running = false;

catch (InterruptedException e)

e.printStackTrace();

catch (ExecutionException e)

e.printStackTrace();

}
catch (TimeoutException e)

// The user exceeded the 20 second


timeout, so close the connection

ch.write( ByteBuffer.wrap( "Good


Bye\n".getBytes() ) );

System.out.println( "Connection timed


out, closing connection" );

System.out.println( "End of conversation" );

try

// Close the connection if we need to

if( ch.isOpen() )

ch.close();

catch (I/OException e1)

e1.printStackTrace();

@Override
public void failed(Throwable exc, Void att) {

///...

});

catch (I/OException e)

e.printStackTrace();

public static void main( String[] args )

NioSocketServer server = new NioSocketServer();

try

Thread.sleep( 60000 );

catch( Exception e )

e.printStackTrace();

}
In Listing 3 we first create a new AsynchronousServerSocketChannel and then bind it
to port 5000:

final AsynchronousServerSocketChannel listener =

AsynchronousServerSocketChannel.open().bind(new
InetSocketAddress(5000));

From this AsynchronousServerSocketChannel, we invoke accept() to tell it to start


listening for connections, passing to it a custom CompletionHandler instance. When we
invoke accept(), it returns immediately. Note that this example is different from
the ServerSocket class in Listing 1; whereas the accept() method blocked until a client
connected to it, the AsynchronousServerSocketChannel accept() method handles it
for us.

The completion handler

Our next responsibility is to create a CompletionHandler class and provide an


implementation of the completed() and failed() methods. The completed() method
is called when the AsynchronousServerSocketChannel receives a connection from a
client and it includes an AsynchronousSocketChannel to the client.
The completed() method first accepts the connection from
the AsynchronousServerSocketChannel and then starts communicating with the client.
The first thing that it does is write out a "Hello" message: It builds a string, converts it to a byte
array, and then passes it to ByteBuffer.wrap() to construct a ByteBuffer.
The ByteBuffer can then be
passed AsynchronousSocketChannel's write() method.

To read from the client, we create a new ByteBuffer by invoking


its allocate(4096) (which creates a 4K buffer), then we invoke
the AsynchronousSocketChannel's read() method. The read() returns
a Future<Integer> on which we can invoke get() to retrieve the number of bytes read
from the client. In this example, we pass get() a timeout value of 20 seconds: if we do not get
a response in 20 seconds then the get() method will throw a TimeoutException. Our rule
for this echo server is that if we observe 20 seconds of silence then we terminate the
conversation.

Futures in asynchronous computation


"The Future<V> interface represents the result of an asynchronous computation. The result is
known as a future because it typically will not be available until some moment in the future. You
can invoke methods to cancel a task, return a task's result (waiting indefinitely or for a timeout to
elapse when the task hasn't finished), and determine if a task has been cancelled or has
finished..."
--From "Java concurrency without the pain, Part 1"
Next we check the position of the buffer, which will be the location of the last byte received
from the client. If the client sends an empty line then we receive two bytes: a carriage return and
a line feed. The check ensures that if the client sends a blank line that we take it as an indicator
that the client is finished with the conversation. If we have meaningful data then we call
the ByteBuffer's flip() method to prepare it for reading. We create a temporary byte array
to hold the number of bytes read from the client and then invoke the ByteBuffer's get() to
load data into that byte array. Finally, we convert the byte array to a string by creating a
new String instance. We echo the line back to the client by converting the string to a byte
array, passing that to the ByteBuffer.wrap() method and invoking
the AsynchronousSocketChannel's write() method. Now
we clear() the ByteBuffer, which recall means that it repositions the position to zero
and puts the ByteBuffer into write mode, and then we read the next line from the client.

The only thing to be aware of is that the main() method, which creates the server, also sets up
a 60 second timer to keep the application running. Because
the AsynchronousSocketChannel's accept() method returns immediately, if we don't
have the Thread.sleep() then our application will stop immediately.

To test this out, launch the server and connect to it using a telnet client:

telnet localhost 5000

Send a few strings to the server, observe that they are echoed back to you, and then send an
empty line to terminate the conversation.

In conclusion
In this article I've presented two approaches to socket programming with Java: the traditional
approach introduced with Java 1.0 and the newer, non-blocking NIO and NIO.2 approaches
introduced in Java 1.4 and Java 7, respectively. You've seen several iterations of a Java socket
client and a Java socket server example, demonstrating both the utility of basic Java I/O and
some scenarios where non-blocking I/O improves the Java socket programming model. Using
non-blocking I/O, you can program Java networked applications to handle multiple simultaneous
connections without having to manage multiple thread collections. You can also take advantage
of the new server scalability that is built in to NIO and NIO.2.

You might also like