Network Programming :: Lessons :: Sockets
Using Sockets
As noted in an earlier lesson, data is sent across the Internet in packets called datagrams. Each datagram contains a header and a payload. It's often necessary to split the data across multiple packets and assemble it at the destination. Sockets allow a programmer to treat a network connection as just another stream onto which bytes can be written or from which bytes can be read. This shields the programmer from the low-level details of the network.
A socket can perform the following 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 has methods that correspond to the first four operations above. The last three are implemented by the ServerSocket class since they are only needed for servers. Java programs normally use client sockets in the following way:
- The program creates a new socket using 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 is known as a full-duplex connection since both hosts can send and receive data simultaneously.
Reading from Servers
To retrieve data using sockets you first need to open a socket on the desired port:
try (Socket socket = new Socket(hostName, portNumber)) { // Read from the socket } catch (IOException ex) { System.err.println("Could not connect to " + hostName); }
You should also set the socket to timeout so an accepted connection that isn't properly closed will timeout after a set amount of time. The example below will timeout after 20 seconds of inactivity.
socket.setSoTimeout(20000);
After creating a socket and setting its timeout, you can call getInputStream() to create an InputStream you can use to read bytes from the socket.
InputStream input = socket.getInputStream(); StringBuilder stream = new StringBuilder(); InputStreamReader reader = new InputStreamReader(input, "ASCII"); for (int c = reader.read(); c != -1; c = reader.read()) stream.append((char) c); System.out.println((char) c);
Writing to Servers
You can create an OutputStream to write to a server similarly to how you created an InputStream:
try (Socket socket = new Socket(hostName, portNumber)) { OutputStream output = socket.getOutputStream(); Writer writer = new OutputSreamWriter(output, "UTF-8"); // Write to socket } catch (IOException ex) { System.err.println("Could not connect to " + hostName); }
You should also set the timeout for the socket and you can use the write() method to write to the server. You should also use the flush() method to ensure the data is sent over the network.
Constructing and Connecting Sockets
The Socket class has two constructors that specify the host and port, one that uses a String for the host and one that uses an InetAddress:
public Socket(String host, int port) throws UnknownHostException, IOException public Socket(InetAddress host, int port) throws IOException
Both constructors connect the socket by establishing an active connection to the remote host.
There are also two constructors that allow you to specify the host and port to connect to as well as the interface and port to connect from:
public Socket(String host, int port, InetAddress interface, intlocalPort) throws IOException, UnknownHostException public Socket(InetAddress host, int port, InetAddress interface, intlocalPort) throws IOException
You can also use the default Socket() constructor to construct a socket without connecting. You can then use the connect method to establish a connection:
public void connect(SocketAddress endpoint) throws IOException public void connect(SocketAddress endpoint, int timeout) throws IOException
SocketAddresses represent a connection endpoint. The SocketAddress class is an empty abstract class with only a default constructor. It provides a convenient place to store socket information that may change. The Socket class provides two methods that let you retrieve SocketAddresses, which will return null if the socket isn't connected:
public SocketAddress getRemoteSocketAddress() public SocketAddress getLocalSocketAddress()
The InetSocketAddress class is a subclass of SocketAddress and is usually created with a host and a port for client or just a port for servers:
public InetSocketAddress(InetAddress address, int port) public InetSocketAddress(String host, int port) public InetSocketAddress(int port)
The InetSocketAddress class has a few accessors you can use to inspect the address:
public final InetAddress getAddress() public final int getPort() public final String getHostName()
You can also use the following constructor to create an unconnected socket that connects through a proxy server:
public Socket(Proxy proxy)
Socket Options
Sockets have four properties that are available via these accessors:
public InetAddress getInetAddress() public int getPort() public InetAddress getLocalAddress() public int getLocalPort()
You cannot set any of the above properties; they are set as soon as the socket connects and are fixed from that point on. There is also an isClosed() method that returns true if the socket is closed and false if it isn't. The toString() method returns a String like the following that can be useful for debugging:
Socket[addr=www.yhscs.us/173.236.147.80,port=80,localport=50055]
There are nine options that Java supports for client-side sockets that specify how the Socket class sends and receives data:
- TCP_NODELAY
- Set to true to ensure packets are sent as quickly as possible regardless of their size. Small packets are typically combined, but not if you set this option to true.
public void setTcpNoDelay(boolean on) throws SocketException public boolean getTcpNoDelay() throws SocketException
- Set to true to allow other sockets to immediately bind to a port when the connection is closed. Typically, a small amount of time must pass before the port is released in case there are lingering packets on the port.
public void setReuseAddress(boolean on) throws SocketException public boolean getReuseAddress() throws SocketException
- Set a timeout that will cause an InterruptedIOException to be thrown if a call takes more than the given milliseconds.
public void setSoTimeout(int milliseconds) throws SocketException public int getSoTimeout() throws SocketException
- Set this to true, and set the linger time to any positive value to wait the given amount of seconds before packets are discarded when a connection is closed. By default, the system will always try to send any remaining data.
public void setSoLinger(boolean on, int seconds) throws SocketException public int getSoLinger() throws SocketException
- This controls the suggested buffer size used for network output. You should try to make this a little less than the latency of the connection to maximize bandwidth.
public void setSendBufferSize(int size) throws SocketException, IllegalArugmentException public int getSendBufferSize() throws SocketException
- This controls the suggested buffer size used for network input. You should try to make this a little less than the latency of the connection to maximize bandwidth.
public void setReceiveBufferSize(int size) throws SocketException, IllegalArugmentException public int getReceiveBufferSize() throws SocketException
- Set this to true to make the client occasionally send a data packet over an idle connection to determine if the server has crashed. If the server does not respond within 12 minutes the connection will close. If this is set to false the connection will stay open even if the server has crashed.
public void setKeepAlive(boolean on) throws SocketException public boolean getKeepAlive() throws SocketException
- This controls how urgent data is handled. Urgent data can be sent with the sendUrgentData(int data) method. If this option is turned on Java will add urgent data to the socket's input stream, otherwise urgent data is ignored.
public void setOOBInline(boolean on) throws SocketException public boolean getOOBInline() throws SocketException
- This lets you specify the class of service for your socket. Different services have different requirements. For example, VOIP needs less bandwidth than streaming video, but needs to minimize jitter. This option is often ignored, however, so a better way to express preferences is with the setPerformancePreferences(int connectionTime, int latency, int bandwidth) method, where you rank connection time, latency, and bandwidth from 1 to 3 with 3 as the most important performance priority.
ServerSockets
The ServerSockets class contains everything necessary to write a server in Java. In Java, the life cycle of a server is the following:
- A new ServerSocket is created on a particular port.
- The ServerSocket listens for incoming connection attempts on that port using the accept() method. Once a connection is made accept() returns a Socket object connecting the client and server.
- Depending on the type of server, either the getInputStream() method, getOutputStream() method, or both methods are called to get input and output streams that communicate with the client.
- The server and client interact using a specified protocol until its time to close the connection.
- The server, the client, or both close the connection.
- The server returns to step 2 and waits for the next connection.
Below is a simple example of a daytime server:
public class Daytime { public final static int PORT = 13; public static void main(String[] args) { try (ServerSocket server = new ServerSocket(PORT)) { while (true) { try (Socket connection = server.accept()) { Writer out = new OutputStreamWriter(connection.getOutputStream()); Date now = new Date(); out.write(now.toString() + "\r\n"); out.flush(); connection.close(); } catch (IOException ex) { System.err.println(ex); } } } } }
The program above creates a new ServerSocket on port 13. An infinite loop then attempts to accept an incoming server connection. This loop may run for hours, days, or weeks waiting for a connection, but once it accepts a connection it get the OutputStreamWriter for the connection. It then writes the current date and time to the connection, flushes the connection, and closes the connection.
The example below is a echo server, which repeats the input sent to it from a client.
import java.net.*; import java.io.*; import java.util.concurrent.*; public class Echo { public final static int PORT = 7; public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(500); try (ServerSocket server = new ServerSocket(PORT)) { while (true) { try { Socket connection = server.accept(); Callable<Void> task = new EchoTask(connection); pool.submit(task); } catch (IOException ex) {} } } catch (IOException ex) { System.err.println("Couldn't start server."); } } private static class EchoTask implements Callable<Void> { private Socket connection; EchoTask(Socket connection) { this.connection = connection; } @Override public Void call() throws IOException { try { InputStream input = new BufferedInputStream(connection.getInputStream()); OutputStream output = connection.getOutputStream(); int c; while ((c = in.read()) != -1) { out.write(c); out.flush(); } } catch (IOException ex) { System.err.println(ex); } finally { connection.close(); } return null; } } }
Server Logs
Servers can run for long periods of time so it's important to be able to debug what happened to a server long after the fact. This is why server logs are so important to keep track of problems on the server.
Requests and server errors are the most common items you would want to store in your server log. Sometimes servers will keep two different logfiles for these two different items. The error log mainly contains unexpected exceptions that occurred while the server was running. It does not contain client errors, which belong in the request log. The general rule with an error log is that every line should be looked at and resolved and an ideal error log should have 0 entries.
You can use the java.util.logging package to create a log, and it is usually easiest to create one log per class like so:
private final static Logger auditLogger = Logger.getLogger("requests");
Loggers are thread safe so they can be stored in a shared static field. Even if the Logger object wasn't shared between thread, the logfile or database has to be shared. Multiple Logger objects can output to the same log, but each logger always logs to exactly one log. Typically the log is a file, but it can also be a database or another Java program. There are 7 levels of logs:
- Level.SEVERE (highest value)
- Level.WARNING
- Level.INFO
- Level.CONFIG
- Level.FINE
- Level.FINER
- Level.FINEST (lowest value)
Lower levels are for debugging and should not be used on production systems. Below is an example of a log message:
catch (RuntimeException ex) { logger.log(Level.SEVERE, "Runtime Error: " + ex.getMessage(), ex); }
By default, the logs are output to the console. However, you can configure the runtime environment so logs go to a more permanent destination. You can do this by setting up a configuration file. The java.util.logging.config.file system property points to a file that controls the logging. You set this property by passing the -Djava.util.logging.config.file=_filename_ argument when launching the virtual machine. An example logging properties file is provided below:
handlers=java.util.logging.FileHandler java.util.logging.FileHandler.pattern = /var/logs/daytime/requests.log java.util.logging.FileHandler.limit = 10000000 java.util.logging.FileHandler.count = 2 java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.FileHandler.append = true java.util.logging.SimpleFormatter.format=%4s: %5s [%1$tc]%n requests.level = INFO audit.level = SEVERE
The above configuration specifies the following:
- Logs should be written to a file
- The request log should be in /var/logs/daytime/requests.log at level Info
- The errors log should be at the same address, but at level Severe
- Limit the log size to 10 megabytes
- Keep two logs, the current one and the previous one
- Use the simple text formatter
- Each line of the logfile should be in the form level message timestamp
Constructing Server Sockets
There are four constructors for creating ServerSockets:
public ServerSocket(int port) throws BindException, IOException public ServerSocket(int port, int queueLength) throws BindException, IOException public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws IOException public ServerSocket() throws IOException
You can specify the port for the server and how many unaccepted connections will be queued at a time. You can also add an IP address to bind the socket to a particular local address. The default constructor creates a ServerSocket but does not bind it to a port. This can be done later using one of the bind() methods:
public void bind(SocketAddress endpoint) throws IOException public void bind(SocketAddress endpoint, int queueLength) throws IOException
There are also two accessors you can use the get the local address and port occupied by the server socket:
public InetAddress getInetAddress() public int getLocalPort()
There are three options that Java supports for server-side sockets that specify how the ServerSocket class sends and receives data:
- SO_TIMEOUT
- Set a timeout that will cause an InterruptedIOException to be thrown if a call takes more than the given milliseconds.
public void setSoTimeout(int milliseconds) throws SocketException public int getSoTimeout() throws SocketException
- Set to true to allow other sockets to immediately bind to a port when the connection is closed. Typically, a small amount of time must pass before the port is released in case there are lingering packets on the port.
public void setReuseAddress(boolean on) throws SocketException public boolean getReuseAddress() throws SocketException
- This controls the suggested buffer size used for network input. You should try to make this a little less than the latency of the connection to maximize bandwidth.
public void setReceiveBufferSize(int size) throws SocketException, IllegalArugmentException public int getReceiveBufferSize() throws SocketException
Secure Sockets
Creating secure sockets is fairly easy in Java. The requirements for secure sockets are spread over four different packages:
- javax.net.ssl
- The abstract classes that define Java's API for secure communications.
- javax.net
- The abstract socket factory classes are used instead of constructors to create secure sockets.
- java.security.cert
- The classes for handling the public-key certificates needed for SSL.
- com.sun.net.ssl
- The concrete classes that implement the encryption algorithms and protocols.
To create an encrypted SSL socket to talk to an existing server socket you use the createSocket() method from javax.net.ssl.SSLSocketFactory like so:
SocketFactory factory = SSLSocketFactory.getDefault(); Socket socket = factory.createSocket("yhscs.us", 7000);
You can use any of the following createSocket methods to create your secure socket:
public abstract Socket createSocket(String host, int port) throws IOException, UnknownHostException public abstract Socket createSocket(InetAddress host, int port) throws IOException public abstract Socket createSocket(String host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException public abstract Socket createSocket(InetAddress host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException public abstract Socket createSocket(Socket proxy, String host, int port, boolean autoClose) throws IOException
All of the above methods create an SSLSocket, but you can treat it just like a regular socket.
You can use the SSLServerSocket class to create a secure server socket. There are three versions of the createServerSocket method you can use:
public abstract ServerSocket createServerSocket(int port) throws IOException public abstract ServerSocket createServerSocket(int port, int queueLength) throws IOException public abstract ServerSocket createServerSocket(int port, int queueLength, InetAddress interface) throws IOException
You can use the getDefault() method of the SSLServerSocketFactory() class, but it does not support encryption, only authentication. To create a secure server socket depends on the implementation, but you generally have to take the following steps:
- Generate public keys and certificates using keytool.
- Pay money to have your certificates authenticated by a trusted third party.
- Create an SSLContext for the algorithm you'll use.
- Create a TrustManagerFactory for the source of certificate material you'll use.
- Create a KeyManagerFactory for the type of key material you'll use.
- Create a KeyStore object for the key and certificate database.
- Fill the KeyStore object with keys and certificates.
- Initialize the KeyManagerFactory with the KeyStore and its passphrase.
- Initialize the context with the necessary key managers from the KeyManagerFactory, tryst managers from the TrustManagerFactory, and a source of randomness.