Your First Server
A server does three things:
- Start a listener (WelcomeRequest)
- Accept connections (receive HelloRequest, send HelloResponse)
- Exchange messages with clients
Step 1: Create the Engine
Before anything, create the tofu engine and get interfaces.
const std = @import("std");
const tofu = @import("tofu");
const Reactor = tofu.Reactor;
const Ampe = tofu.Ampe;
const ChannelGroup = tofu.ChannelGroup;
const Message = tofu.Message;
const Address = tofu.address.Address;
const address = tofu.address;
pub fn main() !void {
var alc: std.heap.DebugAllocator(.{}) = .init;
defer _ = alc.deinit();
const allocator = alc.allocator();
// Create the engine
var rtr: *Reactor = try Reactor.create(allocator, tofu.DefaultOptions);
defer rtr.destroy();
// Get the ampe interface (message pool + channel factory)
const ampe: Ampe = try rtr.ampe();
// Create a channel group (handles multiple channels)
const chnls: ChannelGroup = try ampe.create();
defer tofu.DestroyChannels(ampe, chnls);
// Now ready to use tofu
}
Step 2: Start the Listener
Send a WelcomeRequest to start listening.
// Get a message from pool
var welcomeReq: ?*Message = try ampe.get(.always);
defer ampe.put(&welcomeReq);
// Set up the listen address
var addr: Address = .{
.tcp_server_addr = address.TCPServerAddress.init("0.0.0.0", 7099)
};
try addr.format(welcomeReq.?);
// Submit - tofu creates the listener socket
const bhdr = try chnls.post(&welcomeReq);
const listener_ch = bhdr.channel_number;
// Wait for confirmation
var welcomeResp: ?*Message = try chnls.waitReceive(tofu.waitReceive_INFINITE_TIMEOUT);
defer ampe.put(&welcomeResp);
// Check status
if (welcomeResp.?.bhdr.status != 0) {
// Failed to start listener
return error.ListenerFailed;
}
// Listener is ready on listener_ch
Save the listener channel
You need listener_ch to identify messages related to the listener.
New client connections arrive on different channels.
Step 3: Accept Connections
Wait for HelloRequest from clients, send HelloResponse to accept.
while (true) {
var msg: ?*Message = try chnls.waitReceive(tofu.waitReceive_INFINITE_TIMEOUT);
defer ampe.put(&msg);
// Check origin first
if (msg.?.isFromEngine()) {
// Status notification from tofu
continue;
}
switch (msg.?.bhdr.proto.opCode) {
.HelloRequest => {
// New client connected
const client_ch = msg.?.bhdr.channel_number;
// Accept by sending HelloResponse
msg.?.bhdr.proto.opCode = .HelloResponse;
_ = try chnls.post(&msg);
// Now client_ch is ready for data exchange
},
.Request => {
// Client sent a request - handle it
const ch = msg.?.bhdr.channel_number;
// ... process request ...
},
.ByeRequest => {
// Client wants to close
msg.?.bhdr.proto.opCode = .ByeResponse;
_ = try chnls.post(&msg);
},
else => {},
}
}
NAQ: How do I reject a connection?
Send ByeSignal instead of HelloResponse:
This closes the channel immediately.Step 4: Handle Requests
Process incoming requests and send responses.
.Request => {
const client_ch = msg.?.bhdr.channel_number;
const job_id = msg.?.bhdr.message_id;
// Read request body
const request_data = msg.?.body.body().?;
// Process (your logic here)
const result = processRequest(request_data);
// Reuse message for response
msg.?.bhdr.proto.opCode = .Response;
// channel_number and message_id stay the same
msg.?.body.clear();
try msg.?.body.append(result);
_ = try chnls.post(&msg);
},
Reuse messages
You can reuse the received message for the response. Just change the opCode and body. Channel and message_id stay the same.
Complete Example
const std = @import("std");
const tofu = @import("tofu");
const Reactor = tofu.Reactor;
const Ampe = tofu.Ampe;
const ChannelGroup = tofu.ChannelGroup;
const Message = tofu.Message;
const Address = tofu.address.Address;
const address = tofu.address;
pub fn runServer(gpa: std.mem.Allocator, port: u16) !void {
// Create engine
var rtr: *Reactor = try Reactor.create(gpa, tofu.DefaultOptions);
defer rtr.destroy();
const ampe: Ampe = try rtr.ampe();
const chnls: ChannelGroup = try ampe.create();
defer tofu.DestroyChannels(ampe, chnls);
// Start listener
var welcomeReq: ?*Message = try ampe.get(.always);
defer ampe.put(&welcomeReq);
var addr: Address = .{
.tcp_server_addr = address.TCPServerAddress.init("0.0.0.0", port)
};
try addr.format(welcomeReq.?);
_ = try chnls.post(&welcomeReq);
var welcomeResp: ?*Message = try chnls.waitReceive(tofu.waitReceive_INFINITE_TIMEOUT);
defer ampe.put(&welcomeResp);
if (welcomeResp.?.bhdr.status != 0) {
return error.ListenerFailed;
}
std.debug.print("Server listening on port {d}\n", .{port});
// Main loop
while (true) {
var msg: ?*Message = try chnls.waitReceive(tofu.waitReceive_INFINITE_TIMEOUT);
defer ampe.put(&msg);
if (msg.?.isFromEngine()) {
continue;
}
switch (msg.?.bhdr.proto.opCode) {
.HelloRequest => {
// Accept connection
msg.?.bhdr.proto.opCode = .HelloResponse;
_ = try chnls.post(&msg);
},
.Request => {
// Echo back
msg.?.bhdr.proto.opCode = .Response;
_ = try chnls.post(&msg);
},
.ByeRequest => {
msg.?.bhdr.proto.opCode = .ByeResponse;
_ = try chnls.post(&msg);
},
else => {},
}
}
}
Key Points
| Step | Message | What happens |
|---|---|---|
| Start listener | WelcomeRequest → WelcomeResponse | tofu creates socket, binds, listens |
| Accept client | HelloRequest → HelloResponse | New channel for this client |
| Handle request | Request → Response | Your application logic |
| Close | ByeRequest → ByeResponse | Graceful channel close |
Next
See Your First Client for the client side.