Skip to content

Day 41: std.net模块:网络基础

欢迎来到第四十一天!今天我们正式踏入网络编程的世界。网络是现代软件的命脉,几乎所有应用程序都需要通过网络进行通信。Zig的 std.net 模块提供了一套跨平台的底层网络API,它基于套接字(Sockets),这是操作系统提供的网络通信的基本接口。std.net 封装了不同平台(Windows, Linux, macOS等)的套接字API,为我们提供了一致的、Zig风格的接口。今天,我们将学习两种最核心的互联网协议:TCP和UDP,并看看如何在Zig中创建客户端和服务器。

TCP是一种面向连接的、可靠的协议。它保证数据能够按顺序、无错误地从一方传输到另一方。它非常适合那些要求高可靠性的应用,如网页浏览、文件传输和远程登录。

创建TCP客户端:

std.net.tcpConnectToHost 是连接到一个TCP服务器的最简单方式。

const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
// 连接到 example.com 的 80 端口 (HTTP)
const stream = try std.net.tcpConnectToHost(allocator, "example.com", 80);
defer stream.close();
// stream 对象同时实现了 Reader 和 Writer 接口
try stream.writer().print("GET / HTTP/1.0\r\n\r\n", .{});
const response = try stream.reader().readAllAlloc(allocator, 8192);
defer allocator.free(response);
std.debug.print("{s}", .{response});
}

tcpConnectToHost 返回一个 std.net.Stream 对象,它封装了一个TCP连接。这个 Stream 对象就像一个文件句柄,你可以通过它的 .reader() 和 .writer() 方法来进行读写,这与我们之前学习的 std.io 和 std.fs 的模式完全一致。

UDP是一种无连接的、不可靠的协议。它只是尽力将数据包(datagrams)从一方发送到另一方,但不保证它们会到达,也不保证它们的顺序。它非常适合那些能容忍少量丢包但要求低延迟的应用,如在线游戏、视频会议和DNS查询。

const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const remote_addr = try std.net.resolveIp(allocator, "8.8.8.8", 53);
var socket = try std.net.UdpSocket.connect(remote_addr);
defer socket.close();
// 发送DNS查询
try socket.write("some dns query bytes");
var buffer: [512]u8 = undefined;
const len = try socket.read(&buffer);
// ... 处理响应 ...
}

要创建一个服务器,你需要:

  1. 绑定(bind)到一个本地的IP地址和端口。
  2. 监听(listen)这个地址和端口,等待客户端连接。
  3. 接受(accept)进入的连接。

std.net.tcpListen 是一个便捷函数,它完成了绑定和监听的步骤。

const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
var listener = try std.net.tcpListen(allocator, "127.0.0.1", 8080);
defer listener.deinit();
std.log.info("Listening on 127.0.0.1:8080", .{});
while (true) {
// 接受一个新的连接
const conn = try listener.accept();
// 在真实应用中,你应该为每个连接创建一个新线程或异步任务来处理
// 这里为了简单,我们只处理一个连接然后退出
handleConnection(conn);
}
}
fn handleConnection(conn: std.net.Stream) void {
defer conn.close();
// ... 与客户端交互 ...
}

listener.accept() 会阻塞程序,直到有一个新的客户端连接进来,然后返回一个代表该连接的 Stream 对象。

5. 示例:一个简单的TCP回声服务器 (Echo Server)

Section titled “5. 示例:一个简单的TCP回声服务器 (Echo Server)”

回声服务器是一个简单的服务器,它会将从客户端收到的任何数据原样地发送回去。

// 在上面的 handleConnection 函数中实现
fn handleConnection(conn: std.net.Stream) !void {
defer conn.close();
std.log.info("Accepted connection from {s}", .{conn.remote_address});
var buffer: [1024]u8 = undefined;
while (true) {
const len = try conn.read(&buffer);
if (len == 0) break; // 客户端关闭了连接
// 将收到的数据写回
try conn.writeAll(buffer[0..len]);
}
}

6. 实践练习:编写一个TCP客户端来测试回声服务器

Section titled “6. 实践练习:编写一个TCP客户端来测试回声服务器”

编写一个单独的Zig程序作为客户端。

  1. 使用 std.net.tcpConnectToHost 连接到你刚刚创建的回声服务器(127.0.0.1:8080)。
  2. 从标准输入读取一行文本。
  3. 将这行文本发送到服务器。
  4. 读取服务器的回应,并将其打印到标准输出。

问:tcpConnectToHost 和 tcpListen 中的主机名是如何解析成IP地址的?

答:它们在内部使用了DNS解析。std.net.resolveIp 和 std.net.resolveIpList 函数可以让你手动进行DNS查询,将一个域名解析成一个或多个IP地址。

问:我应该如何选择端口号?

答:端口号0到1023是“周知端口”(well-known ports),通常被标准服务(如HTTP(80), SSH(22))占用,并且在类Unix系统上需要root权限才能绑定。你应该选择1024到65535之间的“动态”或“私有”端口用于你自己的应用。

今天,我们迈出了网络编程的第一步。我们学习了 std.net 模块,了解了TCP和UDP协议的基本特性,并掌握了如何使用 tcpConnectToHost 创建TCP客户端和 tcpListen 创建TCP服务器。我们还看到,std.net.Stream 与 std.io 的 Reader/Writer 接口的无缝集成,使得网络I/O操作与文件I/O操作一样简单直观。网络编程是一个广阔的领域,但今天所学的基础知识,是构建任何网络应用的起点。

明天,我们将学习一个更高级的网络主题:std.http模块,看看如何在Zig中处理HTTP协议。