Skip to content

【Zig 日报】异步 DNS 解析 #263

@jiacai2050

Description

@jiacai2050

作者: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 接口)

https://ziglang.org/devlog/2025/#2025-10-15

加入我们

Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:

  1. 供稿,分享自己使用 Zig 的心得
  2. 改进 ZigCC 组织下的开源项目
  3. 加入微信群Telegram 群组

Metadata

Metadata

Assignees

No one assigned

    Labels

    日报daily report

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions