Uma passada de olhos em websockets

Esses dias implementando websockets no tupi-proxy e precisava de um cliente e um servidor websocket pra poder testar e ao invés de pegar algo pronto eu escrevi o que eu precisava. Então pra não ficar parecendo que foi um trabalho inútil, vou escrever sobre websockets agora.

Websockets são uma conexão tcp normal onde assim que a conexão é estabelecida o cliente envia os headers de uma requisição http, com os headers Upgrade: websocket e Connection: upgrade. Ao receber esses headers o servidor responde com esses mesmo headers indicando que suporta websockets. Depois disso o cliente e servidor podem trocar mensagens.

Mas como assim?

Bom, primeiro o servidor tem que estar escutando em uma porta, aí o cliente faz uma conexão tcp e manda uma requisição http GET para o servidor. Assim:

GET /ws HTTP/1.1
Host: myhost.net:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

O header Sec-WebSocket-Key enviado pelo cliente é uma string de 16 caracteres ascii aleatórios encodados em base64.

Ao receber essa requisição o servidor deve responder assim:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

O header Sec-WebSocket-Accept enviado pelo servidor é obtida da seguinte maneira: o servidor concatena o Sec-WebSocket-Key enviado pelo cliente com a string mágica «258EAFA5-E914-47DA-95CA-C5AB0DC85B11», gera um hash sha1 com essa string concatenada e por fim encoda em base64.

Depois desse processo de handshake o servidor e o cliente devem manter a conexão aberta e aí podem trocar mensagens usando o formato descrito na RFC 6455.

O formato das mensagens

As mensagens trocadas via websockets são chamadas de frames. Os frames enviados tanto pelo servidor quanto pelo cliente tem o seguinte formato:

0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

Essa coisa feia aí quer dizer que cada frame é formado da seguinte maneira:

  • Primeiro byte:
    • bit 0: Indica se o frame é um frame final de mensagem ou se a mensagem será continuada em outro frame

    • bits 1 a 3: Bits revervados à extensões.

    • bits 4 a 7: Opcode

  • Segundo byte:
    • bit 0: Indica se uma máscara está sendo usada

    • bits 1 a 7: O tamanho do payload

  • Próximos dois bytes:
    • O tamanho do payload se o tamanho no segundo byte for >= 126.

  • Próximos 8 bytes:
    • O tamanho do payload se o tamanho no segundo byte for == 127

  • Próximos 4 bytes:
    • A máscara se uma estiver sendo usada

O restante dos bytes (até o tamanho do payload) é o payload.

Opcodes e máscara

Os opcodes dão informação sobre o tipo do payload ou podem ser opcodes de controle. O opcode 0 indica que a mensagem é uma continuação da mensagem no frame anterior e o payload desse frame deve ser combinado com o payload do anterior; o opcode 1 indica que o payload é um texto encodado em utf-8; o opcode 2 indica que o payload é um binário; o opcode 8 é um opcode de controle usado para encerrar a conexão; o opcode 9 é um opcode de controle para ping e por fim o opcode 10 é um opcode de controle usado para pong.

A máscara são 32 bits aletórios que que vão encriptar os dados usando XOR. Os clientes obrigatóriamente devem usar máscara quando enviando dados pro servidor e o servidor não deve usar máscara quando enviando mensagens ao cliente.

Bom, é basicamente isso o protocolo de websockets. Agora ao que importa.

Uma implementaçãozinha

Primeiro uma implementação para wire encode e wire decode que vai ser usada tanto pelo cliente quanto pelo servidor.

// Frame é como a gente envia mensagens através do websocket.
// Essa struct representa aquele desenho feio lá de cima.
type Frame struct {
    Opcode   byte
    Len      uint
    Payload  []byte
    Mask     []byte
    IsFinal  bool
    IsMasked bool
}

// WebSocket contém as operações básicas do protocolo
// performadas tanto pelo cliente quanto pelo servidor.
// Note que WebSocket não tem um método para criar uma
// conexão já que a conexão sempre tem que ser criada
// pelo cliente e nunca pelo servidor
type WebSocket struct {
    Conn net.Conn
}

// Send wire encode um frame e envia os bytes em uma conxão
// já aberta
func (ws *WebSocket) Send(fr *Frame) error {
    data, err := ws.WireEncode(fr)
    if err != nil {
        return err
    }

    _, err = ws.Conn.Write(data)
    return err
}

// Recv lê da conexão aberta e retorna o frame recebido.
// Se Recv recebe um frame ping, envia um frame pong e
// volta a ler da conexão. Se recebe um frame close
// retorna um erro io.EOF.
// Note que Recv não fecha a conexão.
func (ws *WebSocket) Recv() (*Frame, error) {

    for {
        fr, err := ws.WireDecode()

        if err != nil {
            return &Frame{}, err
        }

        switch fr.Opcode {
        case OpcodeClose:
            return &Frame{}, io.EOF

        case OpcodePing:
            fr.Opcode = OpcodePong
            err := ws.Send(fr)
            if err != nil {
                return &Frame{}, err
            }

        default:
            return fr, nil

        }
    }
}

// RecvPayload retorna todo o payload da mensagem. Se a mensagem
// estiver divida em mais de um frame, lê todos os frames e
// só aí retorna o payload completo
func (ws *WebSocket) RecvPayload() ([]byte, byte, error) {
    var unfinishedPayload []byte
    unfinishedOpcode := byte(0xFF)
    for {
        fr, err := ws.Recv()
        if err != nil {
            return []byte{}, 0, err
        }
        if !fr.IsFinal {
            unfinishedPayload = append(unfinishedPayload, fr.Payload...)
            if unfinishedOpcode == 0xFF {
                unfinishedOpcode = fr.Opcode
            }
            continue
        }

        if fr.Opcode == OpcodeCont {
            unfinishedPayload = append(unfinishedPayload, fr.Payload...)
            return unfinishedPayload, unfinishedOpcode, nil
        }
        return fr.Payload, fr.Opcode, nil

    }
}

// Close manda um frame de controle close e fecha a conexão.
func (ws *WebSocket) Close() error {
    msg := []byte("close connection")
    fr := Frame{
        Opcode:  OpcodeClose,
        Payload: msg,
        Len:     uint(len(msg)),
        IsFinal: true,
    }
    ws.Send(&fr)
    return ws.Conn.Close()
}

// WireEncode transforma um frame em uma sequencia de bytes
// que vai ser enviada pela conexão.
// WireEncode não força o uso de máscara
func (ws *WebSocket) WireEncode(fr *Frame) ([]byte, error) {
    data := make([]byte, 2)

    if fr.IsFinal {
        // aqui se o frame for o frame final de uma mensagem
        // a gente seta o primeiro bit pra zero.
        data[0] = 0x00
    } else {
        // se não for um frame final a gente seta pra 1
        data[0] = 0x80
    }
    // os quatro últimos bits do primeiro byte são
    // o opcode
    data[0] |= fr.Opcode

    l := len(fr.Payload)

    if l <= 125 {
        // se o payload for menor que 126 bytes
        // o temanho será os últimos 7 bits do
        // primeiro byte
        data[1] = byte(l)

    } else if float64(l) < math.Pow(2, 16) {
        // se o tamanho do payload couber em dois bytes a gente
        // marca os sete últimos bits do segundo byte como 126
        // e marca o tamanho do payload nos próximos dois.
        data[1] = byte(126)
        s := make([]byte, 2)
        binary.BigEndian.PutUint16(s, uint16(l))
        data = append(data, s...)
    } else if float64(l) < math.Pow(2, 64) {
        // se o tamanho do payload cabe em oito bytes marcamos
        // nos próximos 8
        data[1] = byte(127)
        s := make([]byte, 8)
        binary.BigEndian.PutUint64(s, uint64(l))
        data = append(data, s...)
    } else {
        // muito grande. tem que dividir a mensagem em
        // mais de um frame
        return []byte{}, errors.New("Payload muito grande")
    }

    if fr.Mask != nil && len(fr.Mask) > 0 && len(fr.Mask) != 4 {
        return []byte{}, errors.New("Invalid mask")
    }
    if fr.Mask != nil && len(fr.Mask) == 4 {
        // Se uma mascara é usada setamos o primeiro bit
        // do segundo byte para 1 e fazemos o XOR no payload
        data[1] = 0x80 | data[1]
        data = append(data, fr.Mask...)
        xOR(fr.Payload, fr.Mask)
    }
    // e por fim o payload depois da tralha toda
    data = append(data, fr.Payload...)
    return data, nil
}

// WireDecode lê da conexão aberta e retorna o frame recebido.
// Aqui a gente tá basicamente fazendo o contrário do que fizemos
// em WireEncode
func (ws *WebSocket) WireDecode() (*Frame, error) {
    fr := Frame{}
    d := make([]byte, 2)
    _, err := ws.Conn.Read(d)
    if err != nil {
        return nil, err
    }

    // verificando se o primeiro bit é 0 ou 1 pra saber
    // se é um frame final. 0 == final
    final := (d[0] & 0x80) == 0x00

    // Pegando os últimos 4 bits do primeiro byte que
    // são o opcode
    opcode := d[0] & 0x0F

    // Primeiro byte indica se tá usando máscara ou não
    // 1 == tá usando
    isMasked := (d[1] & 0x80) == 0x80

    // os 7 últimos bits do segundo byte pro tamanho do
    // payload. Se for <= 125 já será o tamanho real
    len := d[1] & 0x7F
    l := uint(len)

    fr.Opcode = opcode
    fr.IsFinal = final
    fr.IsMasked = isMasked

    if l == 126 {
        // se o marcado no segundo byte é 126 então o tamanho
        // está nos próximos dois bytes
        d := make([]byte, 2)
        _, err := ws.Conn.Read(d)
        if err != nil {
            return nil, err
        }
        l = uint(binary.BigEndian.Uint16(d))
    } else if l == 127 {
        // se o marcado no segundo byte é 127 então o tamanho
        // está nos próximos 8 bytes
        d := make([]byte, 8)
        _, err := ws.Conn.Read(d)
        if err != nil {
            return nil, err
        }
        l = uint(binary.BigEndian.Uint64(d))
    }

    fr.Len = l

    mask := make([]byte, 4)
    if isMasked {
        // se tá usando máscara, os próximos 4 bytes serão
        // a máscara.
        _, err = ws.Conn.Read(mask)
        if err != nil {
            return nil, err
        }
    }

    // e por fim o payload do frame
    payload := make([]byte, l)
    _, err = ws.Conn.Read(payload)

    if isMasked {
        xOR(payload, mask)
        fr.Mask = mask

    }
    fr.Payload = payload
    return &fr, nil
}

Agora o código do websocket client:

// WebSocketClient é quem inicia a conexão de websocket.
type WebSocketClient struct {
    WebSocket
    URL *url.URL
}

// Handshake envia uma requisição http com headers upgrade
// perguntando se o servidor suporta websockets
func (ws *WebSocketClient) Handshake() error {
    // O hash aqui são 16 caracteres ascii aleatórios encodados
    // em base64
    hash := getSecHashClient()
    req := &http.Request{
        URL:    ws.URL,
        Header: make(http.Header),
    }

    req.Header.Set("Upgrade", "websocket")
    req.Header.Set("Connection", "upgrade")
    req.Header.Set("Sec-WebSocket-Accept", hash)

    err := req.Write(ws.WebSocket.Conn)
    if err != nil {
        return err
    }
    reader := bufio.NewReaderSize(ws.Conn, 4096)
    resp, err := http.ReadResponse(reader, req)
    if err != nil {
        return err
    }

    // O status que o servidor deve retornar informando
    // que suporta websockets é o status 101
    if resp.StatusCode != http.StatusSwitchingProtocols {
        return errors.New("Server does not support websockets")
    }

    if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
        strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
        return errors.New("Invalid response")
    }
    return nil
}

// Send envia um frame ao servidor e antes de enviar
// gera uma máscara para o frame
func (ws *WebSocketClient) Send(fr *Frame) error {
    fr.Mask = getMask()
    return ws.WebSocket.Send(fr)
}

// NewWebSocketClient retorna um cliente de websocket já
// connectado a um servidor que suporta websockets
func NewWebSocketClient(rawURL string) (*WebSocketClient, error) {
    u, err := url.Parse(rawURL)
    if err != nil {
        return &WebSocketClient{}, nil
    }

    hostPort, err := getHostPort(u)
    if err != nil {
        return &WebSocketClient{}, err
    }

    conn, err := net.Dial("tcp", hostPort)
    if err != nil {
        return &WebSocketClient{}, err
    }
    ws := WebSocketClient{
        WebSocket: WebSocket{
            Conn: conn,
        },
        URL: u,
    }

    err = ws.Handshake()
    if err != nil {
        return &WebSocketClient{}, err
    }

    return &ws, nil
}

Agora o server que a única coisa que faz é retornar o que o cliente mandar, mas se for um texto retorna o texto invertido:

// WebSocketServer responde a uma conexão feita pelo cliente.
type WebSocketServer struct {
    WebSocket
    Header http.Header
}

// Handshake retorna status 101 indicando que aceita websockets
func (ws *WebSocketServer) Handshake() error {
    secKey := ws.Header.Get("Sec-WebSocket-Key")
    hash := getSecHashServer(secKey)
    headers := []string{
        "HTTP/1.1 101 Switching Protocols",
        "Upgrade: websocket",
        "Connection: upgrade",
        "Sec-WebSocket-Accept: " + hash,
        "",
        "",
    }
    _, err := ws.Conn.Write([]byte(strings.Join(headers, "\r\n")))
    return err
}

// Recv retorna o frame enviado pelo cliente. Se o cliente enviar
// um frame sem máscara Recv retorna um erro já que o cliente
// sempre tem que usar uma máscara
func (ws *WebSocketServer) Recv() (*Frame, error) {
    fr, err := ws.WebSocket.Recv()

    if err != nil {
        return fr, err
    }
    if !fr.IsMasked {
        return &Frame{}, errors.New("Clients must mask the payload")
    }
    return fr, err
}

// Echo simplesmente retorna a mensagem enviada pelo cliente
// invertendo a string se o payload for utf-8.
func (ws *WebSocketServer) Echo() error {
    for {
        payload, opcode, err := ws.RecvPayload()
        if err != nil && errors.Is(err, io.EOF) {
            log.Println("Connection closed")
            return nil
        }

        if err != nil {
            log.Println(err.Error())
            return err
        }

        if opcode == OpcodeText {
            runes := []rune(string(payload))
            pl := len(runes)
            reversed := make([]rune, pl)
            for i := pl - 1; i >= 0; i-- {
                j := (pl - 1) - i
                reversed[j] = runes[i]
            }
            payload = []byte(string(reversed))
        }

        fr := Frame{
            Payload: payload,
            Opcode:  opcode,
        }
        err = ws.Send(&fr)
        if err != nil {
            log.Println(err.Error())
            return err
        }
    }
}

E por fim pra testar as coisas tudo junto a gente faz um http handler e uma cli.

func wsCli() {

    ws, err := NewWebSocketClient("ws://localhost:8081")
    if err != nil {
        panic(err.Error())
    }

    var msg string
    for {
        fmt.Print(": ")
        reader := bufio.NewReader(os.Stdin)
        msg, err = reader.ReadString('\n')
        if err != nil {
            panic(err.Error())
        }

        frame := Frame{
            Payload: []byte(msg),
            IsFinal: true,
            Opcode:  OpcodeText,
        }
        ws.Send(&frame)
        resp, err := ws.Recv()
        if err != nil {
            panic(err.Error())
        }
        fmt.Printf(string(resp.Payload) + "\n")
    }

}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    h, ok := w.(http.Hijacker)
    if !ok {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    conn, _, err := h.Hijack()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    ws := WebSocketServer{
        WebSocket: WebSocket{
            Conn: conn,
        },
        Header: r.Header,
    }

    defer ws.Close()

    err = ws.Handshake()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    err = ws.Echo()
    if err != nil {
        log.Println(err.Error())
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}

A main function fica assim:

func main() {
    server := flag.Bool("server", false, "start the server")
    client := flag.Bool("client", false, "start the client")

    flag.Parse()

    if !*server && !*client {
        panic("one of server or client must be true")
    }

    if *server && *client {
        panic("only one of server and client can be true")
    }
    if *server {
        log.Fatal(http.ListenAndServe(":8081", http.HandlerFunc(wsHandler)))
    } else {
        wsCli()
    }
}

Agora só compilar assim:

$ go build -o ws ws.go

Inicie o servidor assim:

$ ./ws -server

E agora você pode usar o cliente para falar com o servidor via websockets

$ ./ws -client
: olá, mundo

odnum ,álo
:

O código completo pode ser baixado aqui.

E é isso!