Movatterモバイル変換


[0]ホーム

URL:


Steven Haines
Contributor

Socket programming in Java: A tutorial

how-to
Jan 8, 202018 mins

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

jw scalable sockets networks
Credit: Stephen D. Strowes
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 aconnectionless 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 thejava.net.Socket class. The following code opens a connection to a server:

Socket socket = new Socket( server, port );

Once oursocket 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 theOutputStream with aPrintStream so that we can easily write text with methods likeprintln(). For another example, we could wrap theInputStream with aBufferedReader, via anInputStreamReader, in order to easily read text with methods likereadLine().

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 aPrintStream to the server and send the requestGET PATH HTTP/1.0, wherePATH 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 anInputStream to the server, wrap it with aBufferedReader 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 aSocket that points to the server and explicitly specifies port80. 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 aswww.google.com, the HTTP client uses DNS servers to find the server’s address: it starts by asking the top-level domain server for thecom domain where the authoritative domain-name server is for thewww.google.com. Then it asks that domain-name server for the IP address (or addresses) forwww.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 asGET,POST,PUT,DELETE,HEAD, orOPTI/ONS. Each method has its own syntax. As shown in the above code snips, theGET method requires a path followed byHTTP/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 anOutputStream and wrapped it in aPrintStream so that we could more easily execute our text-based commands. Our code obtained anInputStream, wrapped that in anInputStreamReader, which converted it to aReader, and then wrapped that in aBufferedReader. We used thePrintStream to execute ourGET method and then used theBufferedReader to read the response line-by-line until we received anull response, indicating that the socket had been closed.

Now execute this class and pass it the following arguments:

java com.geekcap.javaworld.simplesocketclient.SimpleSocketClientExample www.javaworld.com /

You should see output similar to what’s below:

Loading contents of URL: www.javaworld.comHTTP/1.1 200 OKDate: Sun, 21 Sep 2014 22:20:13 GMTServer: ApacheX-Gas_TTL: 10Cache-Control: max-age=10X-GasHost: gas2.uswX-Cooking-With: Gasoline-LocalX-Gasoline-Age: 8Content-Length: 168Last-Modified: Tue, 24 Jan 2012 00:09:09 GMTEtag: "60001b-a8-4b73af4bf3340"Content-Type: text/htmlVary: Accept-EncodingConnection: 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 is200 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 aServerSocket, specifying a port to listen on.
  2. Invoke theServerSocket‘saccept() method to listen on the configured port for a client connection.
  3. When a client connects to the server, theaccept() method returns aSocket through which the server can communicate with the client. This is the sameSocket class that we used for our client, so the process is the same: obtain anInputStream to read from the client and anOutputStream write to the client.
  4. If you server needs to be scalable, you will want to pass theSocket to another thread to process so that your server can continue listening for additional connections.
  5. Call theServerSocket‘saccept() 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 aServerSocket by passing it a port to listen on (more aboutServerSocketFactorys in the next section):

ServerSocket serverSocket = new ServerSocket( port );

And now we can accept incoming connections via theaccept() 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 anecho 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 newSimpleSocketServer instance and start the server. This is required because theSimpleSocketServer extendsThread to create a new thread to handle the blockingaccept() call that you see in theread() method. Therun() method sits in a loop accepting client requests and creatingRequestHandler threads to process the request. Again, this is relatively simple code, but also involves a fair amount of threaded programming.

Note too that theRequestHandler handles the client communication much like the code in Listing 1 did: it wraps theOutputStream with aPrintStream to facilitate easy writes and, similarly, wraps theInputStream with aBufferedReader 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 theRequestHandler 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:

When programming with NIO, you open a channel to your destination and then read data into abuffer 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’sflip() method to prepare it for reading
  3. Read data from the buffer
  4. Call the buffer’sclear() orcompact() 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:

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 thejava.nio.file package andjava.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’sAsynchronousServerSocketChannel.

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”

TheAsynchronousServerSocketChannel provides a non-blocking asynchronous channel for stream-oriented listening sockets. In order to use it, we first execute its staticopen() method and thenbind() it to a specific port. Next, we’ll execute itsaccept() method, passing to it a class that implements theCompletionHandler interface. Most often, you’ll find thathandler 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 Byen".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 newAsynchronousServerSocketChannel and then bind it to port 5000:

        final AsynchronousServerSocketChannel listener =              AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));

From thisAsynchronousServerSocketChannel, we invokeaccept() to tell it to start listening for connections, passing to it a customCompletionHandler instance. When we invokeaccept(), it returns immediately. Note that this example is different from theServerSocket class in Listing 1; whereas theaccept() method blocked until a client connected to it, theAsynchronousServerSocketChannelaccept() method handles it for us.

The completion handler

Our next responsibility is to create aCompletionHandler class and provide an implementation of thecompleted() andfailed() methods. Thecompleted() method is called when theAsynchronousServerSocketChannel receives a connection from a client and it includes anAsynchronousSocketChannel to the client. Thecompleted() method first accepts the connection from theAsynchronousServerSocketChannel 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 toByteBuffer.wrap() to construct aByteBuffer. TheByteBuffer can then be passedAsynchronousSocketChannel‘swrite() method.

To read from the client, we create a newByteBuffer by invoking itsallocate(4096) (which creates a 4K buffer), then we invoke theAsynchronousSocketChannel‘sread() method. Theread() returns aFuture<Integer> on which we can invokeget() to retrieve the number of bytes read from the client. In this example, we passget() a timeout value of 20 seconds: if we do not get a response in 20 seconds then theget() method will throw aTimeoutException. Our rule for this echo server is that if we observe 20 seconds of silence then we terminate the conversation.

Futures in asynchronous computation

“TheFuture<V> interface represents the result of an asynchronous computation. The result is known as afuture 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 theByteBuffer‘sflip() 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 theByteBuffer‘sget() to load data into that byte array. Finally, we convert the byte array to a string by creating a newString instance. We echo the line back to the client by converting the string to a byte array, passing that to theByteBuffer.wrap() method and invoking theAsynchronousSocketChannel‘swrite() method. Now weclear() theByteBuffer, which recall means that it repositions theposition to zero and puts theByteBuffer into write mode, and then we read the next line from the client.

The only thing to be aware of is that themain() method, which creates the server, also sets up a 60 second timer to keep the application running. Because theAsynchronousSocketChannel‘saccept() method returns immediately, if we don’t have theThread.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.

Steven Haines

Steven Haines is a senior technologist, accomplished architect, author, and educator. He currently is a principal software engineer at Veeva Systems, where he builds Spring-based Java applications. Steven previously worked on two startups: Chronosphere, where he helped customers design and implement large-scale observability strategies, and Turbonomic, where he was a principal software architect for cloud optimization products. He's also worked for Disney as a technical architect and lead solution architect, building out the next generation of Disney's guest experience and other Disney solutions. Steven specializes in performance and scalability, cloud-based architectures, high-availability, fault tolerance, business analytics, and integration with new and emerging technologies.

As an author, he has written two books on Java programming and more than 500 articles for publications such as InfoWorld, InformIT.com (Pearson Education), JavaWorld, and Dr. Dobb's Journal. He has also written over a dozen white papers and ebooks on performance management and cloud-based architectures.

Steven has taught computer science and Java programming at Learning Tree University and the University of California, Irvine. He also maintains a personal website dedicated to helping software developers and architects grow in their knowledge:www.geekcap.com (by Geeks for Geeks).

More from this author

Show me more

Sponsored Links


[8]ページ先頭

©2009-2025 Movatter.jp