Yorkville High School Computer Science Department
Yorkville High School Computer Science Department on Facebook  Yorkville High School Computer Science Department Twitter Feed  Yorkville High School Computer Science Department on Instagram

Yorkville High School Computer Science

ASSIGNMENTS: Compiler Part 2 - December 3, 2018 :: Challenges 7 - December 7, 2018 :: Network App - December 14, 2018 >>

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:

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:

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:

ServerSockets

The ServerSockets class contains everything necessary to write a server in Java. In Java, the life cycle of a server is the following:

  1. A new ServerSocket is created on a particular port.
  2. 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.
  3. 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.
  4. The server and client interact using a specified protocol until its time to close the connection.
  5. The server, the client, or both close the connection.
  6. 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:

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:

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:

Secure Sockets

Creating secure sockets is fairly easy in Java. The requirements for secure sockets are spread over four different packages:

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:

  1. Generate public keys and certificates using keytool.
  2. Pay money to have your certificates authenticated by a trusted third party.
  3. Create an SSLContext for the algorithm you'll use.
  4. Create a TrustManagerFactory for the source of certificate material you'll use.
  5. Create a KeyManagerFactory for the type of key material you'll use.
  6. Create a KeyStore object for the key and certificate database.
  7. Fill the KeyStore object with keys and certificates.
  8. Initialize the KeyManagerFactory with the KeyStore and its passphrase.
  9. Initialize the context with the necessary key managers from the KeyManagerFactory, tryst managers from the TrustManagerFactory, and a source of randomness.
Yorkville High School Computer Science Department on Facebook Yorkville High School Computer Science Department Twitter Feed Yorkville High School Computer Science Department on Instagram