karlseguin / websocket.zig

A websocket implementation for zig
MIT License
283 stars 25 forks source link

Help with the client side #14

Closed hajsf closed 11 months ago

hajsf commented 11 months ago

I'm using zig 0.11.0, and I wrote the below as server.zig:

const std=@import("std");
const websocket = @import("./websocket.zig/src/websocket.zig");
const Conn = websocket.Conn;
const Message = websocket.Message;
const Handshake = websocket.Handshake;

// Define a struct for "global" data passed into your websocket handler
const Context = struct {

};

pub fn main() !void {
    var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = general_purpose_allocator.allocator();

    var context = Context{}; 

    try websocket.listen(Handler, allocator, &context, .{
        .port = 9223,
        .max_headers = 10,
        .address = "127.0.0.1",
    });
}

const Handler = struct {
    conn: *Conn,
    context: *Context,

    pub fn init(h: Handshake, conn: *Conn, context: *Context) !Handler {
        _ = h; // we're not using this in our simple case

        return Handler{
            .conn = conn,
            .context = context,
        };
    }

    pub fn afterInit(self: *Handler) !void {
        _ = self;}

    pub fn handle(self: *Handler, message: Message) !void {
        const data = message.data;
        std.log.debug("{s}", .{data});
        try self.conn.write(data); // echo the message back
    }

    pub fn close(_: *Handler) void {}
};

And it looks to be wroking fine, as I tested it with the html file:

<!DOCTYPE HTML>

<html>
   <head>  
      <script type = "text/javascript">
         function WebSocketTest() {
            if ("WebSocket" in window) {
               alert("WebSocket is supported by your Browser!");
               var ws = new WebSocket("ws://localhost:9223/");

               ws.onopen = function() {
                  ws.send("Ping");
                  alert("Message is sent...");
               };

               ws.onmessage = function (evt) { 
                  var received_msg = evt.data;
                  alert("Message is received...");
                  console.log(`Pong: ${received_msg}`)
               };

               ws.onclose = function() { 
                  alert("Connection is closed..."); 
               };
            } else {
               alert("WebSocket NOT supported by your Browser!");
            }
         }
      </script> 
   </head>
   <body>
      <div id = "sse">
         <a href = "javascript:WebSocketTest()">Run WebSocket</a>
      </div>
   </body>
</html>

And tried to test the same with the zig code, and wrote the below which looks to be wrong:

const std = @import("std");
const websocket = @import("./websocket.zig/src/websocket.zig");

const Handler = struct {
    client: *websocket.Client,

    pub fn handle(self: Handler, message: websocket.Message) !void {
        const data = message.data;
        try self.client.write(@constCast(data)); // echo the message back
    }

    pub fn close(_: Handler) void {}
};

pub fn main() !void {
    var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = general_purpose_allocator.allocator();

    var client = try websocket.connect(allocator, "localhost", 9223, .{});
    defer client.deinit();
    var path = "ws://127.0.0.1:9223/";
    try client.handshake(path, .{
        .timeout_ms = 5000,
        .headers = "host: 127.0.0.1:9223\r\n",
    });

    const handler = Handler{ .client = &client };
    try handler.handle(.{
        .type = .text,
        .data="Ping",
    });
}

And I got the below error:

$ zig run client.zig
Segmentation fault at address 0x2586da
/home/hajsf/zig/websocket.zig/src/framing.zig:52:21: 0x374232 in simpleMask (client)
  payload[i] = b ^ m[i & 3];
                    ^
/home/hajsf/zig/websocket.zig/src/framing.zig:19:13: 0x33f0c9 in mask (client)
  simpleMask(m, payload);
            ^
/home/hajsf/zig/websocket.zig/src/client.zig:220:17: 0x33efac in writeFrame (client)
    framing.mask(&mask, data);
                ^
/home/hajsf/zig/websocket.zig/src/client.zig:172:26: 0x33f252 in write (client)
   return self.writeFrame(.text, data);
                         ^
/home/hajsf/zig/client.zig:9:30: 0x33f2ba in handle (client)
        try self.client.write(@constCast(data)); // echo the message back
                             ^
/home/hajsf/zig/client.zig:28:23: 0x33f439 in main (client)
    try handler.handle(.{ // === websocket.Message{
                      ^
/snap/zig/8988/lib/std/start.zig:379:37: 0x278045 in posixCallMainAndExit (client)
            var i: usize = 0;
                                    ^
/snap/zig/8988/lib/std/start.zig:251:5: 0x277b31 in _start (client)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
Aborted (core dumped)

UPDATE I was able to make the client send to the server by changing the handle function to the below, but not sure if this is the correct way or not, also I could not fugure how can I recieve the response from the server:

    pub fn handle(self: Handler, message: websocket.Message) !void {
        var allocator = std.heap.page_allocator;
        const constData = message.data;
        std.log.debug("{s}", .{constData});
        var data = try allocator.alloc(u8, constData.len);
        defer allocator.free(data);

        std.mem.copy(u8, data, constData);
        try self.client.write(data); // echo the message back
    }

I tried making it also as below, but got the same, it looks I do not understand the handler properly, appreciate your explaination:

    pub fn handle(self: Handler, message: websocket.Message) !void {
        switch (message.type) {
            .binary, .text => {
                var allocator = std.heap.page_allocator;
                const constData = message.data;
                std.log.debug("{s}", .{constData});
                var data = try allocator.alloc(u8, constData.len);
                defer allocator.free(data);

                std.mem.copy(u8, data, constData);
                try self.client.write(data); // echo the message back
            }, //try self.client.write(message.data), // echo the message back
            .ping => {
                var allocator = std.heap.page_allocator;
                const constData = message.data;
                std.log.debug("{s}", .{constData});
                var data = try allocator.alloc(u8, constData.len);
                defer allocator.free(data);

                std.mem.copy(u8, data, constData);
                try self.client.write(data); // echo the message back
            }, // try self.client.writePong(@constCast(message.data)), // @constCast is safe
            .pong => {
                var allocator = std.heap.page_allocator;
                const constData = message.data;
                std.log.debug("{s}", .{constData});
                var data = try allocator.alloc(u8, constData.len);
                defer allocator.free(data);

                std.mem.copy(u8, data, constData);
                try self.client.write(data); // echo the message back
            }, // noop
            .close => self.client.close(),
        }
    }

Appreciate your support so that I can write a function equivalent to the JS function ws.onmessage

karlseguin commented 11 months ago

Your handler's handle is called when a message is received from the server. You should not be calling this directly. You don't call ws.onmessage("some data") in your javascript example.

So you use the client to write to the server (either from within your handler or outside of it), and handle is used when you get a message. Keeping your server as-is, the following client code works:

const std = @import("std");
const websocket = @import("./websocket.zig/src/websocket.zig");

const Handler = struct {
    client: *websocket.Client,

    pub fn handle(_: Handler, message: websocket.Message) !void {
        const data = message.data;
        std.debug.print("CLIENT GOT: {any}\n", .{data});
    }

    pub fn close(_: Handler) void {}
};

pub fn main() !void {
    var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = general_purpose_allocator.allocator();

    var client = try websocket.connect(allocator, "localhost", 9223, .{});
    defer client.deinit();

    const path = "/";
    try client.handshake(path, .{
        .timeout_ms = 5000,
        .headers = "host: 127.0.0.1:9223\r\n",
    });

    const handler = Handler{.client = &client};

    // Starts the read-loop which will take messages from the server and calls
    // your handle method
    const thread = try client.readLoopInNewThread(handler);

    var data = try allocator.dupe(u8, "hello world");
    try client.write(data);

    // blocks until handle returns
    // normally, you'd probably call thread.detach(); to have it run in the
    // back and not block here.
    thread.join();
}

You'll have to ctrl-c this code to end it, since the thread.join() will block on the read-loop (unless one or the other end closes the connection)

karlseguin commented 11 months ago

You might be wondering why I had to do:

    var data = try allocator.dupe(u8, "hello world");
    try client.write(data);

Rather than just doing:

try client.write("hello world");

Technically, it's because write takes a []u8 and not a []const u8. This is why your code was crashing. You were passing a []const u8 to handle and then calling @constCast on it, which is not valid.

The reason the library takes a []u8 is because, in websocket, all client->server messages are masked. You can see this in action, if you std.debug.print("{s}\n", .{data}); AFTER the call to client.write, you'll get garbage output. write mutates the input. That might seem weird, but this masking has to happen. If the library took a []const u8, then it would HAVE to dupe the value in order to be able to write to it. By making it take a []u8, I'm letting the caller decide whether the data can be mutated as-is, or whether to dupe the value and pass that dupe into the library. In cases where the value can be mutated as-is, we avoid the expensive cost of duplicating the value.

karlseguin commented 11 months ago

The example in the readme exists to document the *websocket.Client API. The client is the library code. The handler is your code...and the documentation tries to showcase them separately.

There's nothing stopping you from merging them. Something like this might be more what you were expecting:

const std = @import("std");
const websocket = @import("./websocket.zig/src/websocket.zig");

const Handler = struct {
    client: websocket.Client,

    pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !Handler {
        return .{
            .client = try websocket.connect(allocator, host, port, .{}),
        };
    }

    pub fn deinit(self: *Handler) void {
        self.client.deinit();
    }

    pub fn connect(self: *Handler, path: []const u8) !void {
        try self.client.handshake(path, .{.timeout_ms = 5000});
        const thread = try self.client.readLoopInNewThread(self);
        thread.detach();
    }

    pub fn handle(_: Handler, message: websocket.Message) !void {
        const data = message.data;
        std.debug.print("CLIENT GOT: {any}\n", .{data});
    }

    pub fn write(self: *Handler, data: []u8) !void {
        return self.client.write(data);
    }

    pub fn close(_: Handler) void {}

};

pub fn main() !void {
    var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = general_purpose_allocator.allocator();

    var handler = try Handler.init(allocator, "127.0.0.1", 9223);
    defer handler.deinit();
    // spins up a thread to listen to new messages
    try handler.connect("/");

    var data = try allocator.dupe(u8, "hello world");
    try handler.write(data);

    // without this, we'll exit immediately without having time to receive the
    // echo'd message from the server
    std.time.sleep(std.time.ns_per_s);
}
hajsf commented 11 months ago

Thanks a lot for the detailed explanation and examples, deeply appreciated.