Sockets

A socket is a two-way link that allows two software that are on the network to communicate.

These two softwares perform two functions: that of the client and that of the server. The server provides some service from a known location (IP address + port), and the client accesses this service. This service must implement a well-defined protocol, either a standard one or a custom-designed one.

The port numbers are:

  • The range 0 to 1023 are well-known or system ports. In Linux, you need to be an administrator to have a service on these ports.
  • The range 1024-49151 is the registered ports, assigned by IANA.
  • The range 49152–65535 are dynamic or private, or short-lived ports.

Since both softwares work in the protocol realm, there are no dependencies on each other in the code of the two softwares. But it is common for whoever implements the protocol to provide a client library to be able to access the service. This reduces the code a client has to write, and ensures that it will use the protocol correctly. In Java, the client library is materialized through a jar file and usage documentation.

TCP and UDP

Either TCP or UDP protocols can be used. TCP is connection oriented, and UDP is not. This means that TCP requires a previous connection step between the client and the server in order to communicate. Once the connection is established, TCP ensures that the data reaches the other end or it will indicate that an error has occurred.

In general, packets that must pass in the correct order, without loss, use TCP, while real-time services where later packets are more important than older packets use UDP . For example, file transfer requires maximum accuracy, so it is usually done over TCP, and audio conferencing is often done over UDP, where momentary interruptions may not be noticeable.

TCP needs control packets to establish the connection in three phases: SYN, SYN + ACK and ACK. Each packet sent is answered with an ACK. And finally, a disconnection occurs from both sides with FIN + ACK and ACK.

UDP, on the other hand, only transmits the request/response packets, without any control over the transmission.

Example protocol: ECHO

Below is a visualization of the ECHO protocol using Wireshark, for both the TCP and UDP implementations.

TCP capture (request and response)

UDP capture (request and response)

Example protocol: SMTP

SMTP is a protocol that works on port 25, over TCP. The client sends commands, and the server responds with a status code.

Next we see a conversation (C: client / S: server). This entire conversation is held over an open connection.

C: <client connects to service port 25> C: HELO snark.thyrsus.com the machine identifies itself S: 250 OK Hello snark, glad to meet you the receptor accepts C: MAIL FROM: <esr@thyrsus.com> identification of the sender S: 250 <esr@thyrsus.com>... Sender ok receptor accepts C: RCPT TO: cor@cpmy.com identification of the destination S: 250 root... Recipient ok receptor accepts C: DATA S: 354 Enter mail, end with "." on a line by itself C: Scratch called. He wants to share C: a room with us at Balticon. C: . end of the multiline message S: 250 WAA01865 Message accepted for delivery C: QUIT sender says goodbye S: 221 cpmy.com closing connection receptor disconnects C: <client hangs up>

TCP communication

Java has two classes in the java.net package that allow this:

  • Socket: implementation of the client socket, which allows two software to communicate on the network.
  • ServerSocket: implementation of the server socket, which allows listening to requests received from the network.

The operation is reflected in the following two images: first the client requests a connection, and then the server accepts it, and it is established.

Server to uppercase

This server listens for lines of text and returns the uppercase version.

Communication begins with the client's connection request, and the server's acceptance. These two actions create a shared socket of type Socket, on which both the client and the server can use the methods:

  • getInputStream()
  • getOutputStream()

The client can send text strings, which the server will convert to uppercase.

The protocol we invented states that communication ends when the client sends an empty line. to which the server responds with a goodbye.

This could be server code that implements the described protocol using TCP.

ServerSocket serverSocket = new ServerSocket(PORT); Socket clientSocket = serverSocket.accept(); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); out.println("hola!"); String text; while ((text = in.readLine()).length() > 0) out.println(text.toUpperCase()); out.println("adeu!"); clientSocket.close(); serverSocket.close();

You can test this using the netcat (nc) command.

What would the protocol for this service be like? In pseudocode:

  1. When you connect to the server, send a line with a greeting.
  2. For each line you send, it returns the same line in capital letters.
  3. When you send a blank line, it replies with goodbye, and disconnects.

Next, you can see a client accessing this service, implementing this protocol.

Socket clientSocket = new Socket(HOST, PORT); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String salutacio = in.readLine(); System.out.println("salutacio: " + salutacio); for (String text: new String[]{"u", "dos", "tres"}) { out.println(text); String resposta = in.readLine(); System.out.println(text + " => " + resposta); } out.println(); String comiat = in.readLine(); System.out.println("comiat: " + comiat); in.close(); out.close(); clientSocket.close();

Text-based communication

In the examples we've seen, PrintWriter and BufferedReader are the objects that allow text strings to be used on the OutputStream and InputStream respectively, which are binary media.

The most correct thing in these cases would be to indicate which is the Charset with which we encode the Strings. If we wanted to use UTF-8, it would look like this:

Charset UTF8 = StandardCharsets.UTF_8; PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), UTF8), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF8));

Related to charset, we have conversion to and from String from binary data, which we can perform in UDP. These are the two operations:

byte[] strBytes = "Hola, món!".getBytes(UTF8); String str = new String(strBytes, UTF8);

UDP communication

With UDP there is no connection: packets (Datagrams) are simply sent to a UDP server. If we want to reply, we need to know the destination address and port, which can be obtained from the received package.

A server can be created with:

DatagramSocket socket = new DatagramSocket(PORT)

The client works exactly the same, but the socket is created with:

DatagramSocket socket = new DatagramSocket();

To receive a maximum size package (MIDA):

byte[] buf = new byte[MIDA]; DatagramPacket paquet = new DatagramPacket(buf, MIDA); socket.receive(paquet); // data in packet.getData() and source in packet.getAddress() and packet.getPort()

To send a packet to the server:

InetAddress address = InetAddress.getByName(HOST); paquet = new DatagramPacket(buf, MIDA, address, PORT); socket.send(paquet);

To send a response packet to a client, we need to use the port in the packet it previously sent us:

InetAddress address = paquetRebut.getAddress(); int port = paquetRebut.getPort(); DatagramPacket paquetResposta = new DatagramPacket(buf, buf.length, address, port); socket.send(paquet);

Competition

How can we make servers accept concurrent requests from multiple clients?

We saw it in the UF "Processes and Threads". Each request should be handled in a separate thread. For example, for the case of TCP:

Executor executor = Executors.newFixedThreadPool(NFILS); ServerSocket serverSocket = new ServerSocket(PORT); while (true) { final Socket clientSocket = serverSocket.accept(); Runnable tasca = new Runnable() { public void run() { atendrePeticio(clientSocket); } }; executor.execute(tasca); }

That way, we don't keep new customers waiting when we serve one.

Timers

When we use TCP sockets, connection and data sending operations require confirmation from the other party. By default, the API we've seen has no timers (timeouts), and blocks indefinitely.

There is also the UDP operation of reading a datagram, which by default blocks waiting for a response.

Here are the TCP connection operations:

  • new Socket(host, port...): creation of a socket and connection without timeout.
  • new Socket(): Create an unconnected socket (no blocking).
  • Socket.connect(SocketAddress, timeout): Connect an unconnected socket with a timeout.

Once a TCP socket is created, either client (Socket) or server (ServerSocket), we can change the timeout using Socket.setSoTimeout(int timeout). When the timer expires, an exception of type SocketTimeoutException is thrown. This timeout (in milliseconds) affects the following TCP/UDP operations:

  • ServerSocket.accept(): Accepting a connection from a client to the TCP server.
  • SocketInputStream.read(): Read data to a TCP socket.
  • DatagramSocket.receive(): Reading a UDP datagram.

Closing

Here are some aspects associated with closing a TCP socket:

  • We can check if a TCP connection is closed with Socket.isClosed() and ServerSocket.isClosed().
  • If we close a socket connection with Socket.close(), its InputStream and OutputStream streams are automatically closed.
  • If we close the connection of any of its streams, that of the associated socket is automatically closed.
  • If we do a ServerSocket.close() and it is waiting with a ServerSocket.accept(), it will abort and throw a SocketException.
  • If one socket is waiting with a SocketInputStream.read() and the other socket closes, the read() operation will be aborted and a SocketException will be thrown.
  • If one socket is waiting in a text channel with a BufferedReader.read() or BufferedReader.readLine() and the other socket closes, a -1 or null will be returned, respectively.

Asynchronous communication

Based on the NIO libraries. See this post.