mirror of
https://github.com/zigzap/zap.git
synced 2025-10-20 15:14:08 +00:00
websockets + example
This commit is contained in:
parent
ab8294b2a1
commit
00291117d8
10 changed files with 775 additions and 12 deletions
|
@ -46,6 +46,8 @@ Here's what works:
|
|||
itself query parameters of all supported types.
|
||||
- **[cookies](examples/cookies/cookies.zig)**: a simple example sending
|
||||
itself a cookie and responding with a session cookie.
|
||||
- **[websockets](examples/websockets/websockets.zig)**: a simple websockets chat
|
||||
for the browser.
|
||||
|
||||
|
||||
I'll continue wrapping more of facil.io's functionality and adding stuff to zap
|
||||
|
|
|
@ -52,6 +52,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||
.{ .name = "endpoint_auth", .src = "examples/endpoint_auth/endpoint_auth.zig" },
|
||||
.{ .name = "http_params", .src = "examples/http_params/http_params.zig" },
|
||||
.{ .name = "cookies", .src = "examples/cookies/cookies.zig" },
|
||||
.{ .name = "websockets", .src = "examples/websockets/websockets.zig" },
|
||||
}) |excfg| {
|
||||
const ex_name = excfg.name;
|
||||
const ex_src = excfg.src;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
.{
|
||||
.name = "zap",
|
||||
.version = "0.0.15",
|
||||
.version = "0.0.16",
|
||||
|
||||
.dependencies = .{
|
||||
.@"facil.io" = .{
|
||||
.url = "https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.7.tar.gz",
|
||||
.hash = "1220d03e0579bbb726efb8224ea289b26227bc421158b45c1b16a60b31bfa400ab33",
|
||||
|
||||
.url = "https://github.com/zigzap/facil.io/archive/refs/tags/zap-0.0.8.tar.gz",
|
||||
.hash = "122071fcc675e114941331726291ca1f0c0c33751d992782c6abf1f0f2ddddc5734d",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
209
examples/websockets/frontend/index.html
Normal file
209
examples/websockets/frontend/index.html
Normal file
|
@ -0,0 +1,209 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<script src="showdown.min.js"></script>
|
||||
|
||||
<title>ZAP-Chat</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #cd0f0d;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
/* height: 500px; */
|
||||
height: calc(100vh - 260px);
|
||||
margin-top: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 80%;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 10px;
|
||||
/* font-size: 1.2rem; */
|
||||
}
|
||||
|
||||
.bot {
|
||||
align-self: flex-start;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.user {
|
||||
align-self: flex-end;
|
||||
background-color: #cd0f0d;;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.busy-indicator {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #ffffff;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #f0f0f0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-bottom: 0.8rem;
|
||||
padding-top: 1rem;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
/* box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
.input-container input {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #cd0f0d;
|
||||
outline: none;
|
||||
/* font-size: 1.2rem; */
|
||||
}
|
||||
|
||||
.input-container button {
|
||||
background-color: #cd0f0d;;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 2rem;
|
||||
cursor: pointer;
|
||||
margin-left: 1rem;
|
||||
outline: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.input-container button:hover {
|
||||
background-color: #A20000;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background-color: #8B0000;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
position: fixed;
|
||||
/* bottom: 60px; */
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
/* border-top-left-radius: 10px; */
|
||||
/* border-top-right-radius: 10px; */
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-online {
|
||||
/* Green, MediumSeaGreen */
|
||||
background-color: #3CB371;
|
||||
}
|
||||
.status-busy {
|
||||
/* Blue, DodgerBlue */
|
||||
background-color: #1E90FF;
|
||||
}
|
||||
.status-thinking {
|
||||
/* Light Gray, LightSlateGray */
|
||||
background-color: #778899;
|
||||
}
|
||||
.status-offline {
|
||||
/* Red, Tomato */
|
||||
background-color: #FF6347;
|
||||
}
|
||||
.status-warning {
|
||||
/* Yellow, Goldenrod */
|
||||
background-color: #DAA520;
|
||||
}
|
||||
.status-error {
|
||||
/* Dark-Red, Firebrick */
|
||||
background-color: #B22222;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>ZAP-Chat</h1>
|
||||
</header>
|
||||
<div class="chat-container" id="chat-container">
|
||||
<!-- ... -->
|
||||
|
||||
<!-- ... -->
|
||||
</div>
|
||||
|
||||
|
||||
<div class="input-container">
|
||||
<input id="prompt-input" type="text" placeholder="Type your message...">
|
||||
<button id="send-button">Send</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Load the script and start executing -->
|
||||
<script>
|
||||
var header = document.getElementById('header');
|
||||
var scriptTag = document.createElement("script");
|
||||
scriptTag.src = "index.js?version=0";
|
||||
scriptTag.type = "module";
|
||||
document.head.appendChild(scriptTag);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
69
examples/websockets/frontend/index.js
Normal file
69
examples/websockets/frontend/index.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
// import { show_welcome } from "./screens/welcome_screen.js?version=0";
|
||||
|
||||
var chatContainer = document.getElementById("chat-container");
|
||||
var promptInput = document.getElementById("prompt-input");
|
||||
var sendButton = document.getElementById("send-button");
|
||||
|
||||
// var ws;
|
||||
|
||||
function addBotMessage(message) {
|
||||
var msg = document.createElement("DIV");
|
||||
msg.classList.add("message");
|
||||
msg.classList.add("bot");
|
||||
|
||||
let converter = new showdown.Converter();
|
||||
msg.innerHTML = converter.makeHtml(message);
|
||||
chatContainer.appendChild(msg);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function addUserMessage(message) {
|
||||
var msg = document.createElement("DIV");
|
||||
msg.classList.add("message");
|
||||
msg.classList.add("user");
|
||||
|
||||
let converter = new showdown.Converter();
|
||||
msg.innerHTML = converter.makeHtml(message);
|
||||
chatContainer.appendChild(msg);
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function onSend() {
|
||||
let prompt = promptInput.value;
|
||||
console.log(prompt);
|
||||
// addUserMessage(prompt);
|
||||
promptInput.value = "";
|
||||
ws.send(prompt);
|
||||
promptInput.focus();
|
||||
}
|
||||
|
||||
function show_markdown_body(container, task, body) {
|
||||
let converter = new showdown.Converter();
|
||||
let m = document.createElement("DIV");
|
||||
m.innerHTML = converter.makeHtml(body);
|
||||
m.classList.add("message");
|
||||
container.appendChild(m);
|
||||
}
|
||||
|
||||
function init() {
|
||||
sendButton.onclick = onSend;
|
||||
promptInput.addEventListener("keypress", function(event) {
|
||||
if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
sendButton.click();
|
||||
}
|
||||
});
|
||||
promptInput.focus();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
|
||||
var ws = new WebSocket("ws://localhost:3010/chat");
|
||||
ws.onmessage = function(e) { addBotMessage(e.data); return false;};
|
||||
ws.onclose = function(e) { console.log("closed"); };
|
||||
ws.onopen = function(e) { /*e.target.send("Yo!");*/ return false;};
|
||||
|
||||
|
||||
|
||||
|
3
examples/websockets/frontend/showdown.min.js
vendored
Normal file
3
examples/websockets/frontend/showdown.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
180
examples/websockets/websockets.zig
Normal file
180
examples/websockets/websockets.zig
Normal file
|
@ -0,0 +1,180 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap");
|
||||
const WebSockets = zap.WebSockets;
|
||||
|
||||
const Context = struct {
|
||||
userName: []const u8,
|
||||
channel: []const u8,
|
||||
// we need to hold on to them and just re-use them for every incoming connection
|
||||
subscribeArgs: WebsocketHandler.SubscribeArgs,
|
||||
settings: WebsocketHandler.WebSocketSettings,
|
||||
};
|
||||
|
||||
const ContextList = std.ArrayList(*Context);
|
||||
|
||||
const ContextManager = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
channel: []const u8,
|
||||
usernamePrefix: []const u8,
|
||||
lock: std.Thread.Mutex = .{},
|
||||
contexts: ContextList = undefined,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, channelName: []const u8, usernamePrefix: []const u8) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.channel = channelName,
|
||||
.usernamePrefix = usernamePrefix,
|
||||
.contexts = ContextList.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.contexts.items) |ctx| {
|
||||
self.allocator.free(ctx.userName);
|
||||
}
|
||||
self.contexts.deinit();
|
||||
}
|
||||
|
||||
pub fn newContext(self: *Self) !*Context {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
var ctx = try self.allocator.create(Context);
|
||||
var userName = try std.fmt.allocPrint(self.allocator, "{s}{d}", .{ self.usernamePrefix, self.contexts.items.len });
|
||||
ctx.* = .{
|
||||
.userName = userName,
|
||||
.channel = self.channel,
|
||||
// used in subscribe()
|
||||
.subscribeArgs = .{
|
||||
.channel = self.channel,
|
||||
.force_text = true,
|
||||
.context = ctx,
|
||||
},
|
||||
// used in upgrade()
|
||||
.settings = .{
|
||||
.on_open = on_open_websocket,
|
||||
.on_close = on_close_websocket,
|
||||
.on_message = handle_websocket_message,
|
||||
.context = ctx,
|
||||
},
|
||||
};
|
||||
try self.contexts.append(ctx);
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Websocket Callbacks
|
||||
//
|
||||
fn on_open_websocket(context: ?*Context, handle: WebSockets.WsHandle) void {
|
||||
if (context) |ctx| {
|
||||
_ = WebsocketHandler.subscribe(handle, &ctx.subscribeArgs) catch |err| {
|
||||
std.log.err("Error opening websocket: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// say hello
|
||||
var buf: [128]u8 = undefined;
|
||||
const message = std.fmt.bufPrint(&buf, "{s} joined the chat.", .{ctx.userName}) catch unreachable;
|
||||
|
||||
// send notification to all others
|
||||
WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
|
||||
std.log.info("new websocket opened: {s}", .{message});
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close_websocket(context: ?*Context, uuid: isize) void {
|
||||
_ = uuid;
|
||||
if (context) |ctx| {
|
||||
// say goodbye
|
||||
var buf: [128]u8 = undefined;
|
||||
const message = std.fmt.bufPrint(&buf, "{s} left the chat.", .{ctx.userName}) catch unreachable;
|
||||
|
||||
// send notification to all others
|
||||
WebsocketHandler.publish(.{ .channel = ctx.channel, .message = message });
|
||||
std.log.info("websocket closed: {s}", .{message});
|
||||
}
|
||||
}
|
||||
fn handle_websocket_message(context: ?*Context, handle: WebSockets.WsHandle, message: []const u8, is_text: bool) void {
|
||||
_ = is_text;
|
||||
_ = handle;
|
||||
if (context) |ctx| {
|
||||
// say goodbye
|
||||
var buf: [128]u8 = undefined;
|
||||
const chat_message = std.fmt.bufPrint(&buf, "{s}: {s}", .{ ctx.userName, message }) catch unreachable;
|
||||
|
||||
// send notification to all others
|
||||
WebsocketHandler.publish(.{ .channel = ctx.channel, .message = chat_message });
|
||||
std.log.info("{s}", .{chat_message});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// HTTP stuff
|
||||
//
|
||||
fn on_request(r: zap.SimpleRequest) void {
|
||||
r.setHeader("Server", "zap.example") catch unreachable;
|
||||
r.sendBody("<html><body><h1>This is a simple Websocket chatroom example</h1></body></html>") catch return;
|
||||
}
|
||||
|
||||
fn on_upgrade(r: zap.SimpleRequest, target_protocol: []const u8) void {
|
||||
// make sure we're talking the right protocol
|
||||
if (!std.mem.eql(u8, target_protocol, "websocket")) {
|
||||
std.log.warn("received illegal target protocol: {s}", .{target_protocol});
|
||||
r.setStatus(.bad_request);
|
||||
r.sendBody("400 - BAD REQUEST") catch unreachable;
|
||||
return;
|
||||
}
|
||||
var context = GlobalContextManager.newContext() catch |err| {
|
||||
std.log.err("Error creating context: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
WebsocketHandler.upgrade(r.h, &context.settings) catch |err| {
|
||||
std.log.err("Error in websocketUpgrade(): {any}", .{err});
|
||||
return;
|
||||
};
|
||||
std.log.info("connection upgrade OK", .{});
|
||||
}
|
||||
|
||||
// global variables, yeah!
|
||||
var GlobalContextManager: ContextManager = undefined;
|
||||
|
||||
const WebsocketHandler = WebSockets.Handler(Context);
|
||||
var handler_instance: WebsocketHandler = .{};
|
||||
|
||||
// here we go
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||
.thread_safe = true,
|
||||
}){};
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
GlobalContextManager = ContextManager.init(allocator, "chatroom", "user-");
|
||||
defer GlobalContextManager.deinit();
|
||||
|
||||
// setup listener
|
||||
var listener = zap.SimpleHttpListener.init(
|
||||
.{
|
||||
.port = 3010,
|
||||
.on_request = on_request,
|
||||
.on_upgrade = on_upgrade,
|
||||
.max_clients = 1000,
|
||||
.max_body_size = 1 * 1024,
|
||||
.public_folder = "examples/websockets/frontend",
|
||||
.log = true,
|
||||
},
|
||||
);
|
||||
try listener.listen();
|
||||
std.log.info("", .{});
|
||||
std.log.info("Connect with browser to http://localhost:3010.", .{});
|
||||
std.log.info("Connect to websocket on ws://localhost:3010.", .{});
|
||||
std.log.info("Terminate with CTRL+C", .{});
|
||||
|
||||
zap.start(.{
|
||||
.threads = 1,
|
||||
.workers = 1,
|
||||
});
|
||||
}
|
39
src/fio.zig
39
src/fio.zig
|
@ -362,6 +362,32 @@ pub const websocket_settings_s = extern struct {
|
|||
on_close: ?*const fn (isize, ?*anyopaque) callconv(.C) void,
|
||||
udata: ?*anyopaque,
|
||||
};
|
||||
|
||||
// struct websocket_subscribe_s_zigcompat {
|
||||
// ws_s *ws;
|
||||
// fio_str_info_s channel;
|
||||
// void (*on_message)(ws_s *ws, fio_str_info_s channel, fio_str_info_s msg, void *udata);
|
||||
// void (*on_unsubscribe)(void *udata);
|
||||
// void *udata;
|
||||
// fio_match_fn match;
|
||||
// unsigned char force_binary;
|
||||
// unsigned char force_text;
|
||||
// };
|
||||
|
||||
pub const websocket_subscribe_s_zigcompat = extern struct {
|
||||
ws: ?*ws_s,
|
||||
channel: fio_str_info_s,
|
||||
on_message: ?*const fn (?*ws_s, fio_str_info_s, fio_str_info_s, ?*anyopaque) callconv(.C) void,
|
||||
on_unsubscribe: ?*const fn (?*anyopaque) callconv(.C) void,
|
||||
udata: ?*anyopaque,
|
||||
match: fio_match_fn,
|
||||
force_binary: u8,
|
||||
force_text: u8,
|
||||
};
|
||||
|
||||
/// 0 on failure
|
||||
pub extern fn websocket_subscribe_zigcompat(websocket_subscribe_s_zigcompat) callconv(.C) usize;
|
||||
|
||||
pub extern fn http_upgrade2ws(http: [*c]http_s, websocket_settings_s) c_int;
|
||||
pub extern fn websocket_connect(url: [*c]const u8, settings: websocket_settings_s) c_int;
|
||||
pub extern fn websocket_attach(uuid: isize, http_settings: [*c]http_settings_s, args: [*c]websocket_settings_s, data: ?*anyopaque, length: usize) void;
|
||||
|
@ -375,6 +401,19 @@ pub const struct_websocket_subscribe_s = opaque {};
|
|||
pub extern fn websocket_subscribe(args: struct_websocket_subscribe_s) usize;
|
||||
pub extern fn websocket_unsubscribe(ws: ?*ws_s, subscription_id: usize) void;
|
||||
pub extern fn websocket_optimize4broadcasts(@"type": isize, enable: c_int) void;
|
||||
|
||||
pub extern fn fio_publish(args: fio_publish_args_s) void;
|
||||
pub const fio_publish_args_s = struct_fio_publish_args_s;
|
||||
pub const struct_fio_publish_args_s = extern struct {
|
||||
engine: ?*anyopaque = null,
|
||||
// we don't support engines other than default
|
||||
// engine: [*c]const fio_pubsub_engine_s,
|
||||
filter: i32 = 0,
|
||||
channel: fio_str_info_s,
|
||||
message: fio_str_info_s,
|
||||
is_json: u8,
|
||||
};
|
||||
|
||||
pub const http_sse_s = struct_http_sse_s;
|
||||
pub const struct_http_sse_s = extern struct {
|
||||
on_open: ?*const fn ([*c]http_sse_s) callconv(.C) void,
|
||||
|
|
207
src/websockets.zig
Normal file
207
src/websockets.zig
Normal file
|
@ -0,0 +1,207 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap.zig");
|
||||
const fio = @import("fio.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const WsHandle = ?*fio.ws_s;
|
||||
pub fn Handler(comptime ContextType: type) type {
|
||||
return struct {
|
||||
/// OnMessage Callback on a websocket
|
||||
pub const WsOnMessageFn = *const fn (
|
||||
/// user-provided context, passed in from websocketHttpUpgrade()
|
||||
context: ?*ContextType,
|
||||
/// websocket handle, used to identify the websocket internally
|
||||
handle: WsHandle,
|
||||
/// the received message
|
||||
message: []const u8,
|
||||
/// indicator if message is text or binary
|
||||
is_text: bool,
|
||||
) void;
|
||||
|
||||
/// Callback when websocket is closed. uuid is a connection identifier,
|
||||
/// it is -1 if a connection could not be established
|
||||
pub const WsOnCloseFn = *const fn (context: ?*ContextType, uuid: isize) void;
|
||||
|
||||
/// A websocket callback function. provides the context passed in at
|
||||
/// websocketHttpUpgrade().
|
||||
pub const WsFn = *const fn (context: ?*ContextType, handle: WsHandle) void;
|
||||
|
||||
pub const WebSocketSettings = struct {
|
||||
/// on_message(context, handle, message, is_text)
|
||||
on_message: ?WsOnMessageFn = null,
|
||||
/// on_open(context)
|
||||
on_open: ?WsFn = null,
|
||||
/// on_ready(context)
|
||||
on_ready: ?WsFn = null,
|
||||
/// on_shutdown(context, uuid)
|
||||
on_shutdown: ?WsFn = null,
|
||||
/// on_close(context)
|
||||
on_close: ?WsOnCloseFn = null,
|
||||
/// passed-in user-defined context
|
||||
context: ?*ContextType = null,
|
||||
};
|
||||
|
||||
/// This function will end the HTTP stage of the connection and attempt to "upgrade" to a WebSockets connection.
|
||||
pub fn upgrade(h: [*c]fio.http_s, settings: *WebSocketSettings) WebSocketError!void {
|
||||
var fio_settings: fio.websocket_settings_s = .{
|
||||
.on_message = internal_on_message,
|
||||
.on_open = internal_on_open,
|
||||
.on_ready = internal_on_ready,
|
||||
.on_shutdown = internal_on_shutdown,
|
||||
.on_close = internal_on_close,
|
||||
.udata = settings,
|
||||
};
|
||||
if (fio.http_upgrade2ws(h, fio_settings) != 0) {
|
||||
return error.UpgradeError;
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_on_message(handle: WsHandle, msg: fio.fio_str_info_s, is_text: u8) callconv(.C) void {
|
||||
var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
|
||||
var message = msg.data[0..msg.len];
|
||||
if (user_provided_settings) |settings| {
|
||||
if (settings.on_message) |on_message| {
|
||||
on_message(settings.context, handle, message, is_text == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_on_open(handle: WsHandle) callconv(.C) void {
|
||||
var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
|
||||
if (user_provided_settings) |settings| {
|
||||
if (settings.on_open) |on_open| {
|
||||
on_open(settings.context, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_on_ready(handle: WsHandle) callconv(.C) void {
|
||||
var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
|
||||
if (user_provided_settings) |settings| {
|
||||
if (settings.on_ready) |on_ready| {
|
||||
on_ready(settings.context, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_on_shutdown(handle: WsHandle) callconv(.C) void {
|
||||
var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), fio.websocket_udata_get(handle)));
|
||||
if (user_provided_settings) |settings| {
|
||||
if (settings.on_shutdown) |on_shutdown| {
|
||||
on_shutdown(settings.context, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_on_close(uuid: isize, udata: ?*anyopaque) callconv(.C) void {
|
||||
var user_provided_settings: ?*WebSocketSettings = @ptrCast(?*WebSocketSettings, @alignCast(@alignOf(?*WebSocketSettings), udata));
|
||||
if (user_provided_settings) |settings| {
|
||||
if (settings.on_close) |on_close| {
|
||||
on_close(settings.context, uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WebSocketError = error{
|
||||
WriteError,
|
||||
UpgradeError,
|
||||
SubscribeError,
|
||||
};
|
||||
|
||||
pub inline fn write(handle: WsHandle, message: []const u8, is_text: bool) WebSocketError!void {
|
||||
if (fio.websocket_write(
|
||||
handle,
|
||||
fio.str2fio(message),
|
||||
if (is_text) 1 else 0,
|
||||
) != 0) {
|
||||
return error.WriteError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn udataToContext(udata: *anyopaque) *ContextType {
|
||||
return @ptrCast(*ContextType, @alignCast(@alignOf(*ContextType), udata));
|
||||
}
|
||||
|
||||
pub inline fn close(handle: WsHandle) void {
|
||||
fio.websocket_close(handle);
|
||||
}
|
||||
|
||||
const PublishArgs = struct {
|
||||
channel: []const u8,
|
||||
message: []const u8,
|
||||
is_json: bool = false,
|
||||
};
|
||||
|
||||
/// publish a message in a channel
|
||||
pub inline fn publish(args: PublishArgs) void {
|
||||
fio.fio_publish(.{
|
||||
.channel = util.str2fio(args.channel),
|
||||
.message = util.str2fio(args.message),
|
||||
.is_json = if (args.is_json) 1 else 0,
|
||||
});
|
||||
}
|
||||
|
||||
pub const SubscriptionOnMessageFn = *const fn (context: ?*ContextType, handle: WsHandle, channel: []const u8, message: []const u8) void;
|
||||
pub const SubscriptionOnUnsubscribeFn = *const fn (context: ?*ContextType) void;
|
||||
|
||||
pub const SubscribeArgs = struct {
|
||||
channel: []const u8,
|
||||
on_message: ?SubscriptionOnMessageFn = null,
|
||||
on_unsubscribe: ?SubscriptionOnUnsubscribeFn = null,
|
||||
/// this is not wrapped nicely yet
|
||||
match: fio.fio_match_fn = null,
|
||||
/// When using direct message forwarding (no on_message callback), this indicates if
|
||||
/// messages should be sent to the client as binary blobs, which is the safest approach.
|
||||
/// By default, facil.io will test for UTF-8 data validity and send the data as text if
|
||||
/// it's a valid UTF-8. Messages above ~32Kb might be assumed to be binary rather than
|
||||
/// tested.
|
||||
force_binary: bool = false,
|
||||
/// When using direct message forwarding (no on_message callback), this indicates if
|
||||
/// messages should be sent to the client as UTF-8 text. By default, facil.io will test
|
||||
/// for UTF-8 data validity and send the data as text if it's a valid UTF-8. Messages
|
||||
/// above ~32Kb might be assumed to be binary rather than tested. force_binary has
|
||||
/// precedence over force_text.
|
||||
force_text: bool = false,
|
||||
context: ?*ContextType = null,
|
||||
};
|
||||
|
||||
/// Returns a subscription ID on success and 0 on failure.
|
||||
/// we copy the pointer so make sure the struct stays valid.
|
||||
/// we need it to look up the ziggified callbacks.
|
||||
pub inline fn subscribe(handle: WsHandle, args: *SubscribeArgs) WebSocketError!usize {
|
||||
if (handle == null) return error.SubscribeError;
|
||||
var fio_args: fio.websocket_subscribe_s_zigcompat = .{
|
||||
.ws = handle.?,
|
||||
.channel = util.str2fio(args.channel),
|
||||
.on_message = if (args.on_message) |_| internal_subscription_on_message else null,
|
||||
.on_unsubscribe = if (args.on_unsubscribe) |_| internal_subscription_on_unsubscribe else null,
|
||||
.match = args.match,
|
||||
.force_binary = if (args.force_binary) 1 else 0,
|
||||
.force_text = if (args.force_text) 1 else 0,
|
||||
.udata = args,
|
||||
};
|
||||
const ret = fio.websocket_subscribe_zigcompat(fio_args);
|
||||
if (ret == 0) {
|
||||
return error.SubscribeError;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn internal_subscription_on_message(handle: WsHandle, channel: fio.fio_str_info_s, message: fio.fio_str_info_s, udata: ?*anyopaque) callconv(.C) void {
|
||||
if (udata) |p| {
|
||||
const args = @ptrCast(*SubscribeArgs, @alignCast(@alignOf(*SubscribeArgs), p));
|
||||
if (args.on_message) |on_message| {
|
||||
on_message(args.context, handle, channel.data[0..channel.len], message.data[0..message.len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn internal_subscription_on_unsubscribe(udata: ?*anyopaque) callconv(.C) void {
|
||||
if (udata) |p| {
|
||||
const args = @ptrCast(*SubscribeArgs, @alignCast(@alignOf(*SubscribeArgs), p));
|
||||
if (args.on_unsubscribe) |on_unsubscribe| {
|
||||
on_unsubscribe(args.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
70
src/zap.zig
70
src/zap.zig
|
@ -10,6 +10,7 @@ pub usingnamespace @import("util.zig");
|
|||
pub usingnamespace @import("http.zig");
|
||||
pub usingnamespace @import("mustache.zig");
|
||||
pub usingnamespace @import("http_auth.zig");
|
||||
pub const WebSockets = @import("websockets.zig");
|
||||
|
||||
pub const Log = @import("log.zig");
|
||||
|
||||
|
@ -496,16 +497,32 @@ pub const CookieArgs = struct {
|
|||
pub const HttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void;
|
||||
pub const SimpleHttpRequestFn = *const fn (SimpleRequest) void;
|
||||
|
||||
/// websocket connection upgrade
|
||||
/// fn(request, targetstring)
|
||||
pub const SimpleHttpUpgradeFn = *const fn (r: SimpleRequest, target_protocol: []const u8) void;
|
||||
|
||||
/// http finish, called when zap finishes. You get your udata back in the
|
||||
/// struct.
|
||||
pub const SimpleHttpFinishSettings = [*c]fio.struct_http_settings_s;
|
||||
pub const SimpleHttpFinishFn = *const fn (SimpleHttpFinishSettings) void;
|
||||
|
||||
pub const SimpleHttpListenerSettings = struct {
|
||||
port: usize,
|
||||
interface: [*c]const u8 = null,
|
||||
on_request: ?SimpleHttpRequestFn,
|
||||
on_response: ?*const fn ([*c]fio.http_s) callconv(.C) void = null,
|
||||
on_response: ?SimpleHttpRequestFn = null,
|
||||
on_upgrade: ?SimpleHttpUpgradeFn = null,
|
||||
on_finish: ?SimpleHttpFinishFn = null,
|
||||
// provide any pointer in there for "user data". it will be passed pack in
|
||||
// on_finish()'s copy of the struct_http_settings_s
|
||||
udata: ?*anyopaque = null,
|
||||
public_folder: ?[]const u8 = null,
|
||||
max_clients: ?isize = null,
|
||||
max_body_size: ?usize = null,
|
||||
timeout: ?u8 = null,
|
||||
log: bool = false,
|
||||
ws_timeout: u8 = 40,
|
||||
ws_max_msg_size: usize = 262144,
|
||||
};
|
||||
|
||||
pub const SimpleHttpListener = struct {
|
||||
|
@ -520,6 +537,9 @@ pub const SimpleHttpListener = struct {
|
|||
};
|
||||
}
|
||||
|
||||
// on_upgrade: ?*const fn ([*c]fio.http_s, [*c]u8, usize) callconv(.C) void = null,
|
||||
// on_finish: ?*const fn ([*c]fio.struct_http_settings_s) callconv(.C) void = null,
|
||||
|
||||
// we could make it dynamic by passing a SimpleHttpListener via udata
|
||||
pub fn theOneAndOnlyRequestCallBack(r: [*c]fio.http_s) callconv(.C) void {
|
||||
if (the_one_and_only_listener) |l| {
|
||||
|
@ -534,6 +554,39 @@ pub const SimpleHttpListener = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn theOneAndOnlyResponseCallBack(r: [*c]fio.http_s) callconv(.C) void {
|
||||
if (the_one_and_only_listener) |l| {
|
||||
var req: SimpleRequest = .{
|
||||
.path = util.fio2str(r.*.path),
|
||||
.query = util.fio2str(r.*.query),
|
||||
.body = util.fio2str(r.*.body),
|
||||
.method = util.fio2str(r.*.method),
|
||||
.h = r,
|
||||
};
|
||||
l.settings.on_response.?(req);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theOneAndOnlyUpgradeCallBack(r: [*c]fio.http_s, target: [*c]u8, target_len: usize) callconv(.C) void {
|
||||
if (the_one_and_only_listener) |l| {
|
||||
var req: SimpleRequest = .{
|
||||
.path = util.fio2str(r.*.path),
|
||||
.query = util.fio2str(r.*.query),
|
||||
.body = util.fio2str(r.*.body),
|
||||
.method = util.fio2str(r.*.method),
|
||||
.h = r,
|
||||
};
|
||||
var zigtarget: []u8 = target[0..target_len];
|
||||
l.settings.on_upgrade.?(req, zigtarget);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theOneAndOnlyFinishCallBack(s: [*c]fio.struct_http_settings_s) callconv(.C) void {
|
||||
if (the_one_and_only_listener) |l| {
|
||||
l.settings.on_finish.?(s);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn listen(self: *Self) !void {
|
||||
var pfolder: [*c]const u8 = null;
|
||||
var pfolder_len: usize = 0;
|
||||
|
@ -546,22 +599,23 @@ pub const SimpleHttpListener = struct {
|
|||
|
||||
var x: fio.http_settings_s = .{
|
||||
.on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null,
|
||||
.on_upgrade = null,
|
||||
.on_response = self.settings.on_response,
|
||||
.on_finish = null,
|
||||
.on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null,
|
||||
.on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null,
|
||||
.on_finish = if (self.settings.on_finish) |_| Self.theOneAndOnlyFinishCallBack else null,
|
||||
.udata = null,
|
||||
.public_folder = pfolder,
|
||||
.public_folder_length = pfolder_len,
|
||||
.max_header_size = 32 * 1024,
|
||||
.max_body_size = self.settings.max_body_size orelse 50 * 1024 * 1024,
|
||||
.max_clients = self.settings.max_clients orelse 100,
|
||||
// fio provides good default:
|
||||
.max_clients = self.settings.max_clients orelse 0,
|
||||
.tls = null,
|
||||
.reserved1 = 0,
|
||||
.reserved2 = 0,
|
||||
.reserved3 = 0,
|
||||
.ws_max_msg_size = 0,
|
||||
.timeout = self.settings.timeout orelse 5,
|
||||
.ws_timeout = 0,
|
||||
.ws_timeout = self.settings.ws_timeout,
|
||||
.log = if (self.settings.log) 1 else 0,
|
||||
.is_client = 0,
|
||||
};
|
||||
|
@ -624,7 +678,7 @@ pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSetti
|
|||
var x: fio.http_settings_s = .{
|
||||
.on_request = settings.on_request,
|
||||
.on_upgrade = settings.on_upgrade,
|
||||
.on_response = settings.on_response orelse null,
|
||||
.on_response = settings.on_response,
|
||||
.on_finish = settings.on_finish,
|
||||
.udata = null,
|
||||
.public_folder = pfolder,
|
||||
|
@ -636,7 +690,7 @@ pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSetti
|
|||
.reserved1 = 0,
|
||||
.reserved2 = 0,
|
||||
.reserved3 = 0,
|
||||
.ws_max_msg_size = 0,
|
||||
.ws_max_msg_size = settings.ws_max_msg_size,
|
||||
.timeout = settings.keepalive_timeout_s,
|
||||
.ws_timeout = 0,
|
||||
.log = if (settings.log) 1 else 0,
|
||||
|
|
Loading…
Add table
Reference in a new issue