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 porb
, começando no índice indicado poroff
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 porb
, começando no índice indicado poroff
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,
- Obter o valor do cabeçalho de pedido da
Sec-WebSocket-Key
sem qualquer espaço em branco - Combine esse valor com "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- Calcule o código SHA-1 e Base64 do mesmo
- 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();
}
}
}