Introdução
WebSocket é um protocolo de comunicação bidirecional que opera sobre TCP. Diferente do HTTP tradicional (request-response), WebSocket permite que tanto o cliente quanto o servidor enviem mensagens a qualquer momento, sendo ideal para aplicações em tempo real como chats, dashboards e jogos online.
Nesta receita, você aprenderá a implementar um cliente WebSocket em Zig, desde o handshake HTTP até o envio e recebimento de frames.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento de TCP em Zig
- Familiaridade com o protocolo HTTP
Protocolo WebSocket
O WebSocket começa com um handshake HTTP e depois troca frames binários:
- Handshake: O cliente envia um HTTP Upgrade request
- Comunicação: Mensagens são trocadas como frames WebSocket
- Fechamento: Troca de frames Close para encerrar
Cliente WebSocket Básico
Implementação de um cliente WebSocket usando a biblioteca padrão:
const std = @import("std");
const net = std.net;
const crypto = std.crypto;
const WebSocketClient = struct {
stream: net.Stream,
allocator: std.mem.Allocator,
const Opcode = enum(u4) {
continuation = 0x0,
text = 0x1,
binary = 0x2,
close = 0x8,
ping = 0x9,
pong = 0xA,
};
pub fn connect(
allocator: std.mem.Allocator,
host: []const u8,
port: u16,
path: []const u8,
) !WebSocketClient {
// Conectar via TCP
const stream = try net.tcpConnectToHost(allocator, host, port);
errdefer stream.close();
// Gerar chave WebSocket aleatória
var key_bytes: [16]u8 = undefined;
crypto.random.bytes(&key_bytes);
const key = std.base64.standard.Encoder.encode(&[_]u8{0} ** 24, &key_bytes);
// Enviar handshake HTTP
var buf: [1024]u8 = undefined;
const request = try std.fmt.bufPrint(&buf,
"GET {s} HTTP/1.1\r\n" ++
"Host: {s}\r\n" ++
"Upgrade: websocket\r\n" ++
"Connection: Upgrade\r\n" ++
"Sec-WebSocket-Key: {s}\r\n" ++
"Sec-WebSocket-Version: 13\r\n" ++
"\r\n",
.{ path, host, key },
);
_ = try stream.write(request);
// Ler resposta do handshake
var response_buf: [1024]u8 = undefined;
const n = try stream.read(&response_buf);
const response = response_buf[0..n];
// Verificar se o upgrade foi aceito
if (std.mem.indexOf(u8, response, "101") == null) {
return error.HandshakeFailed;
}
return .{
.stream = stream,
.allocator = allocator,
};
}
/// Enviar uma mensagem de texto
pub fn sendText(self: *WebSocketClient, message: []const u8) !void {
try self.sendFrame(.text, message);
}
/// Enviar um frame WebSocket mascarado
fn sendFrame(self: *WebSocketClient, opcode: Opcode, payload: []const u8) !void {
var header: [14]u8 = undefined;
var header_len: usize = 2;
// Byte 1: FIN + opcode
header[0] = 0x80 | @intFromEnum(opcode);
// Byte 2: MASK + payload length
if (payload.len < 126) {
header[1] = 0x80 | @intCast(payload.len);
} else if (payload.len <= 65535) {
header[1] = 0x80 | 126;
header[2] = @intCast((payload.len >> 8) & 0xFF);
header[3] = @intCast(payload.len & 0xFF);
header_len = 4;
} else {
header[1] = 0x80 | 127;
const len: u64 = @intCast(payload.len);
inline for (0..8) |i| {
header[2 + i] = @intCast((len >> @intCast(56 - i * 8)) & 0xFF);
}
header_len = 10;
}
// Gerar máscara
var mask: [4]u8 = undefined;
crypto.random.bytes(&mask);
@memcpy(header[header_len..][0..4], &mask);
header_len += 4;
// Enviar header
_ = try self.stream.write(header[0..header_len]);
// Enviar payload mascarado
var masked = try self.allocator.alloc(u8, payload.len);
defer self.allocator.free(masked);
for (payload, 0..) |byte, i| {
masked[i] = byte ^ mask[i % 4];
}
_ = try self.stream.write(masked);
}
/// Receber uma mensagem
pub fn receive(self: *WebSocketClient, buffer: []u8) !?struct {
opcode: Opcode,
data: []const u8,
} {
var header: [2]u8 = undefined;
const header_read = try self.stream.read(&header);
if (header_read == 0) return null;
const opcode: Opcode = @enumFromInt(header[0] & 0x0F);
const masked = (header[1] & 0x80) != 0;
var payload_len: u64 = header[1] & 0x7F;
if (payload_len == 126) {
var ext: [2]u8 = undefined;
_ = try self.stream.read(&ext);
payload_len = (@as(u64, ext[0]) << 8) | ext[1];
} else if (payload_len == 127) {
var ext: [8]u8 = undefined;
_ = try self.stream.read(&ext);
payload_len = 0;
inline for (0..8) |i| {
payload_len |= @as(u64, ext[i]) << @intCast(56 - i * 8);
}
}
// Ler máscara se presente
var mask: [4]u8 = .{ 0, 0, 0, 0 };
if (masked) {
_ = try self.stream.read(&mask);
}
// Ler payload
const len: usize = @intCast(@min(payload_len, buffer.len));
var total_read: usize = 0;
while (total_read < len) {
const n = try self.stream.read(buffer[total_read..len]);
if (n == 0) break;
total_read += n;
}
// Desmascarar se necessário
if (masked) {
for (buffer[0..total_read], 0..) |*byte, i| {
byte.* ^= mask[i % 4];
}
}
return .{
.opcode = opcode,
.data = buffer[0..total_read],
};
}
/// Enviar frame de ping
pub fn ping(self: *WebSocketClient) !void {
try self.sendFrame(.ping, "");
}
/// Fechar a conexão WebSocket
pub fn close(self: *WebSocketClient) void {
self.sendFrame(.close, "") catch {};
self.stream.close();
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Conectar ao servidor WebSocket
var ws = try WebSocketClient.connect(
allocator,
"echo.websocket.org",
80,
"/",
);
defer ws.close();
std.debug.print("Conectado ao WebSocket!\n", .{});
// Enviar mensagem
try ws.sendText("Olá, WebSocket!");
// Receber resposta
var buffer: [4096]u8 = undefined;
if (try ws.receive(&buffer)) |msg| {
std.debug.print("Recebido ({s}): {s}\n", .{
@tagName(msg.opcode),
msg.data,
});
}
}
Loop de Mensagens
Para uma aplicação interativa que mantém a conexão aberta:
const std = @import("std");
// Usando o WebSocketClient definido acima
pub fn messageLoop(ws: *WebSocketClient) !void {
var buffer: [4096]u8 = undefined;
// Enviar algumas mensagens
const messages = [_][]const u8{
"Primeira mensagem",
"Segunda mensagem",
"Terceira mensagem",
};
for (messages) |msg| {
try ws.sendText(msg);
std.debug.print("Enviado: {s}\n", .{msg});
// Esperar resposta
if (try ws.receive(&buffer)) |response| {
switch (response.opcode) {
.text => std.debug.print("Texto: {s}\n", .{response.data}),
.ping => {
std.debug.print("Ping recebido, enviando pong...\n", .{});
// Responder com pong automaticamente
},
.close => {
std.debug.print("Servidor solicitou fechamento.\n", .{});
return;
},
else => std.debug.print("Frame tipo: {s}\n", .{@tagName(response.opcode)}),
}
}
std.time.sleep(std.time.ns_per_s);
}
}
Dicas e Boas Práticas
Sempre mascare frames: O protocolo WebSocket exige que frames do cliente sejam mascarados com uma chave aleatória de 4 bytes.
Responda a Pings: Servidores enviam frames Ping para manter a conexão. Responda sempre com Pong.
Trate desconexões: A conexão pode ser fechada pelo servidor a qualquer momento. Implemente reconexão automática se necessário.
Use TLS para segurança: Em produção, use
wss://(WebSocket Secure) em vez dews://.Fragmente mensagens grandes: O protocolo suporta fragmentação para mensagens maiores que o buffer.
Receitas Relacionadas
- Como criar um cliente TCP em Zig - Base do WebSocket
- Como fazer requisições HTTP GET em Zig - Handshake usa HTTP
- Como fazer lookup DNS em Zig - Resolução de nomes
- Como usar Base64 em Zig - Usado no handshake