Java Socket客户端编程

Published on 2017 - 05 - 02

Using Sockets

A socket is a connection between two hosts. It can perform seven basic operations:

  • Connect to a remote machine
  • Send data
  • Receive data
  • Close a connection
  • Bind to a port
  • Listen for incoming data
  • Accept connections from remote machines on the bound port

Java’s Socket class, which is used by both clients and servers, has methods that correspond to the first four of these operations. The last three operations are needed only by servers, which wait for clients to connect to them. They are implemented by the ServerSocket class. Java programs normally use client sockets in the following fashion:

  • The program creates a new socket with a constructor.
  • The socket attempts to connect to the remote host.

Once the connection is established, the local and remote hosts get input and output streams from the socket and use those streams to send data to each other. This connection is full-duplex. Both hosts can send and receive data simultaneously. What the data means depends on the protocol; different commands are sent to an FTP server than to an HTTP server. There will normally be some agreed-upon handshaking followed by the transmission of data from one to the other.

When the transmission of data is complete, one or both sides close the connection. Some protocols, such as HTTP 1.0, require the connection to be closed after each request is serviced. Others, such as FTP and HTTP 1.1, allow multiple requests to be processed in a single connection.

Reading from Servers with Sockets

Let’s begin with a simple example. You’re going to connect to the daytime server at the National Institute for Standards and Technology (NIST) and ask it for the current time. This protocol is defined in RFC 867. Reading that, you see that the daytime server listens on port 13, and that the server sends the time in a human-readable format and closes the connection. You can test the daytime server with Telnet like this:

$ telnet time.nist.gov 13
Trying 129.6.15.28...
Connected to time.nist.gov.
Escape character is '^]'.

56375 13-03-24 13:37:50 50 0 0 888.8 UTC(NIST) *
Connection closed by foreign host.

The line “56375 13-03-24 13:37:50 50 0 0 888.8 UTC(NIST)” is sent by the daytime server. When you read the Socket’s InputStream, this is what you will get. The other lines are produced either by the Unix shell or by the Telnet program.

RFC 867 does not specify any particular format for the output other than that it be human readable. In this case, you can see this connection was made on March 24, 2013, at 1:37: 50 P.M., Greenwich Meantime. More specifically, the format is defined as JJJJJ YY-MM-DD HH:MM:SS TT L H msADV UTC(NIST) OTM where:

  • JJJJJ is the “Modified Julian Date” (i.e., it is the number of whole days since midnight on November 17, 1858).
  • YY-MM-DD is the last two digits of the year, the month, and the current day of month.
  • HH:MM:SS is the time in hours, minutes, and seconds in Coordinated Universal Time (UTC, essentially Greenwich Mean Time).
  • TT indicates whether the United States is currently observing on Standard Time or Daylight Savings Time: 00 means standard time; 50 means daylight savings time. Other values count down the number of days until the switchover.
  • L is a one-digit code that indicates whether a leap second will be added or subtracted at midnight on the last day of the current month: 0 for no leap second, 1 to add a leap second, and 2 to subtract a leap second.
  • H represents the health of the server: 0 means healthy, 1 means up to 5 seconds off, 2 means more than 5 seconds off, 3 means an unknown amount of inaccuracy, and 4 is maintenance mode.
  • msADV is a number of milliseconds that NIST adds to the time it sends to roughly compensate for network delays. In the preceding code, you can see that it added 888.8 milliseconds to this result, because that’s how long it estimates it’s going to take for the response to return.
  • The string UTC(NIST) is a constant, and the OTM is almost a constant (an asterisk unless something really weird has happened).

These details are all NIST specific. They are not part of the daytime standard. Although they do offer a lot of data, if you have a real programmatic need to sync with a network time server, you’re better off using the NTP protocol defined in RFC 5905 instead.

Now let’s see how to retrieve this same data programmatically using sockets. First, open a socket to time.nist.gov on port 13:

Socket socket = new Socket("time.nist.gov", 13);

This doesn’t just create the object. It actually makes the connection across the network. If the connection times out or fails because the server isn’t listening on port 13, then the constructor throws an IOException, so you’ll usually wrap this in a try block. In Java 7, Socket implements Autocloseable so you can use try-with-resources:

try (Socket socket = new Socket("time.nist.gov", 13)) {
  // read from the socket...
} catch (IOException ex) {
  System.err.println("Could not connect to time.nist.gov");
}

In Java 6 and earlier, you’ll want to explicitly close the socket in a finally block to release resources the socket holds:

Socket socket = null;
try {
  socket = new Socket(hostname, 13);
  // read from the socket...
} catch (IOException ex) {
  System.err.println(ex);
} finally {
  if (socket != null) {
    try {
      socket.close();
    } catch (IOException ex) {
      // ignore
    }
  }
}

The next step is optional but highly recommended. Set a timeout on the connection using the setSoTimeout() method. Timeouts are measured in milliseconds, so this statement sets the socket to time out after 15 seconds of nonresponsiveness:

socket.setSoTimeout(15000);

Although a socket should throw a ConnectException pretty quickly if the server rejects the connection, or a NoRouteToHostException if the routers can’t figure out how to send your packets to the server, neither of these help you with the case where a misbehaving server accepts the connection and then stops talking to you without actively closing the connection. Setting a timeout on the socket means that each read from or write to the socket will take at most a certain number of milliseconds. If a server hangs while you’re connected to it, you will be notified with a SocketTimeoutException. Exactly how long a timeout to set depends on the needs of your application and how responsive you expect the server to be. Fifteen seconds is a long time for a local intranet server to respond, but it’s rather short for an overloaded public server like time.nist.gov.

Once you’ve opened the socket and set its timeout, call getInputStream() to return an InputStream you can use to read bytes from the socket. In general, a server can send any bytes at all; but in this specific case, the protocol specifies that those bytes must be ASCII:

InputStream in = socket.getInputStream();
StringBuilder time = new StringBuilder();
InputStreamReader reader = new InputStreamReader(in, "ASCII");
for (int c = reader.read(); c != -1; c = reader.read()) {
  time.append((char) c);
}
System.out.println(time);

Here I’ve stored the bytes in a StringBuilder. You can, of course, use any data structure that fits your problem to hold the data that comes off the network.

Example 1 puts this all together in a program that also allows you to choose a different daytime server.

import java.net.*;
import java.io.*;

public class DaytimeClient {

  public static void main(String[] args) {

    String hostname = args.length > 0 ? args[0] : "time.nist.gov";
    Socket socket = null;
    try {
      socket = new Socket(hostname, 13);
      socket.setSoTimeout(15000);
      InputStream in = socket.getInputStream();
      StringBuilder time = new StringBuilder();
      InputStreamReader reader = new InputStreamReader(in, "ASCII");
      for (int c = reader.read(); c != -1; c = reader.read()) {
        time.append((char) c);
      }
      System.out.println(time);
    } catch (IOException ex) {
      System.err.println(ex);
    } finally {
      if (socket != null) {
        try {
          socket.close();
        } catch (IOException ex) {
          // ignore
        }
      }
    }
  }
}

Typical output is much the same as if you connected with Telnet:

$ java DaytimeClient
56375 13-03-24 15:05:42 50 0 0 843.6 UTC(NIST) *

As far as network-specific code goes, that’s pretty much it. In most network programs like this, the real effort is in speaking the protocol and comprehending the data formats. For instance, rather than simply printing out the text the server sends you, you might want to parse it into a java.util.Date object instead. Example 2 shows you how to do this. For variety, I also wrote this example taking advantage of Java 7’s AutoCloseable and try-with-resources.

import java.net.*;
import java.text.*;
import java.util.Date;
import java.io.*;

public class Daytime {

  public Date getDateFromNetwork() throws IOException, ParseException {
    try (Socket socket = new Socket("time.nist.gov", 13)) {
      socket.setSoTimeout(15000);
      InputStream in = socket.getInputStream();
      StringBuilder time = new StringBuilder();
      InputStreamReader reader = new InputStreamReader(in, "ASCII");
      for (int c = reader.read(); c != -1; c = reader.read()) {
        time.append((char) c);
      }
      return parseDate(time.toString());
    }
  }

  static Date parseDate(String s) throws ParseException {
    String[] pieces = s.split(" ");
    String dateTime = pieces[1] + " " + pieces[2] + " UTC";
    DateFormat format = new SimpleDateFormat("yy-MM-dd hh:mm:ss z");
    return format.parse(dateTime);
  }
}

Notice, however, this class doesn’t actually do anything with the network that Example 1 didn’t do. It just added a bunch of code to turn strings into dates.

When reading data from the network, it’s important to keep in mind that not all protocols use ASCII or even text. For example, the time protocol specified in RFC 868 specifies that the time be sent as the number of seconds since midnight, January 1, 1900, Greenwich Mean Time. However, this is not sent as an ASCII string like 2,524,521,600 or –1297728000. Rather, it is sent as a 32-bit, unsigned, big-endian binary number.

Because the time protocol doesn’t send back text, you can’t easily use Telnet to test such a service, and your program can’t read the server response with a Reader or any sort of readLine() method. A Java program that connects to time servers must read the raw bytes and interpret them appropriately. In this example, that job is complicated by Java’s lack of a 32-bit unsigned integer type. Consequently, you have to read the bytes one at a time and manually convert them into a long using the bitwise operators << and |. Example 3 demonstrates. When speaking other protocols, you may encounter data formats even more alien to Java. For instance, a few network protocols use 64-bit fixed-point numbers. There’s no shortcut to handle all possible cases. You simply have to grit your teeth and code the math you need to handle the data in whatever format the server sends.

import java.net.*;
import java.text.*;
import java.util.Date;
import java.io.*;

public class Time {

  private static final String HOSTNAME = "time.nist.gov";

  public static void main(String[] args) throws IOException, ParseException {
    Date d = Time.getDateFromNetwork();
    System.out.println("It is " + d);
  }

  public static Date getDateFromNetwork() throws IOException, ParseException {
    // The time protocol sets the epoch at 1900,
    // the Java Date class at 1970. This number
    // converts between them.

    long differenceBetweenEpochs = 2208988800L;

    // If you'd rather not use the magic number, uncomment
    // the following section which calculates it directly.
    /*
    TimeZone gmt = TimeZone.getTimeZone("GMT");
    Calendar epoch1900 = Calendar.getInstance(gmt);
    epoch1900.set(1900, 01, 01, 00, 00, 00);
    long epoch1900ms = epoch1900.getTime().getTime();
    Calendar epoch1970 = Calendar.getInstance(gmt);
    epoch1970.set(1970, 01, 01, 00, 00, 00);
    long epoch1970ms = epoch1970.getTime().getTime();

    long differenceInMS = epoch1970ms - epoch1900ms;
    long differenceBetweenEpochs = differenceInMS/1000;
    */

    Socket socket = null;
    try {
      socket = new Socket(HOSTNAME, 37);
      socket.setSoTimeout(15000);

      InputStream raw = socket.getInputStream();

      long secondsSince1900 = 0;
      for (int i = 0; i < 4; i++) {
        secondsSince1900 = (secondsSince1900 << 8) | raw.read();
      }

      long secondsSince1970
                = secondsSince1900 - differenceBetweenEpochs;
      long msSince1970 = secondsSince1970 * 1000;
      Date time = new Date(msSince1970);

      return time;
    } finally {
      try {
        if (socket != null) socket.close();
      }
      catch (IOException ex) {}
    }
  }
}

Here’s the output of this program from a sample run:

$ java Time
It is Sun Mar 24 12:22:17 EDT 2013

The time protocol actually specifies Greenwich Mean Time, but the toString() method in Java’s Date class, implicitly invoked by System.out.println(), converts this into the time zone of the local host, Eastern Daylight Time in this case.

Writing to Servers with Sockets

Writing to a server is not noticeably harder than reading from one. You simply ask the socket for an output stream as well as an input stream. Although it’s possible to send data over the socket using the output stream at the same time you’re reading data over the input stream, most protocols are designed so that the client is either reading or writing over a socket, not both at the same time. In the most common pattern, the client sends a request. Then the server responds. The client may send another request, and the server responds again. This continues until one side or the other is done, and closes the connection.

One simple bidirectional TCP protocol is dict, defined in RFC 2229. In this protocol, the client opens a socket to port 2628 on the dict server and sends commands such as “DEFINE eng-lat gold”. This tells the server to send a definition of the word gold using its English-to-Latin dictionary. (Different servers have different dictionaries installed.) After the first definition is received, the client can ask for another. When it’s done it sends the command “quit”. You can explore dict with Telnet like this:

$ telnet dict.org 2628
Trying 216.18.20.172...
Connected to dict.org.
Escape character is '^]'.
220 pan.alephnull.com dictd 1.12.0/rf on Linux 3.0.0-14-server
    <auth.mime> <499772.29595.1364340382@pan.alephnull.com>
DEFINE eng-lat gold
150 1 definitions retrieved
151 "gold" eng-lat "English-Latin Freedict dictionary"
gold [gould]
   aurarius; aureus; chryseus
   aurum; chrysos

.
250 ok [d/m/c = 1/0/10; 0.000r 0.000u 0.000s]
DEFINE eng-lat computer
552 no match [d/m/c = 0/0/9; 0.000r 0.000u 0.000s]
quit
221 bye [d/m/c = 0/0/0; 42.000r 0.000u 0.000s]

You can see that control response lines begin with a three-digit code. The actual definition is plain text, terminated with a period on a line by itself. If the dictionary doesn’t contain the word you asked for, it returns 552 no match. Of course, you could also find this out, and a lot more, by reading the RFC.

It’s not hard to implement this protocol in Java. First, open a socket to a dict server—dict.org_ is a good one—on port 2628:

Socket socket = new Socket("dict.org", 2628);

Once again you’ll want to set a timeout in case the server hangs while you’re connected to it:

socket.setSoTimeout(15000);

In the dict protocol, the client speaks first, so ask for the output stream using getOutputStream():

OutputStream out = socket.getOutputStream();

The getOutputStream() method returns a raw OutputStream for writing data from your application to the other end of the socket. You usually chain this stream to a more convenient class like DataOutputStream or OutputStreamWriter before using it. For performance reasons, it’s a good idea to buffer it as well. Because the dict protocol is text based, more specifically UTF-8 based, it’s convenient to wrap this in a Writer:

Writer writer = new OutputStreamWriter(out, "UTF-8");

Now write the command over the socket:

writer.write("DEFINE eng-lat gold\r\n");

Finally, flush the output so you’ll be sure the command is sent over the network:

writer.flush();

The server should now respond with a definition. You can read that using the socket’s input stream:

InputStream in = socket.getInputStream();
BufferedReader reader = new BufferedReader(
  new InputStreamReader(in, "UTF-8"));
for (String line = reader.readLine();
  !line.equals(".");
  line = reader.readLine()) {
    System.out.println(line);
}

When you see a period on a line by itself, you know the definition is complete. You can then send the quit over the output stream:

writer.write("quit\r\n");
writer.flush();

Example 4 shows a complete dict client. It connects to dict.org, and translates any words the user enters on the command line into Latin. It filters out all the metadata lines that begin with response codes such as 150 or 220. However, it does specifically check for a line that begins “552 no match” in case the server doesn’t recognize the word.

import java.io.*;
import java.net.*;

public class DictClient {

  public static final String SERVER = "dict.org";
  public static final int PORT = 2628;
  public static final int TIMEOUT = 15000;

  public static void main(String[] args) {

    Socket socket = null;
    try {
      socket = new Socket(SERVER, PORT);
      socket.setSoTimeout(TIMEOUT);
      OutputStream out = socket.getOutputStream();
      Writer writer = new OutputStreamWriter(out, "UTF-8");
      writer = new BufferedWriter(writer);
      InputStream in = socket.getInputStream();
      BufferedReader reader = new BufferedReader(
          new InputStreamReader(in, "UTF-8"));

      for (String word : args) {
        define(word, writer, reader);
      }

      writer.write("quit\r\n");
      writer.flush();
    } catch (IOException ex) {
      System.err.println(ex);
    } finally { // dispose
      if (socket != null) {
        try {
          socket.close();
        } catch (IOException ex) {
          // ignore
        }
      }
    }
  }

  static void define(String word, Writer writer, BufferedReader reader)
      throws IOException, UnsupportedEncodingException {
    writer.write("DEFINE eng-lat " + word + "\r\n");
    writer.flush();

    for (String line = reader.readLine(); line != null; line = reader.readLine()) {
      if (line.startsWith("250 ")) { // OK
        return;
      } else if (line.startsWith("552 ")) { // no match
        System.out.println("No definition found for " + word);
        return;
      }
      else if (line.matches("\\d\\d\\d .*")) continue;
      else if (line.trim().equals(".")) continue;
      else System.out.println(line);
    }
  }
}

Here’s a sample run:

$ java DictClient gold uranium silver copper lead
gold [gould]
   aurarius; aureus; chryseus
   aurum; chrysos

No definition found for uranium
silver [silvər]
   argenteus
   argentum

copper [kɔpər]
   æneus; aheneus; ærarius; chalceus
   æs

lead [led]
   ducere
   molybdus; plumbum

Example 4 is line oriented. It reads a line of input from the console, sends it to the server, and waits to read a line of output it gets back.

Half-closed sockets

The close() method shuts down both input and output from the socket. On occasion, you may want to shut down only half of the connection, either input or output. The shutdownInput() and shutdownOutput() methods close only half the connection:

public void shutdownInput() throws IOException
public void shutdownOutput() throws IOException

Neither actually closes the socket. Instead, they adjust the stream connected to the socket so that it thinks it’s at the end of the stream. Further reads from the input stream after shutting down input return –1. Further writes to the socket after shutting down output throw an IOException.

Many protocols, such as finger, whois, and HTTP, begin with the client sending a request to the server, then reading the response. It would be possible to shut down the output after the client has sent the request. For example, this code fragment sends a request to an HTTP server and then shuts down the output, because it won’t need to write anything else over this socket:

try (Socket connection = new Socket("www.oreilly.com", 80)) {
  Writer out = new OutputStreamWriter(
           connection.getOutputStream(), "8859_1");
  out.write("GET / HTTP 1.0\r\n\r\n");
  out.flush();
  connection.shutdownOutput();
  // read the response...
} catch (IOException ex) {
  ex.printStackTrace();
}

Notice that even though you shut down half or even both halves of a connection, you still need to close the socket when you’re through with it. The shutdown methods simply affect the socket’s streams. They don’t release the resources associated with the socket, such as the port it occupies.

The isInputShutdown() and isOutputShutdown() methods tell you whether the input and output streams are open or closed, respectively. You can use these (rather than isConnected() and isClosed()) to more specifically ascertain whether you can read from or write to a socket:

public boolean isInputShutdown()
public boolean isOutputShutdown()

Constructing and Connecting Sockets

The java.net.Socket class is Java’s fundamental class for performing client-side TCP operations. Other client-oriented classes that make TCP network connections such as URL, URLConnection, Applet, and JEditorPane all ultimately end up invoking the methods of this class. This class itself uses native code to communicate with the local TCP stack of the host operating system.

Basic Constructors

Each Socket constructor specifies the host and the port to connect to. Hosts may be specified as an InetAddress or a String. Remote ports are specified as int values from 1 to 65535:

public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress host, int port) throws IOException

These constructors connect the socket (i.e., before the constructor returns, an active network connection is established to the remote host). If the connection can’t be opened for some reason, the constructor throws an IOException or an UnknownHostException. For example:

try {
  Socket toOReilly = new Socket("www.oreilly.com", 80);
  // send and receive data...
} catch (UnknownHostException ex) {
  System.err.println(ex);
} catch (IOException ex) {
  System.err.println(ex);
}

In this constructor, the host argument is just a hostname expressed as a String. If the domain name server cannot resolve the hostname or is not functioning, the constructor throws an UnknownHostException. If the socket cannot be opened for some other reason, the constructor throws an IOException. There are many reasons a connection attempt might fail: the host you’re trying to reach may not accept connections on that port, the hotel WiFi service may be blocking you until you log in to its website and pay $14.95, or routing problems may be preventing your packets from reaching their destination.

Because this constructor doesn’t just create a Socket object but also tries to connect the socket to the remote host, you can use the object to determine whether connections to a particular port are allowed, as in Example 5.

import java.net.*;
import java.io.*;

public class LowPortScanner {

  public static void main(String[] args) {

    String host = args.length > 0 ? args[0] : "localhost";

    for (int i = 1; i < 1024; i++) {
      try {
        Socket s = new Socket(host, i);
        System.out.println("There is a server on port " + i + " of "
         + host);
        s.close();
      } catch (UnknownHostException ex) {
        System.err.println(ex);
        break;
      } catch (IOException ex) {
        // must not be a server on this port
      }
    }
  }
}

Here’s the output this program produces on my local host (your results will vary, depending on which ports are occupied):

$ java LowPortScanner
There is a server on port 21 of localhost
There is a server on port 22 of localhost
There is a server on port 23 of localhost
There is a server on port 25 of localhost
There is a server on port 37 of localhost
There is a server on port 111 of localhost
There is a server on port 139 of localhost
There is a server on port 210 of localhost
There is a server on port 515 of localhost
There is a server on port 873 of localhost

If you’re curious about what servers are running on these ports, try experimenting with Telnet. On a Unix system, you may be able to find out which services reside on which ports by looking in the file /etc/services. If LowPortScanner finds any ports that are running servers but are not listed in /etc/services, then that’s interesting.

Although this program looks simple, it’s not without its uses. The first step to securing a system is understanding it. This program helps you understand what your system is doing so you can find (and close) possible entrance points for attackers. You may also find rogue servers: for example, LowPortScanner might tell you that there’s a server on port 800, which, on further investigation, turns out to be an HTTP server somebody is running to serve MP3 files, and which is saturating your T1.

Three constructors create unconnected sockets. These provide more control over exactly how the underlying socket behaves, for instance by choosing a different proxy server or an encryption scheme:

public Socket()
public Socket(Proxy proxy)
protected Socket(SocketImpl impl)

Picking a Local Interface to Connect From

Two constructors specify both the host and port to connect to and the interface and port to connect from:

public Socket(String host, int port, InetAddress interface, int localPort)
      throws IOException, UnknownHostException
public Socket(InetAddress host, int port, InetAddress interface, int localPort)
      throws IOException

This socket connects to the host and port specified in the first two arguments. It connects from the local network interface and port specified by the last two arguments. The network interface may be either physical (e.g., an Ethernet card) or virtual (a multihomed host with more than one IP address). If 0 is passed for the localPort argument, Java chooses a random available port between 1024 and 65535.

Selecting a particular network interface from which to send data is uncommon, but a need does come up occasionally. One situation where you might want to explicitly choose the local address would be on a router/firewall that uses dual Ethernet ports. Incoming connections would be accepted on one interface, processed, and forwarded to the local network from the other interface. Suppose you were writing a program to periodically dump error logs to a printer or send them over an internal mail server. You’d want to make sure you used the inward-facing network interface instead of the outward-facing network interface. For example:

try {
  InetAddress inward = InetAddress.getByName("router");
  Socket socket = new Socket("mail", 25, inward, 0);
  // work with the sockets...
} catch (IOException ex) {
  System.err.println(ex);
}

By passing 0 for the local port number, I say that I don’t care which port is used but I do want to use the network interface bound to the local hostname router.

This constructor can throw an IOException or an UnknownHostException for the same reasons as the previous constructors. In addition, it throws an IOException (probably a BindException, although again that’s just a subclass of IOException and not specifically declared in the throws clause of this method) if the socket is unable to bind to the requested local network interface. For instance, a program running on a.example.com can’t connect from b.example.org. You could take deliberate advantage of this to restrict a compiled program to run on only a predetermined host. It would require customizing distributions for each computer and is certainly overkill for cheap products. Furthermore, Java programs are so easy to disassemble, decompile, and reverse engineer that this scheme is far from foolproof. Nonetheless, it might be part of a scheme to enforce a software license.

Constructing Without Connecting

All the constructors we’ve talked about so far both create the socket object and open a network connection to a remote host. Sometimes you want to split those operations. If you give no arguments to the Socket constructor, it has nowhere to connect to:

public Socket()

You can connect later by passing a SocketAddress to one of the connect() methods. For example:

try {
  Socket socket = new Socket();
  // fill in socket options
  SocketAddress address = new InetSocketAddress("time.nist.gov", 13);
  socket.connect(address);
  // work with the sockets...
} catch (IOException ex) {
  System.err.println(ex);
}

You can pass an int as the second argument to specify the number of milliseconds to wait before the connection times out:

public void connect(SocketAddress endpoint, int timeout) throws IOException

The default, 0, means wait forever.

However, the prime benefit I find is that it enables me to clean up the code in try-catch-finally blocks, especially prior to Java 7. The noargs constructor throws no exceptions so it enables you to avoid the annoying null check when closing a socket in a finally block. With the original constructor, most code looks like this:

Socket socket = null;
try {
  socket = new Socket(SERVER, PORT);
  // work with the socket...
} catch (IOException ex) {
  System.err.println(ex);
} finally {
  if (socket != null) {
    try {
      socket.close();
    } catch (IOException ex) {
      // ignore
    }
  }
}

With the noargs constructor, it looks like this:

Socket socket = new Socket();
SocketAddress address = new InetSocketAddress(SERVER, PORT);
try {
  socket.connect(address);
  // work with the socket...
} catch (IOException ex) {
  System.err.println(ex);
} finally {
  try {
    socket.close();
  } catch (IOException ex) {
    // ignore
  }
}

That’s not quite as nice as the autoclosing version in Java 7, but it is an improvement.

Socket Addresses

The SocketAddress class represents a connection endpoint. It is an empty abstract class with no methods aside from a default constructor. At least theoretically, the SocketAddress class can be used for both TCP and non-TCP sockets. In practice, only TCP/IP sockets are currently supported and the socket addresses you actually use are all instances of InetSocketAddress.

The primary purpose of the SocketAddress class is to provide a convenient store for transient socket connection information such as the IP address and port that can be reused to create new sockets, even after the original socket is disconnected and garbage collected. To this end, the Socket class offers two methods that return SocketAddress objects (getRemoteSocketAddress() returns the address of the system being connected to and getLocalSocketAddress() returns the address from which the connection is made):

public SocketAddress getRemoteSocketAddress()
public SocketAddress getLocalSocketAddress()

Both of these methods return null if the socket is not yet connected. For example, first you might connect to Yahoo! then store its address:

Socket socket = new Socket("www.yahoo.com", 80);
SocketAddress yahoo = socket.getRemoteSocketAddress();
socket.close();

Later, you could reconnect to Yahoo! using this address:

Socket socket2 = new Socket();
socket2.connect(yahoo);

The InetSocketAddress class (which is the only subclass of SocketAddress in the JDK, and the only subclass I’ve ever encountered) is usually created with a host and a port (for clients) or just a port (for servers):

public InetSocketAddress(InetAddress address, int port)
public InetSocketAddress(String host, int port)
public InetSocketAddress(int port)

You can also use the static factory method InetSocketAddress.createUnresolved() to skip looking up the host in DNS:

public static InetSocketAddress createUnresolved(String host, int port)

InetSocketAddress has a few getter methods you can use to inspect the object:

public final InetAddress getAddress()
public final int         getPort()
public final String      getHostName()

Proxy Servers

The last constructor creates an unconnected socket that connects through a specified proxy server:

public Socket(Proxy proxy)

Normally, the proxy server a socket uses is controlled by the socksProxyHost and socksProxyPort system properties, and these properties apply to all sockets in the system. However, a socket created by this constructor will use the specified proxy server instead. Most notably, you can pass Proxy.NO_PROXY for the argument to bypass all proxy servers completely and connect directly to the remote host. Of course, if a firewall prevents direct connections, there’s nothing Java can do about it; and the connection will fail.

To use a particular proxy server, specify it by address. For example, this code fragment uses the SOCKS proxy server at myproxy.example.com to connect to the host login.ibiblio.org:

SocketAddress proxyAddress = new InetSocketAddress("myproxy.example.com", 1080);
Proxy proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress)
Socket s = new Socket(proxy);
SocketAddress remote = new InetSocketAddress("login.ibiblio.org", 25);
s.connect(remote);

SOCKS is the only low-level proxy type Java understands. There’s also a high-level Proxy.Type.HTTP that works in the application layer rather than the transport layer and a Proxy.Type.DIRECT that represents proxyless connections.

Getting Information About a Socket

Socket objects have several properties that are accessible through getter methods:

  • Remote address
  • Remote port
  • Local address
  • Local port

Here are the getter methods for accessing these properties:

public InetAddress getInetAddress()
public int getPort()
public InetAddress getLocalAddress()
public int getLocalPort()

There are no setter methods. These properties are set as soon as the socket connects, and are fixed from there on.

The getInetAddress() and getPort() methods tell you the remote host and port the Socket is connected to; or, if the connection is now closed, which host and port the Socket was connected to when it was connected. The getLocalAddress() and getLocalPort() methods tell you the network interface and port the Socket is connected from.

Unlike the remote port, which (for a client socket) is usually a “well-known port” that has been preassigned by a standards committee, the local port is usually chosen by the system at runtime from the available unused ports. This way, many different clients on a system can access the same service at the same time. The local port is embedded in outbound IP packets along with the local host’s IP address, so the server can send data back to the right port on the client.

Example 6 reads a list of hostnames from the command line, attempts to open a socket to each one, and then uses these four methods to print the remote host, the remote port, the local address, and the local port.

import java.net.*;
import java.io.*;

public class SocketInfo {

  public static void main(String[] args) {

    for (String host : args) {
      try {
        Socket theSocket = new Socket(host, 80);
        System.out.println("Connected to " + theSocket.getInetAddress()
            + " on port "  + theSocket.getPort() + " from port "
            + theSocket.getLocalPort() + " of "
            + theSocket.getLocalAddress());
      }  catch (UnknownHostException ex) {
        System.err.println("I can't find " + host);
      } catch (SocketException ex) {
        System.err.println("Could not connect to " + host);
      } catch (IOException ex) {
        System.err.println(ex);
      }
    }
  }
}

Here’s the result of a sample run. I included www.oreilly.com on the command line twice in order to demonstrate that each connection was assigned a different local port, regardless of the remote host; the local port assigned to any connection is unpredictable and depends mostly on what other ports are in use. The connection to login.ibiblio.org failed because that machine does not run any servers on port 80:

$ java SocketInfo www.oreilly.com www.oreilly.com www.elharo.com
  login.ibiblio.org
Connected to www.oreilly.com/208.201.239.37 on port 80 from port 49156 of
/192.168.254.25
Connected to www.oreilly.com/208.201.239.37 on port 80 from port 49157 of
/192.168.254.25
Connected to www.elharo.com/216.254.106.198 on port 80 from port 49158 of
/192.168.254.25
Could not connect to login.ibiblio.org

Closed or Connected?

The isClosed() method returns true if the socket is closed, false if it isn’t. If you’re uncertain about a socket’s state, you can check it with this method rather than risking an IOException. For example:

if (socket.isClosed()) {
  // do something...
} else {
  // do something else...
}

However, this is not a perfect test. If the socket has never been connected in the first place, isClosed() returns false, even though the socket isn’t exactly open.

The Socket class also has an isConnected() method. The name is a little misleading. It does not tell you if the socket is currently connected to a remote host (like if it is unclosed). Instead, it tells you whether the socket has ever been connected to a remote host. If the socket was able to connect to the remote host at all, this method returns true, even after that socket has been closed. To tell if a socket is currently open, you need to check that isConnected() returns true and isClosed() returns false. For example:

boolean connected = socket.isConnected() && ! socket.isClosed();

Finally, the isBound() method tells you whether the socket successfully bound to the outgoing port on the local system. Whereas isConnected() refers to the remote end of the socket, isBound() refers to the local end. This isn’t very important yet.

toString()

The Socket class overrides only one of the standard methods from java.lang.Object: toString(). The toString() method produces a string that looks like this:

Socket[addr=www.oreilly.com/198.112.208.11,port=80,localport=50055]

This is useful primarily for debugging. Don’t rely on this format; it may change in the future. All parts of this string are accessible directly through other methods (specifically getInetAddress(), getPort(), and getLocalPort()).

Setting Socket Options

Socket options specify how the native sockets on which the Java Socket class relies send and receive data. Java supports nine options for client-side sockets:

  • TCP_NODELAY
  • SO_BINDADDR
  • SO_TIMEOUT
  • SO_LINGER
  • SO_SNDBUF
  • SO_RCVBUF
  • SO_KEEPALIVE
  • OOBINLINE
  • IP_TOS

The funny-looking names for these options are taken from the named constants in the C header files used in Berkeley Unix where sockets were invented. Thus, they follow classic Unix C naming conventions rather than the more legible Java naming conventions. For instance, SO_SNDBUF really means “Socket Option Send Buffer Size.”

TCP_NODELAY

public void setTcpNoDelay(boolean on) throws SocketException
public boolean getTcpNoDelay() throws SocketException

Setting TCP_NODELAY to true ensures that packets are sent as quickly as possible regardless of their size. Normally, small (one-byte) packets are combined into larger packets before being sent. Before sending another packet, the local host waits to receive acknowledgment of the previous packet from the remote system. This is known as Nagle’s algorithm. The problem with Nagle’s algorithm is that if the remote system doesn’t send acknowledgments back to the local system fast enough, applications that depend on the steady transfer of small parcels of information may slow down. This issue is especially problematic for GUI programs such as games or network computer applications where the server needs to track client-side mouse movement in real time. On a really slow network, even simple typing can be too slow because of the constant buffering. Setting TCP_NODELAY to true defeats this buffering scheme, so that all packets are sent as soon as they’re ready.

setTcpNoDelay(true) turns off buffering for the socket. setTcpNoDelay(false) turns it back on. getTcpNoDelay() returns true if buffering is off and false if buffering is on. For example, the following fragment turns off buffering (that is, it turns on TCP_NODELAY) for the socket s if it isn’t already off:

if (!s.getTcpNoDelay()) s.setTcpNoDelay(true);

These two methods are each declared to throw a SocketException, which will happen if the underlying socket implementation doesn’t support the TCP_ NODELAY option.

SO_LINGER

public void setSoLinger(boolean on, int seconds) throws SocketException
public int getSoLinger() throws SocketException

The SO_LINGER option specifies what to do with datagrams that have not yet been sent when a socket is closed. By default, the close() method returns immediately; but the system still tries to send any remaining data. If the linger time is set to zero, any unsent packets are thrown away when the socket is closed. If SO_LINGER is turned on and the linger time is any positive value, the close() method blocks while waiting the specified number of seconds for the data to be sent and the acknowledgments to be received. When that number of seconds has passed, the socket is closed and any remaining data is not sent, acknowledgment or no.

These two methods each throw a SocketException if the underlying socket implementation does not support the SO_LINGER option. The setSoLinger() method can also throw an IllegalArgumentException if you try to set the linger time to a negative value. However, the getSoLinger() method may return –1 to indicate that this option is disabled, and as much time as is needed is taken to deliver the remaining data; for example, to set the linger timeout for the Socket s to four minutes, if it’s not already set to some other value:

if (s.getTcpSoLinger() == -1) s.setSoLinger(true, 240);

The maximum linger time is 65,535 seconds, and may be smaller on some platforms. Times larger than that will be reduced to the maximum linger time. Frankly, 65,535 seconds (more than 18 hours) is much longer than you actually want to wait. Generally, the platform default value is more appropriate.

SO_TIMEOUT

public void setSoTimeout(int milliseconds) throws SocketException
public int getSoTimeout() throws SocketException

Normally when you try to read data from a socket, the read() call blocks as long as necessary to get enough bytes. By setting SO_TIMEOUT, you ensure that the call will not block for more than a fixed number of milliseconds. When the timeout expires, an InterruptedIOException is thrown, and you should be prepared to catch it. However, the socket is still connected. Although this read() call failed, you can try to read from the socket again. The next call may succeed.

Timeouts are given in milliseconds. Zero is interpreted as an infinite timeout; it is the default value. For example, to set the timeout value of the Socket object s to 3 minutes if it isn’t already set, specify 180,000 milliseconds:

if (s.getSoTimeout() == 0) s.setSoTimeout(180000);

These two methods each throw a SocketException if the underlying socket implementation does not support the SO_TIMEOUT option. The setSoTimeout() method also throws an IllegalArgumentException if the specified timeout value is negative.

SO_RCVBUF and SO_SNDBUF

TCP uses buffers to improve network performance. Larger buffers tend to improve performance for reasonably fast (say, 10Mbps and up) connections whereas slower, dial-up connections do better with smaller buffers. Generally, transfers of large, continuous blocks of data, which are common in file transfer protocols such as FTP and HTTP, benefit from large buffers, whereas the smaller transfers of interactive sessions, such as Telnet and many games, do not. Relatively old operating systems designed in the age of small files and slow networks, such as BSD 4.2, use two-kilobyte buffers. Windows XP used 17,520 byte buffers. These days, 128 kilobytes is a common default.

Maximum achievable bandwidth equals buffer size divided by latency. For example, on Windows XP suppose the latency between two hosts is half a second (500 ms). Then the bandwidth is 17520 bytes / 0.5 seconds = 35040 bytes / second = 273.75 kilobits / second. That’s the maximum speed of any socket, regardless of how fast the network is. That’s plenty fast for a dial-up connection, and not bad for ISDN, but not really adequate for a DSL line or FIOS.

You can increase speed by decreasing latency. However, latency is a function of the network hardware and other factors outside the control of your application. On the other hand, you do control the buffer size. For example, if you increase the buffer size from 17,520 bytes to 128 kilobytes, the maximum bandwidth increases to 2 megabits per second. Double the buffer size again to 256 kilobytes, and the maximum bandwidth doubles to 4 megabits per second. Of course, the network itself has limits on maximum bandwidth. Set the buffer too high and your program will try to send and receive data faster than the network can handle, leading to congestion, dropped packets, and slower performance. Thus, when you want maximum bandwidth, you need to match the buffer size to the latency of the connection so it’s a little less than the bandwidth of the network.

You can use ping to check the latency to a particular host manually, or you can time a call to InetAddress.isReachable() from inside your program.

The SO_RCVBUF option controls the suggested send buffer size used for network input. The SO_SNDBUF option controls the suggested send buffer size used for network output:

public void setReceiveBufferSize(int size)
    throws SocketException, IllegalArgumentException
public int getReceiveBufferSize() throws SocketException
public void setSendBufferSize(int size)
    throws SocketException, IllegalArgumentException
public int getSendBufferSize() throws SocketException

Although it looks like you should be able to set the send and receive buffers independently, the buffer is usually set to the smaller of these two. For instance, if you set the send buffer to 64K and the receive buffer to 128K, you’ll have 64K as both the send and receive buffer size. Java will report that the receive buffer is 128K, but the underlying TCP stack will really be using 64K.

The setReceiveBufferSize()/setSendBufferSize methods suggest a number of bytes to use for buffering output on this socket. However, the underlying implementation is free to ignore or adjust this suggestion. In particular, Unix and Linux systems often specify a maximum buffer size, typically 64K or 256K, and do not allow any socket to have a larger one. If you attempt to set a larger value, Java will just pin it to the maximum possible buffer size. On Linux, it’s not unheard of for the underlying implementation to double the requested size. For example, if you ask for a 64K buffer, you may get a 128K buffer instead.

These methods throw an IllegalArgumentException if the argument is less than or equal to zero. Although they’re also declared to throw SocketException, they probably won’t in practice, because a SocketException is thrown for the same reason as IllegalArgumentException and the check for the IllegalArgumentException is made first.

In general, if you find your application is not able to fully utilize the available bandwidth (e.g., you have a 25 Mbps Internet connection, but your data is transferring at a piddling 1.5 Mbps) try increasing the buffer sizes. By contrast, if you’re dropping packets and experiencing congestion, try decreasing the buffer size. However, most of the time, unless you’re really taxing the network in one direction or the other, the defaults are fine. In particular, modern operating systems use TCP window scaling (not controllable from Java) to dynamically adjust buffer sizes to fit the network. As with almost any performance tuning advice, the rule of thumb is not to do it until you’ve measured a problem. And even then you may well get more speed by increasing the maximum allowed buffer size at the operating system level than by adjusting the buffer sizes of individual sockets.

SO_KEEPALIVE

If SO_KEEPALIVE is turned on, the client occasionally sends a data packet over an idle connection (most commonly once every two hours), just to make sure the server hasn’t crashed. If the server fails to respond to this packet, the client keeps trying for a little more than 11 minutes until it receives a response. If it doesn’t receive a response within 12 minutes, the client closes the socket. Without SO_KEEPALIVE, an inactive client could live more or less forever without noticing that the server had crashed. These methods turn SO_KEEPALIVE on and off and determine its current state:

public void setKeepAlive(boolean on) throws SocketException
public boolean getKeepAlive() throws SocketException

The default for SO_KEEPALIVE is false. This code fragment turns SO_KEEPALIVE off, if it’s turned on:

if (s.getKeepAlive()) s.setKeepAlive(false);

OOBINLINE

TCP includes a feature that sends a single byte of “urgent” data out of band. This data is sent immediately. Furthermore, the receiver is notified when the urgent data is received and may elect to process the urgent data before it processes any other data that has already been received. Java supports both sending and receiving such urgent data. The sending method is named, obviously enough, sendUrgentData():

public void sendUrgentData(int data) throws IOException

This method sends the lowest-order byte of its argument almost immediately. If necessary, any currently cached data is flushed first.

How the receiving end responds to urgent data is a little confused, and varies from one platform and API to the next. Some systems receive the urgent data separately from the regular data. However, the more common, more modern approach is to place the urgent data in the regular received data queue in its proper order, tell the application that urgent data is available, and let it hunt through the queue to find it.

By default, Java ignores urgent data received from a socket. However, if you want to receive urgent data inline with regular data, you need to set the OOBINLINE option to true using these methods:

public void setOOBInline(boolean on) throws SocketException
public boolean getOOBInline() throws SocketException

The default for OOBINLINE is false. This code fragment turns OOBINLINE on, if it’s turned off:

if (!s.getOOBInline()) s.setOOBInline(true);

Once OOBINLINE is turned on, any urgent data that arrives will be placed on the socket’s input stream to be read in the usual way. Java does not distinguish it from nonurgent data. That makes it less than ideally useful, but if you have a particular byte (e.g., a Ctrl-C) that has special meaning to your program and never shows up in the regular data stream, then this would enable you to send it more quickly.

SO_REUSEADDR

When a socket is closed, it may not immediately release the local port, especially if a connection was open when the socket was closed. It can sometimes wait for a small amount of time to make sure it receives any lingering packets that were addressed to the port that were still crossing the network when the socket was closed. The system won’t do anything with any of the late packets it receives. It just wants to make sure they don’t accidentally get fed into a new process that has bound to the same port.

This isn’t a big problem on a random port, but it can be an issue if the socket has bound to a well-known port because it prevents any other socket from using that port in the meantime. If the SO_REUSEADDR is turned on (it’s turned off by default), another socket is allowed to bind to the port even while data may be outstanding for the previous socket.

In Java this option is controlled by these two methods:

public void setReuseAddress(boolean on) throws SocketException
public boolean getReuseAddress() throws SocketException

For this to work, setReuseAddress() must be called before the new socket binds to the port. This means the socket must be created in an unconnected state using the noargs constructor; then setReuseAddress(true) is called, and the socket is connected using the connect() method. Both the socket that was previously connected and the new socket reusing the old address must set SO_REUSEADDR to true for it to take effect.

IP_TOS Class of Service

Different types of Internet service have different performance needs. For instance, video chat needs relatively high bandwidth and low latency for good performance, whereas email can be passed over low-bandwidth connections and even held up for several hours without major harm. VOIP needs less bandwidth than video but minimum jitter. It might be wise to price the different classes of service differentially so that people won’t ask for the highest class of service automatically. After all, if sending an overnight letter cost the same as sending a package via media mail, we’d all just use FedEx overnight, which would quickly become congested and overwhelmed. The Internet is no different.

The class of service is stored in an eight-bit field called IP_TOS in the IP header. Java lets you inspect and set the value a socket places in this field using these two methods:

public int getTrafficClass() throws SocketException
public void setTrafficClass(int trafficClass) throws SocketException

The traffic class is given as an int between 0 and 255. Because this value is copied to an eight-bit field in the TCP header, only the low order byte of this int is used; and values outside this range cause IllegalArgumentExceptions.

In 21st-century TCP stacks, the high-order six bits of this byte contain a Differentiated Services Code Point (DSCP) value and the low-order two bits contain an Explicit Congestion Notification (ECN) value. The DSCP thus has room for up to 26 different traffic classes. However, it’s up to individual networks and routers to specify exactly what the 64 different possible DSCP values mean. The four values shown in Table 1 are fairly common.

PHB (Per Hop Behavior) Binary value Purpose
Default 00000 Best-effort traffic.
Expedited Forwarding (EF) 101110 Low-loss, low-delay, low-jitter traffic. Often limited to 30% or less of network capacity.
Assured Forwarding (AF) multiple Assured delivery up to a specified rate.
Class Selector xxx000 Backward compatibility with the IPv4 TOS header, as stored in the first three bits.

For example, the Expedited Forwarding PHB is a good choice for for VOIP. EF traffic is often given strict priority queuing above all other traffic classes. This code fragment sets a socket to use Expedited Forwarding by setting the traffic class to 10111000:

Socket s = new Socket("www.yahoo.com", 80);
s.setTrafficClass(0xB8); // 10111000 in binary

Remember the low-order two bits of this number are Explicit Congestion Notification, and should be set to zero.

Assured Forwarding is actually 12 different DSCP values divided into four classes as shown in Table 2. The purpose here is to allow a sender to express relative preferences for which packets to drop when the network is congested. Within a class, packets with lower priority are dropped before packets with a higher priority. Between classes, packest from a higher-priority class are given preference, though lower-priority classes are not starved completely.

Class 1 (lowest priority) Class 2 Class 3 Class 4 (highest priority)
Low Drop Rate AF11 (001010) AF21 (010010) AF31 (011010) AF41 (100010)
Medium Drop Rate AF12 (001100) AF22 (010100) AF32 (011100) AF42 (100100)
High Drop Rate AF13 (001110) AF23 (010110) AF33 (011110) AF43 (100110)

For example, the following code fragment sets up three sockets with different forwarding characteristics. If the network gets congested enough, socket 3, in class 4 with a high drop rate, will send most of its data. Socket 1, in class 1 with a low drop rate, will also get to send data, though not as quickly as socket 1; and socket 3, with a high drop rate also in class 1 will be blocked completely until the congestion eases up enough that socket 2 is no longer dropping packets:

Socket s1 = new Socket("www.example.com", 80);
s1.setTrafficClass(0x26); // 00100110 in binary
Socket s2 = new Socket("www.example.com", 80);
s2.setTrafficClass(0x0A); // 00001010 in binary
Socket s3 = new Socket("www.example.com", 80);
s3.setTrafficClass(0x0E); // 00001110 in binary

DSCP values are not hard and fast guarantees of service. In practice, although DSCP values are respected on some networks internally, any time a packet crosses ISPs, this information is almost always ignored.

The underlying socket implementation is not required to respect any of these requests. They only provide a hint to the TCP stack about the desired policy. Many implementations ignore these values completely. Android in particular treats the setTrafficClass() method as a no-op. If the TCP stack is unable to provide the requested class of service, it may, but is not required to, throw a SocketException.

As an alternative way to express preferences, the setPerformancePreferences() method assigns relative preferences to connection time, latency, and bandwidth:

public void setPerformancePreferences(int connectionTime,
    int latency, int bandwidth)

For instance, if connectionTime is 2, latency is 1, and bandwidth is 3, then maximum bandwidth is the most important characteristic, minimum latency is the least important, and connection time is in the middle. If connectionTime is 2, latency is 2, and bandwidth is 3, then maximum bandwidth is the most important characteristic, while minimum latency and connection time are equally important. Exactly how any given VM implements this is implementation dependent. Indeed, it may be a no-op in some implementations.

Socket Exceptions

Most methods of the Socket class are declared to throw IOException or its subclass, java.net.SocketException:

public class SocketException extends IOException

However, knowing that a problem occurred is often not sufficient to deal with the problem. Did the remote host refuse the connection because it was busy? Did the remote host refuse the connection because no service was listening on the port? Did the connection attempt timeout because of network congestion or because the host was down? There are several subclasses of SocketException that provide more information about what went wrong and why:

public class BindException extends SocketException
public class ConnectException extends SocketException
public class NoRouteToHostException extends SocketException

A BindException is thrown if you try to construct a Socket or ServerSocket object on a local port that is in use or that you do not have sufficient privileges to use. A ConnectException is thrown when a connection is refused at the remote host, which usually happens because the host is busy or no process is listening on that port. Finally, a NoRouteToHostException indicates that the connection has timed out.

The java.net package also includes ProtocolException, which is a direct subclass of IOException:

public class ProtocolException extends IOException

This is thrown when data is received from the network that somehow violates the TCP/IP specification.

None of these exception classes have any special methods you wouldn’t find in any other exception class, but you can take advantage of these subclasses to provide more informative error messages or to decide whether retrying the offending operation is likely to be successful.

Reference