-
Notifications
You must be signed in to change notification settings - Fork 1
Description
作者:Andrew Kelley
摆脱了 libc 的束缚,DNS 解析就是一个快乐的异步乌托邦。 我们可以发起多个查询,并使用标准系统原语(例如文件描述符和 poll)来处理收到的响应。
然而,令人不安的是,有一对令人扫兴的家伙计划破坏这种完美:
- libc 的 DNS 解析 API 是 getaddrinfo,它很糟糕。
- 操作系统将配置的名称服务器等重要细节隐藏在 libc 后面,因此您必须使用它。
例如,如果您为了获得好的 API 而绕过 glibc 系统上的 libc,那么最终会有人抱怨您的应用程序与 NSS 集成不正确。
但是 getaddrinfo 太烂了! 它会阻塞直到所有 DNS 查询返回,所以你能做的最好的就是在一个 utility 线程中运行它,然后等待它完成。 即使这样也存在隐患......它会读取 environ,因此如果进程中任何地方的代码调用了 setenv,那么您就会发生损坏的内存访问。 仅凭这一点就让我不再认真对待 POSIX。 如果他们想重新获得我的尊重,他们必须修复这个问题,并使 close 变得不可失败。 传言说他们让 close 变得可能失败,是因为他们认为那是倒带磁带驱动器的合适位置。 🤦
无论如何,值得庆幸的是,某些系统具有支持异步和取消的非标准替代方案:
- glibc 有 getaddrinfo_a / gai_cancel
- Windows 有 GetAddrInfoExW / GetAddrInfoExCancel
- OpenBSD 有 getaddrinfo_async / asr_abort
- macOS 有 CFHostStartInfoResolution / CFHostCancelInfoResolution
- FreeBSD 有 dnsres_getaddrinfo,但没有取消支持
就是这样。 如果您的操作系统不在这个列表中,那么您的 DNS 解析 API 就是垃圾! 修复它!
我还没有利用任何这些 API,但我确实实现了没有讨厌的 libc 的情况。 我想在这里分享这段代码,因为它是一个非常巧妙的新 std.Io 接口的实际示例:
pub const ConnectError = LookupError || IpAddress.ConnectError;
pub fn connect(
host_name: HostName,
io: Io,
port: u16,
options: IpAddress.ConnectOptions,
) ConnectError!Stream {
var connect_many_buffer: [32]ConnectManyResult = undefined;
var connect_many_queue: Io.Queue(ConnectManyResult) = .init(&connect_many_buffer);
var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options });
var saw_end = false;
defer {
connect_many.cancel(io);
if (!saw_end) while (true) switch (connect_many_queue.getOneUncancelable(io)) {
.connection => |loser| if (loser) |s| s.closeConst(io) else |_| continue,
.end => break,
};
}
var aggregate_error: ConnectError = error.UnknownHostName;
while (connect_many_queue.getOne(io)) |result| switch (result) {
.connection => |connection| if (connection) |stream| return stream else |err| switch (err) {
error.SystemResources,
error.OptionUnsupported,
error.ProcessFdQuotaExceeded,
error.SystemFdQuotaExceeded,
error.Canceled,
=> |e| return e,
error.WouldBlock => return error.Unexpected,
else => |e| aggregate_error = e,
},
.end => |end| {
saw_end = true;
try end;
return aggregate_error;
},
} else |err| switch (err) {
error.Canceled => |e| return e,
}
}
pub const ConnectManyResult = union(enum) {
connection: IpAddress.ConnectError!Stream,
end: ConnectError!void,
};
/// Asynchronously establishes a connection to all IP addresses associated with
/// a host name, adding them to a results queue upon completion.
pub fn connectMany(
host_name: HostName,
io: Io,
port: u16,
results: *Io.Queue(ConnectManyResult),
options: IpAddress.ConnectOptions,
) void {
var canonical_name_buffer: [max_len]u8 = undefined;
var lookup_buffer: [32]HostName.LookupResult = undefined;
var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer);
host_name.lookup(io, &lookup_queue, .{
.port = port,
.canonical_name_buffer = &canonical_name_buffer,
});
var group: Io.Group = .init;
while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) {
.address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }),
.canonical_name => continue,
.end => |lookup_result| {
group.waitUncancelable(io);
results.putOneUncancelable(io, .{ .end = lookup_result });
return;
},
} else |err| switch (err) {
error.Canceled => |e| {
group.cancel(io);
results.putOneUncancelable(io, .{ .end = e });
},
}
}这段代码具有以下特性:
- 它异步地将 DNS 查询发送到每个配置的名称服务器。
- 当每个响应到达时,它立即异步地尝试 TCP 连接到返回的 IP 地址。
- 在第一次成功的 TCP 连接之后,所有其他正在进行的连接尝试(包括 DNS 查询)都会被取消。
- 无论 Io 是通过线程实现还是通过事件循环实现,此代码都以最佳方式运行。
- 即使操作是顺序发生的,该代码在使用单线程阻塞 Io 时也能工作。
- 没有堆分配。
我很高兴的是,最重要的是,它读起来像是标准的、惯用的 Zig 代码。 我们仍然使用所有常规的控制流:try、defer、while 等。
代码是扁平的,没有太多嵌套。 就像非异步代码一样,我们几乎可以在任何地方插入 try foo,并且可以放心,相同的资源管理模式已经处理了错误处理。
所以希望这能让您了解这些更改带来的好处:
std: Introduce Io Interface (std:引入 Io 接口)
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组