Escrever um servidor WebSocket em Java

Introdução

Este exemplo mostra-lhe como criar um servidor com API de WebSocket utilizando Oracle Java.

Embora outras linguagens do lado do servidor possam ser utilizadas para criar um servidor WebSocket, este exemplo utiliza Oracle Java para simplificar o código do exemplo.

Este servidor está em conformidade com o RFC 6455, pelo que apenas trata de ligações a partir das seguintes versões dos navegadores; Chrome 16, Firefox 11, IE 10 e superior.

Primeiros passos

Os WebSockets comunicam através de uma ligação TCP (Transmission Control Protocol). A classe ServerSocket do Java está localizada no pacote java.net.

ServerSocket

Construtor

ServerSocket(int port)
Quando se instância a classe ServerSocket, esta é ligada ao número da porta que se especificou pelo argumento port.

Aqui está uma implementação dividida em partes:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class WebSocket {
	public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
		ServerSocket server = new ServerSocket(80);
		try {
			System.out.println("Server has started on 127.0.0.1:80.\r\nWaiting for a connection...");
			Socket client = server.accept();
			System.out.println("A client connected.");

Socket

Métodos

getInputStream()
Devolve uma input stream para este socket.
getOutputStream()
Devolve uma output stream para este socket.

OutputStream

Métodos

write(byte[] b, int off, int len)
Escreve o número de bytes especificado por len a partir da matriz de bytes especificada por b, começando no índice indicado por off para este output stream.

InputStream

Métodos

read(byte[] b, int off, int len)
Lê até um número de bytes especificado por len da matriz de bytes especificada por b, começando no índice indicado por off para este input stream.

Vamos continuar o nosso exemplo.

			InputStream in = client.getInputStream();
			OutputStream out = client.getOutputStream();
			Scanner s = new Scanner(in, "UTF-8");

Handshaking (aperto de mão)

Quando um cliente se liga a um servidor, envia um pedido GET para actualizar a ligação do protocolo HTTP a uma ligação WebSocket. Isto é conhecido como aperto de mão.

			try {
				String data = s.useDelimiter("\\r\\n\\r\\n").next();
				Matcher get = Pattern.compile("^GET").matcher(data);

Criar a resposta é mais fácil do que compreender porque o deve fazer desta forma.

Você deve,

  1. Obter o valor do cabeçalho de pedido da Sec-WebSocket-Key sem qualquer espaço em branco
  2. Combine esse valor com "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  3. Calcule o código SHA-1 e Base64 do mesmo
  4. Devolve-o como o valor do cabeçalho de resposta Sec-WebSocket-Accept numa resposta HTTP.
				if (get.find()) {
					Matcher match = Pattern.compile("Sec-WebSocket-Key: (.*)").matcher(data);
					match.find();
					byte[] response = ("HTTP/1.1 101 Switching Protocols\r\n"
						+ "Connection: Upgrade\r\n"
						+ "Upgrade: websocket\r\n"
						+ "Sec-WebSocket-Accept: "
						+ Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1").digest((match.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("UTF-8")))
						+ "\r\n\r\n").getBytes("UTF-8");
					out.write(response, 0, response.length);

Descodificar mensagens

Após um aperto de mão bem-sucedido, o cliente pode enviar mensagens para o servidor, mas agora estas estão codificadas.

Se enviarmos "abcdef", recebemos estes bytes:

129 134 167 225 225 210 198 131 130 182 194 135

129 (FIN, RSV1, RSV2, RSV3, opcode)

Pode enviar a sua mensagem em fragmentos, mas por enquanto o FIN é 1 indicando que a mensagem está toda num fragmento. RSV1, RSV2 e RSV3 são sempre 0, a não ser que um significado para os bytes é negociado. Opcode 0x1 significa que isto é um texto (ver lista completa de opcodes).

FIN (É o último fragmento da mensagem?) RSV1 RSV2 RSV3 Opcode
1 0 0 0 0x1=0001

134 (comprimento da mensagem)

O comprimento da mensagem é indicada das seguintes formas:

  • Se o segundo byte menos 128 estiver entre 0 e 125, este é o comprimento da mensagem,
  • Se o resultado é 126, os seguintes 2 bytes (inteiro de 16 bits sem assinatura) ditam o comprimento,
  • E se o resultado é 127, os 8 bytes seguintes (64-bit inteiro não assinado, o bit mais significativo DEVE ser 0) são o comprimento.

167, 225, 225 e 210 (chave para descodificar)

Estes são os bytes da chave para descodificar. A chave muda para cada mensagem.

198, 131, 130, 182, 194, 135 (conteúdo da mensagem)

Os restantes bytes codificados são a mensagem.

Algoritmo para descodificar

O algoritmo usado para descodificar a mensagem é o seguinte:

Di = Ei XOR M(i mod 4)

onde D é a série de bytes da mensagem descodificados, E é a série de bytes da mensagem codificados, M é a série de bytes da chave, e i é o índice do byte da mensagem a ser descodificado.

Exemplo em Java:

					byte[] decoded = new byte[6];
					byte[] encoded = new byte[] { (byte) 198, (byte) 131, (byte) 130, (byte) 182, (byte) 194, (byte) 135 };
					byte[] key = new byte[] { (byte) 167, (byte) 225, (byte) 225, (byte) 210 };
					for (int i = 0; i < encoded.length; i++) {
						decoded[i] = (byte) (encoded[i] ^ key[i & 0x3]);
					}
				}
			} finally {
				s.close();
			}
		} finally {
			server.close();
		}
	}
}

Ver também