Last active
June 4, 2023 19:50
-
-
Save lmllrjr/5120f5ac0070dc74c9519c39fc534e2e to your computer and use it in GitHub Desktop.
[Zig⚡️] basic http server with multiplexer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// zig version 0.10.1 | |
const std = @import("std"); | |
const net = std.net; | |
const mem = std.mem; | |
const log = std.log; | |
const stdout = std.io.getStdOut().writer(); | |
const BUFSIZE = 8192; | |
const HTTPHEAD = | |
"HTTP/1.1 {s}\r\n" ++ | |
"Connection: close\r\n" ++ | |
"Content-Type: {s}\r\n" ++ | |
"Content-Length: {}\r\n\r\n" ++ | |
"{s}"; | |
const ServeFileError = error{ | |
RecvHeaderEOF, | |
RecvHeaderExceededBuffer, | |
HeaderDidNotMatch, | |
}; | |
pub fn main() anyerror!void { | |
const self_addr = try net.Address.resolveIp("127.0.0.1", 9000); | |
var listener = net.StreamServer.init(.{}); | |
try (&listener).listen(self_addr); | |
// define routes | |
const routes = comptime [_]route{ | |
route{ .method = "GET", .path = "/t", .handler = teapotHandler }, | |
route{ .method = "GET", .path = "/hello", .handler = helloWorldHandler }, | |
}; | |
log.info("starting server, listen on {}", .{self_addr}); | |
// start listen and serve | |
while ((&listener).accept()) |conn| { | |
log.info("{{\"conn\":\"{}\"}}", .{conn.address}); | |
router(&conn.stream, routes) catch |err| { | |
if (@errorReturnTrace()) |bt| { | |
log.err("{{\"err\":\"{}\",\"stacktrace\":\"{}\"}}", .{ err, bt }); | |
} else { | |
log.err("{{\"err\":\"{}\"}}", .{err}); | |
} | |
}; | |
conn.stream.close(); | |
} else |err| { | |
log.info("{{\"err\":\"{}\"}}", .{err}); | |
return err; | |
} | |
} | |
const route = struct { | |
handler: *const fn (stream: *const net.Stream) void, | |
method: []const u8, | |
path: []const u8, | |
}; | |
fn helloWorldHandler(stream: *const net.Stream) void { | |
const hello = "hello world"; | |
stream.writer().print(HTTPHEAD, .{ "200 OK", "text/html", hello.len, hello }) catch unreachable; | |
} | |
fn teapotHandler(stream: *const net.Stream) void { | |
const teapod = "418 i'm a teapot"; | |
stream.writer().print(HTTPHEAD, .{ "418 OK", "text/html", teapod.len, teapod }) catch unreachable; | |
} | |
fn notFoundHandler(stream: *const net.Stream) void { | |
stream.writer().print(HTTPHEAD, .{ "404", "text/html", 13, "404 not found" }) catch unreachable; | |
} | |
/// router is a very basic multiplexer | |
fn router(stream: *const net.Stream, comptime routes: [2]route) !void { | |
var recv_buf: [BUFSIZE]u8 = undefined; | |
var recv_total: usize = 0; | |
while (stream.read(recv_buf[recv_total..])) |recv_len| { | |
if (recv_len == 0) | |
return ServeFileError.RecvHeaderEOF; | |
recv_total += recv_len; | |
if (mem.containsAtLeast(u8, recv_buf[0..recv_total], 1, "\r\n\r\n")) | |
break; | |
if (recv_total >= recv_buf.len) | |
return ServeFileError.RecvHeaderExceededBuffer; | |
} else |read_err| { | |
return read_err; | |
} | |
const recv_slice = recv_buf[0..recv_total]; | |
var tok_itr = mem.tokenize(u8, recv_slice, " "); | |
const request_method = tok_itr.next() orelse ""; | |
const path = tok_itr.next() orelse ""; | |
if (path[0] != '/') | |
return ServeFileError.HeaderDidNotMatch; | |
// loop through routes and check if path matches | |
for (routes) |rt| { | |
if (!mem.eql(u8, request_method, rt.method)) | |
return ServeFileError.HeaderDidNotMatch; | |
if (mem.eql(u8, path, rt.path)) { | |
rt.handler(stream); | |
log.info("{{\"method\":\"{s}\",\"path\":\"{s}\"}}", .{ rt.method, rt.path }); | |
} | |
} | |
notFoundHandler(stream); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment