How to send a WebSocket frame

Hi again Ziggit! :slight_smile:

I’m currently working on a WebSocket project. I was able to create a simple socket connection, and even send an HTTP request to upgrade the connection to a WebSocket connection. My only question is, how do I send a websocket data frame to the websocket server? I looked around for help and found these:

I honestly don’t know how to code this at all in Zig, or let alone, any language as this is my first time doing anything networking related at this deep of a level. I would show the code I have tried, but I was being a script kiddie and used AI to tell me how to do this in C and I tried to translate it to Zig, but it didn’t end up working lol. Any explanations, examples, and hints would be greatly appreciated. And thank you again for always helping me out. I really appreciate you guys.

EDIT:
I decided to add my code in order to prevent confusion.

// client.zig
const std = @import("std");
const fmt = std.fmt;
const mem = std.mem;
const net = std.net;
const time = std.time;
const math = std.math;
const crypto = std.crypto;
const base64 = std.base64;
const random = std.Random;

const RequestUpgradeError = error{ NoResponse, UpgradeFailed };

const SERVER_IP = [4]u8{ 127, 0, 0, 1 };
const SERVER_PORT: u16 = 9001;

const WebSocket = struct {
    stream: net.Stream,

    pub fn recieveFrame(self: *WebSocket) !?[]u8 {
        var buffer: [65535]u8 = undefined;
        const body_index: usize = 134;
        const msg_len = try self.stream.read(&buffer);

        if (body_index < msg_len) {
            return buffer[body_index..msg_len];
        } else {
            return null;
        }
    }

    pub fn sendFrame(self: *WebSocket, msg: []const u8) !void {
        // I need help here :(
    }
};

const Client = struct {
    allocator: std.mem.Allocator,
    server_ip: [4]u8,
    server_ip_str: []u8,
    server_port: u16,
    websocket_key: []u8,
    stream: net.Stream,

    pub fn requestUpgrade(self: *Client) !WebSocket {
        const reader = self.stream.reader();
        var rd_buf: [65535]u8 = undefined;
        const http_payload_fmt = "GET / HTTP/1.1\r\n" ++
            "Host: {s}:{d}\r\n" ++
            "Upgrade: websocket\r\n" ++
            "Connection: Upgrade\r\n" ++
            "Sec-WebSocket-Key: {s}\r\n" ++
            "Sec-WebSocket-Version: 13\r\n\r\n";

        const req_upgrade = try fmt.allocPrint(self.allocator, http_payload_fmt, .{ self.server_ip_str, SERVER_PORT, self.websocket_key });
        defer self.allocator.free(req_upgrade);

        std.debug.print("[*] Sending WebSocket upgrade request to the server...\n", .{});

        try self.stream.writeAll(req_upgrade);

        const res_upgrade = try reader.readUntilDelimiterOrEof(&rd_buf, '\n') orelse return RequestUpgradeError.NoResponse;
        const successful_res = "HTTP/1.1 101 Switching Protocols\r";

        if (mem.eql(u8, res_upgrade, successful_res) == false) return RequestUpgradeError.UpgradeFailed;

        std.debug.print("[*] Session successfully upgraded to a WebSocket connection...\n\n", .{});

        return .{ .stream = self.stream };
    }

    pub fn init(allocator: std.mem.Allocator, ip: [4]u8, port: u16, server_ip_str: []u8, websoc_key: []u8, stream: net.Stream) Client {
        return .{ .allocator = allocator, .server_ip = ip, .server_port = port, .server_ip_str = server_ip_str, .websocket_key = websoc_key, .stream = stream };
    }

    pub fn deinit(self: *Client) void {
        self.stream.close();
        self.allocator.free(self.server_ip_str);
        self.allocator.free(self.websocket_key);
    }
};
// server.ts
console.log("Running WebSocket server!");
Bun.serve({
    fetch(req, server) {
        console.log(req);

        if(server.upgrade(req)) {
            return;
        }

        return new Response("Upgrade failed", { status: 500 });
    },
    websocket: {
        open(ws) {
            console.log("Connection opened!");
            ws.send("Howdy :) welcome to the server!");
        },
        message(ws, message) {
            console.log("Message recieved!");
            ws.send("Thanks for your message :)");
        },
        close(ws, code, reason) {
            console.log("Session closed");
        },
        
    },
    port: 9001,
});

You can connect using std.net.tcpConnectToHost or std.net.tcpConnectToAddress.
Both of these return a TCP Stream that can read and write to the connection.

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    const stream = try std.net.tcpConnectToHost(allocator, "localhost", 8000);
    defer stream.close();
    // ...
}

Hey thanks for the reply :slight_smile:

I’m sorry if my post is confusing, I am able to create a normal TCP socket connection. I then send an HTTP request to upgrade the connection type to a WebSocket connection. I am able to successfully upgrade the connection type, and receive messages from the WebSocket Server. I just need to know how do I send messages to the WebSocket Server. All the help I find says I need to use RFC6455 which I have very little knowledge about. The WebSocket server is written using Bun’s builtin WebSocket tool. I have provided code in my original post for both the client program written in Zig, and the server written in TypeScript.

Thank you again for your help!

Try to use the client library from: GitHub - karlseguin/websocket.zig: A websocket implementation for zig
Use wireshark to capture and visualize the exchanged packets.

2 Likes

Howdy, I was able to solve the issue!

After days of careful reading of the resources I linked in my original post, and comparing and contrasting how people do it in C and in Zig (thanks to @dimdin for linking that GitHub project), I was able to get my program to send a message over a WebSocket connection to the server! :smiley:

Here is a more straight forward version of the function I made. (This is assuming you already created a TCP Socket, connected to the server, sent a handshake, and successfully upgraded the connection to a WebSocket stream):

fn sendTextFrame(allocator: std.mem.Allocator, stream: std.net.Stream, msg: []const u8) !void {
  const pseudo_rand = std.crypto.random;
  var frame_size: usize = undefined;

  if(msg.len <= 125) {
    frame_size = 0;
  } else if (msg.len < 65535) {
    frame_size = 2;
  } else {
    frame_size = 8;
  }

  frame_size += (6 + msg.len); // Prevent any un-needed bytes in the frame.

  var frame = try allocator.alloc(u8, frame_size);
  defer allocator.free(frame);

  frame[0] = 0x81; // (FIN bit == 1 & OpCode bit == 1(Text OpCode)) == 1000 0001

  var masking_key: [4]u8 = undefined;

  for(0..masking_key.len) |i| {
    masking_key[i] = std.Random.int(pseduo_rand, u8) % 255; // This MIGHT not be needed, I'll edit this if it's not.
  }

  if (msg.len <= 125) {
    frame[1] = @as(u8, @intCast(msg.len)) | 128; // == Message length | Mask bit (128) = 1

    for(2..6) |i| {
      frame[i] = masking_key[i - 2]; // Masking key (4 bytes)
    }

    for(msg, 6..) |char, i| {
      frame[i] = char ^ masking_key[(i - 6) % 4]; // Every char within the message gets XOR'd directly with a byte within the masking key, and put into the frame.
    }
  }

  try stream.writeAll(frame);
}

Explanation:

  • This function is only used if the msg.len <= 125. If msg.len is greater than 125, then you would need to handle the frame differently which I do not cover here.
  • Make sure frame.len does not have any unused bytes. Sending a frame with unused bytes to the WebSocket Server kills the connection with a 1006 (Unusual Closure) Error Code. This is the purpose of the frame_size variable.
  • Setting 0x81 to the first byte within frame does two things.
    • Sets the FIN bit to 1. You could split a frame into multiple fragments instead of putting everything into one frame. Setting FIN to 1 tells the WebSocket Server that this is the last fragment of the frame.
    • Sets the OpCode bit to 1. When the OpCode bit is 1, this is telling the WebSocket Server that the type of frame the Client is sending is a Text message. There are multiple types of OpCodes which I do not cover here.
  • Setting the second byte within frame to msg.len and setting the MASK bit to 1 does two things:
    • It tells the WebSocket Server how much of the frame to read.
    • If the WebSocket Server is going to need to de-mask the message when it receives the frame.
    • Basically, it’s msg.len and flipping the furthest left bit (128) to 1.

If there is anything I did wrong or could do better, please let me know! If anyone would like any further explanation, I would be happy to explain more and get into the weeds of it with you :slight_smile: thanks again @dimdin for the help!

5 Likes