I’m playing around a toy TCP server. From the very simple example which only handle one connection at a time, I’ve added a Client structure and a handler function which is launched with async, so multiple clients can connect.
Fields in Client are very simple:
const Client = struct {
id: usize,
stream: std.Io.net.Stream,
};
The problem is, every time I need to read or write from the net.Stream I need to reinit the Reader/Writer with a buffer, and take a reference to the interface. For instance, warning the client that the connection will close is done through a deinit() function like this:
fn deinit(self: Client, io: std.Io) void {
var writer = self.stream.writer(io, &.{});
const w = &writer.interface;
w.print("Server stopped\n", .{}) catch unreachable;
self.stream.close(io);
}
I’ve tried to add fields like this (with the respective init function), but I get Sigfault on read or write operations.
const Client = struct {
id: usize,
stream: std.Io.net.Stream,
stream_reader: std.Io.net.Stream.Reader,
stream_writer: std.Io.net.Stream.Writer,
reader: *std.Io.Reader,
writer: *std.Io.Reader,
};
Here is my current code that works well, but I’m not happy about how it is designed. Reader/Writer are not part of the struct, so I can’t really pass it around (to a game loop/app for instance)
const std = @import("std");
const log = std.log;
const Client = struct {
id: usize,
stream: std.Io.net.Stream,
fn init(id: usize, stream: std.Io.net.Stream) Client {
return .{
.id = id,
.stream = stream,
};
}
fn deinit(self: Client, io: std.Io) void {
std.log.debug("Deinit client {}", .{self.id});
var writer = self.stream.writer(io, &.{});
const w = &writer.interface;
w.print("Server stopped\n", .{}) catch unreachable;
self.stream.close(io);
}
};
fn handler(io: std.Io, client: Client) void {
var buf: [1024]u8 = undefined;
var reader = client.stream.reader(io, &buf);
const r = &reader.interface;
var writer = client.stream.writer(io, &.{});
const w = &writer.interface;
while (true) {
const text = r.takeDelimiterInclusive('\n') catch {
std.log.debug("Client {} disconnected", .{client.id});
break;
};
std.debug.print("Received from {}: {s}", .{ client.id, text });
w.printAscii(text, .{}) catch unreachable;
}
client.deinit(io);
}
pub fn main(init: std.process.Init) !void {
const io = init.io;
var tasks: std.Io.Group = .init;
const address: std.Io.net.IpAddress = try .parse("127.0.0.1", 7777);
var server = try address.listen(io, .{});
std.log.debug("Server listening:", .{});
var i: usize = 1;
while (true) : (i += 1) {
const stream = try server.accept(io);
std.log.debug("Client connected: {}", .{stream.socket.address});
std.log.debug("Id: {}", .{i});
const client: Client = .init(i, stream);
tasks.async(io, handler, .{ io, client });
}
tasks.cancel(io);
}
How should I proceed to handle many concurrent connections and have a complete easy-to-use Client structure ?
On a side note, working with std.Io is amazing ! Breaking the main server loop calls cancel, which gracefully breaks the client loops, and eventually calls client.deinit(). It’s much simpler than synchronizing raw threads. Impressive work on Io !