Technè

Tecnologia & Experiência do Usuário no C.E.S.A.R

HTML5: Construindo um servidor de WebSockets com .net framework (C#)

Posted by Rafael Amaral On março - 10 - 2011

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/:

Suporte a Websockets

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:

.

Categorias: Geral

2 Responses to “HTML5: Construindo um servidor de WebSockets com .net framework (C#)”

  1. Moisés says:

    Muito bom o tutorial. Sempre bom ver estes conteúdos em português. :P

    Olá. alterei o app.config e o WebSocketClient.htm com o meu ip local
    porém a aplicação está sempre como “Esperando conexão…”
    Saberias me dizer o que pode ser?

  2. Olá Moisés, que bom que você gostou!

    Eu acabei de baixar o código e testar rodando o servidor e usando o “Chrome 12.0.712.0 dev” como navegador para rodar o “WebSocketClient.htm” e consegui fazer a conexão e trocar mensagens normalmente.
    Geralmente a aplicação fica com essa mensagem até que você inicie a conexão usando o “WebSocketClient.htm”.

    Que navegador você está usando para testar o “WebSocketClient.htm”? Talvez seja problema de incompatibilidade da versão do protocolo com a versão do seu navegador, mas sem mais detalhes eu não consegui reproduzir o comportamento aqui.

Leave a Reply