实战:Zig 编写高性能 Web 服务(2)

2024-06-06 09:20

本文主要是介绍实战:Zig 编写高性能 Web 服务(2),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.1 编写 HTTP server

我们从python -m http.server 8000启动得到灵感,先确定好目标:

  • 编写一个HTTP/1.1 http server
  • zig version 0.12.0

使用zig init搭建项目的前置工作你先自行搭建好,不会的翻看前面铺垫的章节熟悉zig的项目结构。

关键文件build.zig:

const std = @import("std");// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {// Standard target options allows the person running `zig build` to choose// what target to build for. Here we do not override the defaults, which// means any target is allowed, and the default is native. Other options// for restricting supported target set are available.const target = b.standardTargetOptions(.{});// Standard optimization options allow the person running `zig build` to select// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not// set a preferred release mode, allowing the user to decide how to optimize.const optimize = b.standardOptimizeOption(.{});const exe = b.addExecutable(.{.name = "zig-http-server",.root_source_file = .{ .path = "src/main.zig" },.target = target,.optimize = optimize,// .use_llvm = false,// .use_lld = false,});const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize });exe.root_module.addImport("xev", xev.module("xev"));// This declares intent for the executable to be installed into the// standard location when the user invokes the "install" step (the default// step when running `zig build`).b.installArtifact(exe);// This *creates* a Run step in the build graph, to be executed when another// step is evaluated that depends on it. The next line below will establish// such a dependency.const run_cmd = b.addRunArtifact(exe);// By making the run step depend on the install step, it will be run from the// installation directory rather than directly from within the cache directory.// This is not necessary, however, if the application depends on other installed// files, this ensures they will be present and in the expected location.run_cmd.step.dependOn(b.getInstallStep());// This allows the user to pass arguments to the application in the build// command itself, like this: `zig build run -- arg1 arg2 etc`if (b.args) |args| {run_cmd.addArgs(args);}// This creates a build step. It will be visible in the `zig build --help` menu,// and can be selected like this: `zig build run`// This will evaluate the `run` step rather than the default, which is "install".const run_step = b.step("run", "Run the app");run_step.dependOn(&run_cmd.step);
}

build.zig.zon文件:

.{.name = "zig-http-server",.version = "0.0.0",.dependencies = .{.libxev = .{.url = "https://codeload.github.com/mitchellh/libxev/tar.gz/b3f9918776b8700b337b7ebe769060328fe246b0",.hash = "122044caf67c7833c7110dc93531031899e459a6818ed125a0bcfdb0b5243bd7700b",},},.paths = .{"",},
}

马上开始我们的编程之旅,打开src/main.zig文件:

const std = @import("std");
const xev = @import("xev");const net = std.net;
const Allocator = std.mem.Allocator;const CompletionPool = std.heap.MemoryPoolExtra(xev.Completion, .{});
const ClientPool = std.heap.MemoryPoolExtra(Client, .{});

const xev = @import("xev");

我们采用了第三方的类库:mitchellh/libxev,libxev 是一个跨平台、高性能的事件循环库,提供了非阻塞 IO、定时器、事件等的抽象,并且能够在 Linux(io_uring 或 epoll)、macOS(kqueue)以及 Wasm + WASI 上运行。它既可以作为 Zig 语言的 API,也可以作为 C 语言的 API 使用。

我们需要建一个Client的结构体,代表一个网络客户端。这个客户端结构体包含了几个字段,如id(客户端ID)、socket(网络套接字)、loop(事件循环)、arena(内存分配器)、client_pool(客户端内存池)和completion_pool(完成操作内存池)。此外,它还有一个用于读取数据的缓冲区read_buf。

Client结构体还定义以下方法:

  • work:这个方法启动客户端的工作流程,它创建了一个Completion对象,并使用socket.read方法开始读取数据。
  • readCallback:这个回调函数在数据读取完成后被调用。它处理读取的结果,如果成功,则打印出读取的数据,并准备一个HTTP响应。然后,它使用socket.write方法发送响应。
  • writeCallback:这个回调函数在响应发送完成后被调用。它关闭套接字连接。
  • shutdownCallback:这个回调函数在套接字关闭后被调用。它调用socket.close方法来关闭套接字。
  • closeCallback:这个回调函数在套接字关闭完成后被调用。它清理资源,包括销毁Completion对象和Client对象本身。
  • destroy:这个方法用于手动销毁客户端资源。

这个Client结构体实现了一个简单的HTTP服务器,它能够读取客户端的请求,并返回一个包含"Hello, World!"消息的HTTP响应。每个客户端都有自己的内存分配器和内存池,用于管理内存分配和释放。当客户端连接关闭时,所有分配的内存都会被清理。代码片段如下:

const Client = struct {id: u32,socket: xev.TCP,loop: *xev.Loop,arena: std.heap.ArenaAllocator,client_pool: *ClientPool,completion_pool: *CompletionPool,read_buf: [4096]u8 = undefined,const Self = @This();pub fn work(self: *Self) void {const c_read = self.completion_pool.create() catch unreachable;self.socket.read(self.loop, c_read, .{ .slice = &self.read_buf }, Client, self, Client.readCallback);}pub fn readCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,buf: xev.ReadBuffer,r: xev.TCP.ReadError!usize,) xev.CallbackAction {const self = self_.?;const n = r catch |err| {std.log.err("read error {any}", .{err});s.shutdown(l, c, Client, self, shutdownCallback);return .disarm;};const data = buf.slice[0..n];std.log.info("{s}", .{data});const httpOk =\\HTTP/1.1 200 OK\\Content-Type: text/plain\\Server: xev-http\\Content-Length: {d}\\Connection: close\\\\{s};const content_str =\\Hello, World! {d};const content = std.fmt.allocPrint(self.arena.allocator(), content_str, .{self.id}) catch unreachable;const res = std.fmt.allocPrint(self.arena.allocator(), httpOk, .{ content.len, content }) catch unreachable;self.socket.write(self.loop, c, .{ .slice = res }, Client, self, writeCallback);return .disarm;}fn writeCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,buf: xev.WriteBuffer,r: xev.TCP.WriteError!usize,) xev.CallbackAction {_ = buf; // autofix_ = r catch unreachable;const self = self_.?;s.shutdown(l, c, Client, self, shutdownCallback);return .disarm;}fn shutdownCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,r: xev.TCP.ShutdownError!void,) xev.CallbackAction {_ = r catch {};const self = self_.?;s.close(l, c, Client, self, closeCallback);return .disarm;}fn closeCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,socket: xev.TCP,r: xev.TCP.CloseError!void,) xev.CallbackAction {_ = l;_ = r catch unreachable;_ = socket;var self = self_.?;self.arena.deinit();self.completion_pool.destroy(c);self.client_pool.destroy(self);return .disarm;}pub fn destroy(self: *Self) void {self.arena.deinit();self.client_pool.destroy(self);}
};

 我们还需要定义名为Server的结构体,它代表了一个网络服务器。这个服务器结构体包含了几个字段,如loop(事件循环)、gpa(全局内存分配器)、completion_pool(完成操作内存池)、client_pool(客户端内存池)和conns(当前连接数)。

Server结构体定义了一个方法:

  • acceptCallback:这个回调函数在服务器接受到新的客户端连接时被调用。它创建了一个新的Client对象,并初始化它的各个字段,包括客户端ID、事件循环、网络套接字、内存分配器和内存池。然后,它调用client.work()方法来开始处理客户端请求。

这个回调函数的目的是接受新的客户端连接,并为每个连接创建一个Client实例来处理它。在创建了新的客户端实例后,它会增加conns计数器来记录当前的连接数。最后,它返回xev.CallbackAction.rearm,这意味着服务器会继续监听新的连接。

这个Server结构体实现了一个简单的网络服务器,它能够接受客户端连接,并为每个连接创建一个Client对象来处理通信。服务器使用内存池来管理客户端实例的内存分配,以提高性能和减少内存碎片。代码片段如下:

const Server = struct {loop: *xev.Loop,gpa: Allocator,completion_pool: *CompletionPool,client_pool: *ClientPool,conns: u32 = 0,fn acceptCallback(self_: ?*Server,l: *xev.Loop,// we ignore the completion, to keep the accept loop going for new connections_: *xev.Completion,r: xev.TCP.AcceptError!xev.TCP,) xev.CallbackAction {const self = self_.?;var client = self.client_pool.create() catch unreachable;client.* = Client{.id = self.conns,.loop = l,.socket = r catch unreachable,.arena = std.heap.ArenaAllocator.init(self.gpa),.client_pool = self.client_pool,.completion_pool = self.completion_pool,};client.work();self.conns += 1;return .rearm;}
};

1.2 进入main实现

main整体流程是:

  1. 初始化线程池和事件循环。
  2. 创建一个TCP套接字,并绑定到指定的地址和端口。
  3. 开始监听传入的连接。
  4. 初始化完成池和客户端池。
  5. 创建一个服务器结构体,包含所有必要的组件。
  6. 注册一个接受连接的回调函数。
  7. 运行事件循环,等待连接和处理事件。

http服务处理的就是多线程,所以我们借助了第三方类库xev来管理线程池。

var thread_pool = xev.ThreadPool.init(.{});defer thread_pool.deinit();defer thread_pool.shutdown();
  •  我们创建了一个xev.ThreadPool类型的变量thread_pool,并初始化它。xev是一个库,ThreadPool是这个库中的一个类型,用于管理线程池。
  • defer关键字用于注册一个函数,这个函数会在当前作用域结束时被调用。这里我们注册了thread_pool.deinit()和thread_pool.shutdown(),确保线程池在程序结束时被正确关闭和清理。
const port = 3000;
const addr = try net.Address.parseIp4("0.0.0.0", port);
var socket = try xev.TCP.init(addr);std.log.info("Listening on port {}", .{port});try socket.bind(addr);
try socket.listen(std.os.linux.SOMAXCONN);

绑定socket接口的方式,可以看出zig的简洁之道,和C一样。

socket.accept(&loop, c, Server, &server, Server.acceptCallback);

这行代码调用socket.accept方法,用于接受传入的连接。这个方法需要几个参数:

  • &loop:事件循环的引用,用于注册接受连接的事件。
  • c:完成事件的引用,用于在连接被接受时通知服务器。
  • Server:服务器结构体的类型,用于类型检查。
  • &server:服务器结构体的引用,用于在接受连接时传递给回调函数。
  • Server.acceptCallback:服务器结构体中的一个函数,当连接被接受时会被调用。
try loop.run(.until_done);

这行代码启动事件循环,并开始处理事件。run方法会阻塞当前线程,直到事件循环被关闭或者发生错误。.until_done是一个枚举值,表示事件循环应该运行直到所有任务都完成。

然后在项目直接运行zig build run就可以启动。main.zig完整代码如下:

const std = @import("std");
const xev = @import("xev");const net = std.net;
const Allocator = std.mem.Allocator;const CompletionPool = std.heap.MemoryPoolExtra(xev.Completion, .{});
const ClientPool = std.heap.MemoryPoolExtra(Client, .{});pub fn main() !void {var thread_pool = xev.ThreadPool.init(.{});defer thread_pool.deinit();defer thread_pool.shutdown();var loop = try xev.Loop.init(.{.entries = 4096,.thread_pool = &thread_pool,});defer loop.deinit();var gpa = std.heap.GeneralPurposeAllocator(.{}){};defer _ = gpa.deinit();const alloc = gpa.allocator();const port = 3000;const addr = try net.Address.parseIp4("0.0.0.0", port);var socket = try xev.TCP.init(addr);std.log.info("Listening on port {}", .{port});try socket.bind(addr);try socket.listen(std.os.linux.SOMAXCONN);var completion_pool = CompletionPool.init(alloc);defer completion_pool.deinit();var client_pool = ClientPool.init(alloc);defer client_pool.deinit();const c = try completion_pool.create();var server = Server{.loop = &loop,.gpa = alloc,.completion_pool = &completion_pool,.client_pool = &client_pool,};socket.accept(&loop, c, Server, &server, Server.acceptCallback);try loop.run(.until_done);
}const Client = struct {id: u32,socket: xev.TCP,loop: *xev.Loop,arena: std.heap.ArenaAllocator,client_pool: *ClientPool,completion_pool: *CompletionPool,read_buf: [4096]u8 = undefined,const Self = @This();pub fn work(self: *Self) void {const c_read = self.completion_pool.create() catch unreachable;self.socket.read(self.loop, c_read, .{ .slice = &self.read_buf }, Client, self, Client.readCallback);}pub fn readCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,buf: xev.ReadBuffer,r: xev.TCP.ReadError!usize,) xev.CallbackAction {const self = self_.?;const n = r catch |err| {std.log.err("read error {any}", .{err});s.shutdown(l, c, Client, self, shutdownCallback);return .disarm;};const data = buf.slice[0..n];std.log.info("{s}", .{data});const httpOk =\\HTTP/1.1 200 OK\\Content-Type: text/plain\\Server: xev-http\\Content-Length: {d}\\Connection: close\\\\{s};const content_str =\\Hello, World! {d};const content = std.fmt.allocPrint(self.arena.allocator(), content_str, .{self.id}) catch unreachable;const res = std.fmt.allocPrint(self.arena.allocator(), httpOk, .{ content.len, content }) catch unreachable;self.socket.write(self.loop, c, .{ .slice = res }, Client, self, writeCallback);return .disarm;}fn writeCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,buf: xev.WriteBuffer,r: xev.TCP.WriteError!usize,) xev.CallbackAction {_ = buf; // autofix_ = r catch unreachable;const self = self_.?;s.shutdown(l, c, Client, self, shutdownCallback);return .disarm;}fn shutdownCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,s: xev.TCP,r: xev.TCP.ShutdownError!void,) xev.CallbackAction {_ = r catch {};const self = self_.?;s.close(l, c, Client, self, closeCallback);return .disarm;}fn closeCallback(self_: ?*Client,l: *xev.Loop,c: *xev.Completion,socket: xev.TCP,r: xev.TCP.CloseError!void,) xev.CallbackAction {_ = l;_ = r catch unreachable;_ = socket;var self = self_.?;self.arena.deinit();self.completion_pool.destroy(c);self.client_pool.destroy(self);return .disarm;}pub fn destroy(self: *Self) void {self.arena.deinit();self.client_pool.destroy(self);}
};const Server = struct {loop: *xev.Loop,gpa: Allocator,completion_pool: *CompletionPool,client_pool: *ClientPool,conns: u32 = 0,fn acceptCallback(self_: ?*Server,l: *xev.Loop,// we ignore the completion, to keep the accept loop going for new connections_: *xev.Completion,r: xev.TCP.AcceptError!xev.TCP,) xev.CallbackAction {const self = self_.?;var client = self.client_pool.create() catch unreachable;client.* = Client{.id = self.conns,.loop = l,.socket = r catch unreachable,.arena = std.heap.ArenaAllocator.init(self.gpa),.client_pool = self.client_pool,.completion_pool = self.completion_pool,};client.work();self.conns += 1;return .rearm;}
};

1.3 运行效果:

1.3 学习总结

理解上面代码的关键是理解Zig中的基本概念,如defer、try、结构体、方法和事件循环的工作原理。同时,理解xev库的使用也很重要,因为本次http-server项目大量依赖于这个库的多线程管理能力。

这篇关于实战:Zig 编写高性能 Web 服务(2)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1035703

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

Java Web指的是什么

Java Web指的是使用Java技术进行Web开发的一种方式。Java在Web开发领域有着广泛的应用,主要通过Java EE(Enterprise Edition)平台来实现。  主要特点和技术包括: 1. Servlets和JSP:     Servlets 是Java编写的服务器端程序,用于处理客户端请求和生成动态网页内容。     JSP(JavaServer Pages)

BUUCTF靶场[web][极客大挑战 2019]Http、[HCTF 2018]admin

目录   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 [web][HCTF 2018]admin 考点:弱密码字典爆破 四种方法:   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 访问环境 老规矩,我们先查看源代码

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏