383 lines
14 KiB
Zig
383 lines
14 KiB
Zig
const std = @import("std");
|
|
const Server = @This();
|
|
|
|
const Address = @import("Address.zig").Address;
|
|
const Connection = @import("Connection.zig");
|
|
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
|
|
const http = @import("http.zig");
|
|
const openssl = @import("openssl.zig");
|
|
const Request = @import("Request.zig");
|
|
const RequestHandler = @import("RequestHandler.zig");
|
|
const Worker = @import("Worker.zig");
|
|
|
|
const log = std.log.scoped(.Server);
|
|
const linux = std.os.linux;
|
|
const errno = linux.errno;
|
|
|
|
fd: FileDescriptor,
|
|
address: Address,
|
|
ssl_ctx: ?*openssl.SslContext,
|
|
workers: []Worker,
|
|
threads: []std.Thread,
|
|
request_handler: RequestHandler,
|
|
read_timeout_us: u64,
|
|
|
|
connection_queue: std.DoublyLinkedList,
|
|
// NOTE Connection pool has no need for being doubly-linked, but the queue has
|
|
// (as it's FIFO) and we want a single intrusive Node to be able to participate
|
|
// in both lists. This is possible because a connection will never belong to
|
|
// both lists at the same time.
|
|
connection_pool: std.DoublyLinkedList,
|
|
connection_buffer: []Connection,
|
|
|
|
io: std.Io,
|
|
mutex: std.Io.Mutex,
|
|
cond_connection_queued: std.Io.Condition,
|
|
cond_connection_freed: std.Io.Condition,
|
|
|
|
/// 4 kiB
|
|
const page_size = 4 * 1024;
|
|
/// 2 MiB
|
|
const huge_page_size = 2 * 1024 * 1024;
|
|
|
|
pub const Options = struct {
|
|
request_handler: RequestHandler,
|
|
address: Address = .initIp4(.{ 127, 0, 0, 1 }, 8000),
|
|
/// If not `null`, the server will use TLS with the provided OpenSSL
|
|
/// context.
|
|
ssl_ctx: ?*openssl.SslContext = null,
|
|
max_connections: u32 = 128,
|
|
/// The number of worker threads. If set to `0`, the number of worker
|
|
/// threads will be equal to the number of logical CPU cores.
|
|
worker_count: u32 = 0,
|
|
/// The maximum number of header fields the `Request` object will be able to
|
|
/// store. An HTTP request will be rejected if it has more header fields
|
|
/// than the capacity.
|
|
max_header_fields: u32 = 256,
|
|
/// The number of 2 MiB pages reserved for a single read buffer. Each worker
|
|
/// has its own read buffer. An HTTP request (headers and content combined)
|
|
/// will be rejected if it is larger than the read buffer.
|
|
read_buffer_huge_pages: u32 = 1,
|
|
/// The number of 4 kiB pages reserved for a single header write buffer.
|
|
/// Each worker has its own header write buffer. The HTTP status line, all
|
|
/// header fields and the CRLF terminator must all fit in the header write
|
|
/// buffer.
|
|
header_write_buffer_pages: u32 = 1,
|
|
/// The number of 2 MiB pages reserved for a single body write buffer. Each
|
|
/// worker has its own body write buffer. The HTTP response body must fit
|
|
/// entirely within the body write buffer. This restriction only applies to
|
|
/// bodies generated with the body writer and not to bodies sent with
|
|
/// `sendfile`.
|
|
body_write_buffer_huge_pages: u32 = 1,
|
|
/// How much time should a worker wait on an idle connection before closing
|
|
/// it. Specifically, how much time can a `read` syscall block for, before
|
|
/// the connection is forcefully closed.
|
|
read_timeout_us: u64 = 1 * std.time.us_per_s,
|
|
};
|
|
|
|
pub fn init(allocator: std.mem.Allocator, io: std.Io, options: Options) !Server {
|
|
const worker_count = if (options.worker_count > 0) options.worker_count else try std.Thread.getCpuCount();
|
|
|
|
// Create socket fd
|
|
|
|
const fd: FileDescriptor = try .socket(
|
|
options.address.any.family,
|
|
linux.SOCK.STREAM | linux.SOCK.CLOEXEC,
|
|
if (options.address.any.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP,
|
|
);
|
|
errdefer fd.close();
|
|
|
|
const opt = std.mem.toBytes(@as(c_int, 1));
|
|
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEADDR, &opt);
|
|
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEPORT, &opt);
|
|
|
|
var socklen = options.address.getOsSockLen();
|
|
try fd.bind(&options.address.any, socklen);
|
|
try fd.listen(options.max_connections);
|
|
|
|
var listen_address = options.address;
|
|
try fd.getsockname(&listen_address.any, &socklen);
|
|
|
|
// Allocate arrays
|
|
|
|
const workers = try allocator.alloc(Worker, worker_count);
|
|
errdefer allocator.free(workers);
|
|
|
|
const connection_buffer = try allocator.alloc(Connection, options.max_connections);
|
|
errdefer allocator.free(connection_buffer);
|
|
|
|
const threads = try allocator.alloc(std.Thread, worker_count);
|
|
errdefer allocator.free(threads);
|
|
|
|
// Allocate and remap read buffers
|
|
|
|
const single_read_buffer_size = @as(usize, options.read_buffer_huge_pages) * huge_page_size;
|
|
const all_read_buffers_size = worker_count * single_read_buffer_size;
|
|
|
|
const double_single_read_buffer_size = 2 * single_read_buffer_size;
|
|
const double_all_read_buffers_size = 2 * all_read_buffers_size;
|
|
|
|
const read_buffer_fd: FileDescriptor = try .memfd_create("read_buffer", 0);
|
|
defer read_buffer_fd.close();
|
|
|
|
try read_buffer_fd.ftruncate(@intCast(all_read_buffers_size));
|
|
|
|
const read_buffer_ptr = try errOrPtr(linux.mmap(
|
|
null,
|
|
double_all_read_buffers_size,
|
|
.{},
|
|
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1,
|
|
0,
|
|
));
|
|
errdefer _ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size);
|
|
_ = linux.madvise(read_buffer_ptr, double_all_read_buffers_size, linux.MADV.HUGEPAGE);
|
|
|
|
for (0..worker_count) |i| {
|
|
const offset = i * single_read_buffer_size;
|
|
const double_offset = i * double_single_read_buffer_size;
|
|
|
|
try err(linux.mmap(
|
|
read_buffer_ptr + double_offset,
|
|
single_read_buffer_size,
|
|
.{ .READ = true, .WRITE = true },
|
|
.{ .TYPE = .SHARED, .FIXED = true },
|
|
@intFromEnum(read_buffer_fd),
|
|
@intCast(offset),
|
|
));
|
|
|
|
try err(linux.mmap(
|
|
read_buffer_ptr + double_offset + single_read_buffer_size,
|
|
single_read_buffer_size,
|
|
.{ .READ = true, .WRITE = true },
|
|
.{ .TYPE = .SHARED, .FIXED = true },
|
|
@intFromEnum(read_buffer_fd),
|
|
@intCast(offset),
|
|
));
|
|
}
|
|
|
|
// Allocate header write buffer
|
|
|
|
const single_header_write_buffer_size = @as(usize, options.header_write_buffer_pages) * page_size;
|
|
const all_header_write_buffers_size = worker_count * single_header_write_buffer_size;
|
|
|
|
const header_write_buffer_ptr = try errOrPtr(linux.mmap(
|
|
null,
|
|
all_header_write_buffers_size,
|
|
.{ .READ = true, .WRITE = true },
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1,
|
|
0,
|
|
));
|
|
errdefer _ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size);
|
|
|
|
// Allocate body write buffer
|
|
|
|
const single_body_write_buffer_size = @as(usize, options.body_write_buffer_huge_pages) * huge_page_size;
|
|
const all_body_write_buffers_size = worker_count * single_body_write_buffer_size;
|
|
|
|
const body_write_buffer_ptr = try errOrPtr(linux.mmap(
|
|
null,
|
|
all_body_write_buffers_size,
|
|
.{ .READ = true, .WRITE = true },
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1,
|
|
0,
|
|
));
|
|
errdefer _ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size);
|
|
_ = linux.madvise(body_write_buffer_ptr, all_body_write_buffers_size, linux.MADV.HUGEPAGE);
|
|
|
|
// Initialize workers
|
|
|
|
var workers_initialized: usize = 0;
|
|
errdefer {
|
|
for (workers[0..workers_initialized]) |*worker| {
|
|
worker.deinit(allocator);
|
|
}
|
|
}
|
|
|
|
for (workers, 0..) |*worker, i| {
|
|
const read_offset = i * double_single_read_buffer_size;
|
|
const header_write_offset = i * single_header_write_buffer_size;
|
|
const body_write_offset = i * single_body_write_buffer_size;
|
|
worker.* = try Worker.init(allocator, .{
|
|
.worker_id = i,
|
|
|
|
.max_header_fields = options.max_header_fields,
|
|
|
|
.read_buffer_ptr = read_buffer_ptr + read_offset,
|
|
.read_buffer_size = single_read_buffer_size,
|
|
|
|
.header_write_buffer = (header_write_buffer_ptr + header_write_offset)[0..single_header_write_buffer_size],
|
|
.body_write_buffer = (body_write_buffer_ptr + body_write_offset)[0..single_body_write_buffer_size],
|
|
});
|
|
workers_initialized += 1;
|
|
}
|
|
|
|
// Fill connection pool
|
|
|
|
var connection_pool: std.DoublyLinkedList = .{};
|
|
for (connection_buffer) |*c| {
|
|
connection_pool.prepend(&c.node);
|
|
}
|
|
|
|
return .{
|
|
.fd = fd,
|
|
.address = listen_address,
|
|
.ssl_ctx = options.ssl_ctx,
|
|
.workers = workers,
|
|
.threads = threads,
|
|
.request_handler = options.request_handler,
|
|
.read_timeout_us = options.read_timeout_us,
|
|
|
|
.connection_queue = .{},
|
|
.connection_pool = connection_pool,
|
|
.connection_buffer = connection_buffer,
|
|
|
|
.io = io,
|
|
.mutex = .init,
|
|
.cond_connection_queued = .init,
|
|
.cond_connection_freed = .init,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
|
|
log.debug("Deinitializing Server.", .{});
|
|
const worker_count = self.workers.len;
|
|
|
|
const single_read_buffers_size = self.workers[0].read_buffer_size;
|
|
const all_read_buffers_size = worker_count * single_read_buffers_size;
|
|
const double_all_read_buffers_size = 2 * all_read_buffers_size;
|
|
|
|
const single_header_write_buffer_size = self.workers[0].header_write_buffer.len;
|
|
const all_header_write_buffers_size = worker_count * single_header_write_buffer_size;
|
|
|
|
const single_body_write_buffer_size = self.workers[0].body_write_buffer.len;
|
|
const all_body_write_buffers_size = worker_count * single_body_write_buffer_size;
|
|
|
|
const read_buffer_ptr = self.workers[0].read_buffer_ptr;
|
|
const header_write_buffer_ptr = self.workers[0].header_write_buffer.ptr;
|
|
const body_write_buffer_ptr = self.workers[0].body_write_buffer.ptr;
|
|
|
|
for (self.workers) |*worker| {
|
|
worker.deinit(allocator);
|
|
}
|
|
|
|
_ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size);
|
|
_ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size);
|
|
_ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size);
|
|
|
|
allocator.free(self.threads);
|
|
allocator.free(self.connection_buffer);
|
|
allocator.free(self.workers);
|
|
|
|
self.fd.close();
|
|
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// This method block until the server is stopped, which is achieved by storing
|
|
/// `false` into `running`. You should use another thread or interruption
|
|
/// handler to be able to stop the server.
|
|
pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
|
|
var worker_running: std.atomic.Value(bool) = .init(running.load(.acquire));
|
|
|
|
var spawned: usize = 0;
|
|
defer {
|
|
log.debug("Storing `false` into worker_running.", .{});
|
|
worker_running.store(false, .release);
|
|
log.debug("Broadcasting connection queued condition variable.", .{});
|
|
self.cond_connection_queued.broadcast(self.io);
|
|
for (self.threads[0..spawned], 0..) |*thread, i| {
|
|
log.debug("Joining the thread of worker #{d}.", .{i});
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
for (self.workers, 0..) |*worker, i| {
|
|
log.debug("Spawning thread for worker #{d}.", .{i});
|
|
self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
|
|
spawned += 1;
|
|
}
|
|
|
|
while (running.load(.acquire)) {
|
|
var address: Address = undefined;
|
|
var address_size: u32 = @sizeOf(Address);
|
|
|
|
log.debug("Accepting connection.", .{});
|
|
const fd = self.fd.accept(&address.any, &address_size) catch |e| {
|
|
log.err("Error while accepting connection: {}", .{e});
|
|
continue;
|
|
};
|
|
log.debug("Accepted connection from {f}", .{address});
|
|
|
|
const timeout: linux.timeval = .{
|
|
.sec = @intCast(self.read_timeout_us / std.time.us_per_s),
|
|
.usec = @intCast(self.read_timeout_us % std.time.us_per_s),
|
|
};
|
|
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.RCVTIMEO, std.mem.asBytes(&timeout));
|
|
|
|
const ssl: ?*openssl.Ssl = self.maybeInitSsl(fd) catch |e| {
|
|
log.err("Error while estabilishing SSL connection: {}", .{e});
|
|
fd.close();
|
|
continue;
|
|
};
|
|
|
|
{
|
|
log.debug("Acquiring mutex.", .{});
|
|
self.mutex.lockUncancelable(self.io);
|
|
log.debug("Acquired mutex.", .{});
|
|
defer {
|
|
log.debug("Unlocking mutex.", .{});
|
|
self.mutex.unlock(self.io);
|
|
}
|
|
|
|
while (true) {
|
|
if (self.connection_pool.pop()) |node| {
|
|
const connection: *Connection = @fieldParentPtr("node", node);
|
|
connection.reinit(address, fd, ssl);
|
|
log.debug("Adding connection to {f} to the connection queue.", .{connection.address});
|
|
self.connection_queue.prepend(node);
|
|
break;
|
|
}
|
|
|
|
log.debug("Waiting on connection freed condition variable.", .{});
|
|
self.cond_connection_freed.waitUncancelable(self.io, &self.mutex);
|
|
log.debug("Woken up on connection freed condition variable.", .{});
|
|
}
|
|
}
|
|
|
|
log.debug("Signaling connection queued condition variable.", .{});
|
|
self.cond_connection_queued.signal(self.io);
|
|
} else {
|
|
log.debug("Loaded `false` from running, the accept loop exited.", .{});
|
|
}
|
|
}
|
|
|
|
fn maybeInitSsl(self: *const Server, fd: FileDescriptor) !?*openssl.Ssl {
|
|
if (self.ssl_ctx) |ssl_ctx| {
|
|
const ssl = try openssl.Ssl.new(ssl_ctx);
|
|
try ssl.setFd(fd);
|
|
log.debug("Accepting SSL layer.", .{});
|
|
try ssl.accept();
|
|
log.debug("Accepted SSL layer.", .{});
|
|
const opt = std.mem.toBytes(@as(c_int, 1));
|
|
if (fd.setsockopt(linux.SOL.TLS, 3, &opt)) { // TLS_TX_ZEROCOPY_RO
|
|
log.debug("Enabled zero-copy on sendfile.", .{});
|
|
} else |_| {}
|
|
return ssl;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
fn err(rc: usize) !void {
|
|
const e = errno(rc);
|
|
return if (e != .SUCCESS) error.SystemError else {};
|
|
}
|
|
|
|
fn errOrPtr(rc: usize) ![*]u8 {
|
|
const e = errno(rc);
|
|
return if (e != .SUCCESS) error.SystemError else @ptrFromInt(rc);
|
|
}
|