HTML5 é a quinta grande revisão do core da linguagem HTML trazendo novas features e elementos, tentando tratar tópicos que não eram adequadamente cobertos em versões anteriores e trazendo um meio padronizado de se criar aplicações web.
Websockets aparecem como uma interessante nova feature do HTML5. Essa feature define um canal de comunicação full-duplex através do qual mensagens podem ser trocadas entre o lado cliente e servidor de uma aplicação web de forma bidirecional. Abaixo temos uma imagem que mostra o atual suporte a websockets nos navegadores retirada da página http://caniuse.com/:
Assim como o HTML5, websockets ainda estão em versão draft, mas desenvolvedores podem começar a testar esse recurso implementando servidores que suportem uma das versões do protocolo e utilizar clientes que suportem a mesma versão. Atualmente alguns dos mais modernos browsers suportam a versão draft-hixie-thewebsocketprotocol-76 (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76) também conhecida como draft-ietf-hybi-thewebsocketprotocol-00.
Neste post veremos como construir um protótipo de um simples servidor que suporta a versão draft-hixie-thewebsocketprotocol-76 do protocolo utilizando sockets em c#. O servidor será um console application que aceita uma conexão de websocket, recebe mensagens do cliente e envia os dados recebidos de volta em letras maiúsculas. No lado cliente usaremos uma página com HTML/javascript rodando sobre algum navegador que suporte a versão 76 do protocolo. Em testes durante a escrita deste post foi utilizada a versão mais atual do Chrome (no momento a 10.0.648.127).
Overview do projeto
Para estabelecer uma conexão via websocket cliente e servidor precisam se comunicar e verificar que conseguem falar na mesma língua, para isso é necessário se realizar um handshake. Abaixo um exemplo de handshake extraído do documento draft -hixie-thewebsocketprotocol-76:
Exemplo de mensagem de handshake vinda do cliente:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
Sec-WebSocket-Protocol: sample
Upgrade: WebSocket
Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
Origin: http://example.com
^n:ds[4U
Exemplo de resposta vinda do servidor:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample
8jKS'y:G*Co,Wxa-
Explicando de uma forma simplificada, para completar o handshake o servidor precisa analisar a mensagem que veio do cliente e realizar algumas operações sobre os valores de Sec-WebSocket-Key1, Sec-WebSocket-Key2 e os últimos 8 bytes da mensagem (no exemplo acima representada pelo valor ^n:ds[4U) e construir uma mensagem de resposta garantindo ao cliente que os dois lados sabem falar a mesma língua.
Uma vez tendo estabelecida a conexão, os dois lados podem trocar informações via data frames que são delimitados pelos bytes 0x00 e 0xFF e contém dados com codificação UTF-8 entre os delimitadores.
Todos os métodos para realizar o handshake e as trocas de mensagens estão explicados mais detalhadamente no projeto disponibilizado neste post, bem como a implementação de um cliente HTML/javascript. É necessário que antes de testar o projeto seja modificado o ip no arquivo de configuração do servidor (App.config) e no HTML como é visto a seguir.
App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<!--Coloque seu ip aqui-->
<add key="ipaddress" value="SEU_IP_AQUI" />
<add key="port" value="8181" />
<add key="location" value="ts" />
</appSettings>
</configuration>
WebSocketClient.htm
<!--Substuir "SEU_IP_AQUI" pelo ip
que foi configurado no .config do servidor-->
<input id="txtConnect" type="text" value="ws://SEU_IP_AQUI:8181/ts" />
<input id="btnConnect" type="button" value="Connect" onclick="Connect()" />
<br />
<input id="txtSend" type="text" />
<input id="btnSend" type="button" value="Send" onclick="Send()" />
<input id="btnClose" type="button" value="Close" onclick="Close()" />
</form>
</body>
Após configurar o App.config do servidor e o HTML do cliente basta rodar o servidor e acessar o arquivo HTML pelo browser. O projeto com o código pode ser acessado aqui: http://cid-2279647898802a3c.office.live.com/self.aspx/.Public/WebSocket76.zip e os arquivos do servidor e do cliente são os seguintes:
Program.cs
using System;
using System.Configuration;
using System.Net;
using System.Net.Sockets;
namespace WebSocket76
{
class Program
{
static void Main(string[] args)
{
//Pega dados do arquivo de configuracao WebSocket76.exe.config
//(App.config), nao esqueca de colocar seu ip la e no html tambem
string ipAddress = ConfigurationSettings.AppSettings["ipaddress"];
int port = int.Parse(ConfigurationSettings.AppSettings["port"]);
string location = “ws://” + ipAddress + “:” + port
+ “/” + ConfigurationSettings.AppSettings["location"];
TcpListener tcpListener =
new TcpListener(new IPEndPoint(IPAddress.Parse(ipAddress), port));
tcpListener.Start();
Console.WriteLine(“Esperando conexão…”);
Socket client = tcpListener.AcceptSocket();
Console.WriteLine(“Tentativa de conexão iniciada…”);
WebSocketServer wss = new WebSocketServer(client, location);
Console.Read();
}
}
}
WebSocketServer.cs
using System;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace WebSocket76
{
public class WebSocketServer
{
/// <summary>
/// Socket usado como servidor
/// </summary>
private Socket _socketClient;
/// <summary>
/// Thread para receber dados do cliente
/// </summary>
private Thread _dataReceivedThread;
/// <summary>
/// Indica se o servidor esta ativo
/// </summary>
private bool _webSocketServerActive;
/// <summary>
/// Tamanho do buffer de recebimento de dados
/// </summary>
private const int BUFFER_SIZE = 1024;
/// <summary>
/// Indica se o handshake ja foi feito
/// </summary>
private bool _handShakeDone;
/// <summary>
/// Local do servidor
/// </summary>
private string _location;
/// <summary>
/// Construtor
/// </summary>
/// <param name="client">Socket para ser usado como servidor</param>
/// <param name="location">Location</param>
internal WebSocketServer(Socket client, string location)
{
_socketClient = client;
_webSocketServerActive = true;
_dataReceivedThread = new Thread(dataReceived);
_dataReceivedThread.Start();
_location = location;
_handShakeDone = false;
}
/// <summary>
/// Recebe dados do cliente e os trata
/// </summary>
private void dataReceived()
{
try
{
StringBuilder receivedDataString = new StringBuilder();
while (_webSocketServerActive)
{
byte[] data = new byte[BUFFER_SIZE];
int receivedDataLength = _socketClient.Receive(data);
//verifica se a conexao foi fechada
if (receivedDataLength < 1)
{
_webSocketServerActive = false;
}
else if (_webSocketServerActive)
{
//realiza handshake se o mesmo ainda nao tiver sido
//feito
if (!_handShakeDone)
{
Console.WriteLine("Iniciando handshake...");
StartHandShakeProcess(data, receivedDataLength);
_handShakeDone = true;
}
else //pega dados nos outros casos
{
System.Text.UTF8Encoding decoder =
new System.Text.UTF8Encoding();
string stringData =
decoder.GetString(data, 0, receivedDataLength);
receivedDataString.Append(stringData);
//Verifica se e o final da mensagem
if (receivedDataLength > 0 &&
(data[receivedDataLength - 1]
== BitConverter.GetBytes(0xFF)[0]))
{
string strData = receivedDataString.ToString();
strData = strData.Substring(1,
strData.Length - 2);
Console.WriteLine("DADOS RECEBIDOS: "
+ strData);
Console.WriteLine("Enviando dados" +
" de volta para o cliente");
//envia a mensagem recebida de volta
//ao cliente com tudo em letras maiusculas
SendData(strData.ToUpper());
receivedDataString = new StringBuilder();
}
}
}
}
}
catch
{
_webSocketServerActive = false;
}
finally
{
Console.WriteLine("Encerrando servidor");
Shutdown();
Console.WriteLine("Pressione enter para fechar o servidor");
}
}
/// <summary>
/// Finaliza servidor
/// </summary>
internal void Shutdown()
{
_webSocketServerActive = false;
if (_socketClient != null && _socketClient.Connected)
{
_socketClient.Shutdown(SocketShutdown.Both);
_socketClient.Close();
}
}
/// <summary>
/// Envia dados para o cliente
/// </summary>
/// <param name="data">Dados a serem enviados</param>
internal void SendData(string data)
{
byte[] toSend = Encoding.UTF8.GetBytes(data);
SendDataToClient(toSend);
}
/// <summary>
/// Envia dados para o cliente de acordo com thewebsocketprotocol-76
/// </summary>
/// <param name="toSendBack"></param>
private void SendDataToClient(byte[] toSendBack)
{
//Os dataframes sao delimitados pelos bytes 0x00 e 0xFF e
//contem dados com codificacao UTF-8 entre esses delimitadores
try
{
_socketClient.Send(BitConverter.GetBytes(0x00));
_socketClient.Send(toSendBack);
_socketClient.Send(BitConverter.GetBytes(0xFF));
}
catch
{
Shutdown();
}
}
#region Handshake
/// <summary>
/// Iniicia o processo de handshake
/// </summary>
/// <param name="data">Handshake vindo do cliente</param>
/// <param name="receivedDataLength">Tamanho dos dados recebidos</param>
private void StartHandShakeProcess(byte[] data, int receivedDataLength)
{
byte[] last8Bytes = new byte[8];
Array.Copy(data, receivedDataLength - 8, last8Bytes, 0, 8);
System.Text.UTF8Encoding decoder = new System.Text.UTF8Encoding();
string stringHandShakeRequest =
decoder.GetString(data, 0, receivedDataLength - 8);
DoHandShake(stringHandShakeRequest, last8Bytes);
}
/// <summary>
/// Controi a resposta e a envia para o cliente
/// </summary>
/// <param name="stringHandShakeRequest">Parte inicial
/// do handshake</param>
/// <param name="last8Bytes">Ultimos 8 bytes que compoem
/// o handshake</param>
private void DoHandShake(string stringHandShakeRequest,
byte[] last8Bytes)
{
StringBuilder handShakeSB = new StringBuilder();
handShakeSB.Append("HTTP/1.1 101 Web Socket Protocol Handshake"
+ Environment.NewLine);
handShakeSB.Append("Upgrade: WebSocket" + Environment.NewLine);
handShakeSB.Append("Connection: Upgrade" + Environment.NewLine);
string connectionOrigin =
LoadConnectionOriginFromHandShake(stringHandShakeRequest);
handShakeSB.Append("Sec-WebSocket-Origin: " +
connectionOrigin + Environment.NewLine);
handShakeSB.Append("Sec-WebSocket-Location: "
+ _location + Environment.NewLine);
handShakeSB.Append(Environment.NewLine);
byte[] handShakeResponsePart1 =
Encoding.UTF8.GetBytes(handShakeSB.ToString());
byte[] handShakeChallengeResponse =
BuildChallengeResponse(stringHandShakeRequest, last8Bytes);
byte[] handshakeResponse = new byte[handShakeResponsePart1.Length +
handShakeChallengeResponse.Length];
Array.Copy(handShakeResponsePart1,
handshakeResponse, handShakeResponsePart1.Length);
Array.Copy(handShakeChallengeResponse, 0, handshakeResponse,
handShakeResponsePart1.Length,
handShakeChallengeResponse.Length);
Console.WriteLine("Enviando resposta de handshake...");
//Envia resposta para o cliente
int a = _socketClient.Send(handshakeResponse, 0,
handshakeResponse.Length, 0);
}
/// <summary>
/// Gera a resposta a partir da duas chaves e dos ultimos 8 bytes
/// do handshake
/// </summary>
/// <param name="stringHandShakeRequest">Parte inicial
/// do handshake</param>
/// <param name="last8Bytes">Ultimos 8 bytes que compoem
/// o handshake</param>
/// <returns>A resposta</returns>
private byte[] BuildChallengeResponse(string stringHandShakeRequest,
byte[] last8Bytes)
{
byte[] keyResult1 = null;
byte[] keyResult2 = null;
string[] handShakeTextLines = stringHandShakeRequest.Split(
new string[] { Environment.NewLine },
System.StringSplitOptions.RemoveEmptyEntries);
foreach (string line in handShakeTextLines)
{
if (line.Contains("Sec-WebSocket-Key1:"))
{
keyResult1 = getKeyResult(line.Substring(line.IndexOf(":")
+ 2));
}
else if (line.Contains("Sec-WebSocket-Key2:"))
{
keyResult2 = getKeyResult(line.Substring(line.IndexOf(":")
+ 2));
}
}
byte[] challengeResponse = BuildMD5ChallengeResponse(keyResult1,
keyResult2, last8Bytes);
return challengeResponse;
}
/// <summary>
/// Gera um pedaco da resposta final baseado na chave passada como
/// parametro seguindo as especiicacoes de
/// http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
/// </summary>
/// <param name="originalKey">Uma chave</param>
/// <returns>Um numero no formato big-endian 32-bit(byte mais
/// significativo armazenado primeiro)</returns>
private byte[] getKeyResult(string originalKey)
{
byte[] resultKey = null;
StringBuilder keyNumbers = new StringBuilder();
int whiteSpacesCount = 0;
foreach (char c in originalKey.ToCharArray())
{
//peque os digitos da chave e concatene gerando um numero
if (char.IsDigit(c))
{
keyNumbers.Append(c);
}//conte quantos espacos em branco existem
else if (char.IsWhiteSpace(c))
{
whiteSpacesCount++;
}
}
//Divida o numero gerado pela quantidade de espaços em branco
int numericResult = (int)(Convert.ToInt64(keyNumbers.ToString()) /
whiteSpacesCount);
resultKey = BitConverter.GetBytes(numericResult);
//garanta que esta como big-endian
if (BitConverter.IsLittleEndian)
{
Array.Reverse(resultKey);
}
return resultKey;
}
/// <summary>
/// Constroi a resposta em md5 de key1 + key2 + key3
/// 4 bytes big endian key1 + 4 bytes big endian key2 + 8 bytes key3.
/// </summary>
/// <param name="key1">4 bytes big endian key1</param>
/// <param name="key2">4 bytes big endian key2</param>
/// <param name="last8Bytes">8 bytes key3</param>
/// <returns>16 bytes md5 de key1 + key2 + key3</returns>
private byte[] BuildMD5ChallengeResponse(byte[] key1, byte[] key2,
byte[] last8Bytes)
{
byte[] challengeResponse = null;
byte[] challengeArray = new byte[16];
Array.Copy(key1, 0, challengeArray, 0, 4);
Array.Copy(key2, 0, challengeArray, 4, 4);
Array.Copy(last8Bytes, 0, challengeArray, 8, 8);
MD5 md5Gen = MD5.Create();
challengeResponse = md5Gen.ComputeHash(challengeArray);
return challengeResponse;
}
/// <summary>
/// Pega a origin do handshake
/// </summary>
/// <param name="stringHandShakeRequest">Handshake</param>
/// <returns>Origin</returns>
private string LoadConnectionOriginFromHandShake(
string stringHandShakeRequest)
{
string connectionOrigin = null;
string[] handShakeTextLines = stringHandShakeRequest.Split(
new string[] { Environment.NewLine },
System.StringSplitOptions.RemoveEmptyEntries);
foreach (string line in handShakeTextLines)
{
if (line.Contains("Origin:"))
{
connectionOrigin = line.Substring(line.IndexOf(":") + 2);
}
}
return connectionOrigin;
}
}
#endregion
}
WebSocketClient.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Web socket client test</title>
<script type="text/javascript">
var wsClient;
//Verifica se o browser da suporte a websockets,
//mas nao verifica versao do protocolo
function WebSocketTest() {
if (!("WebSocket" in window)) {
alert("Este browser não suporta websockets!");
}
};
//realiza a conexao
function Connect() {
try {
//cria cliente websocket
wsClient =
new WebSocket(document.getElementById("txtConnect").value);
document.getElementById("txtConnect").disabled = true;
document.getElementById("btnConnect").disabled = true;
//cadastra eventos
//chamado ao realizar a conexao com sucesso
wsClient.onopen = OnOpen;
//chamado quando o servidor manda mensagens para o cliente
wsClient.onmessage = OnMessage;
//chamado em caso de fechamento de conexao
wsClient.onclose = OnClose;
//chamado em caso de erro
wsClient.onerror = OnError;
} catch (ex) {
alert(ex);
}
};
//envia mensagem para o servidor
function Send() {
wsClient.send(document.getElementById("txtSend").value);
document.getElementById("txtSend").value = "";
};
//fecha conexao com servidor
function Close() {
try {
wsClient.close();
} catch (ex) {
alert(ex);
}
};
function OnOpen() {
alert("Conexão realizada com sucesso!");
};
function OnMessage(evt) {
alert("Mensagem recebida: \"" + evt.data + "\"");
};
function OnClose() {
alert("Conexão fechada!");
};
function OnError() {
alert("Erro!");
};
</script>
</head>
<body onload="WebSocketTest()">
<form>
<!--Substuir "SEU_IP_AQUI" pelo ip
que foi configurado no .config do servidor-->
<input id="txtConnect" type="text" value="ws://SEU_IP_AQUI:8181/ts" />
<input id="btnConnect" type="button" value="Connect" onclick="Connect()" />
<br />
<input id="txtSend" type="text" />
<input id="btnSend" type="button" value="Send" onclick="Send()" />
<input id="btnClose" type="button" value="Close" onclick="Close()" />
</form>
</body>
</html>
Considerações finais
Websocket parece ser uma tecnologia promissora. Vale ressaltar que o w3c pretende finalizar a normalização do html5 até 2014 e acontecerão melhorias até lá. O protocolo usado pelo websocket provavelmente ainda passará por algumas mudanças para melhorar, por exemplo, questões relacionadas à segurança.
Seguem alguns links interessantes:
- Especificação da API: http://dev.w3.org/html5/websockets/
- Versão do protocolo utilizada no exemplo: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
- Versão do protocolo mais atual: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-06
- Site com tabelas que indicam o suporte a tecnologias como HTML5, CSS3, SVG: http://caniuse.com/
- Texto falando sobre algumas vantagens no uso de websockets: http://websocket.org/quantum.html
- Tutorial introdutório sobre websockets: http://www.html5rocks.com/tutorials/websockets/basics/
- Demos do HTML5 em geral: http://html5demos.com/
.

home








blog de design do c.e.s.a.r.