网络编程¶
传统的网络编程¶
在 Linux 下进行 C 语言网络编程时,主要依赖于 POSIX 标准定义的系统调用和库函数。这些 API 提供了对网络通信的底层支持,包括创建套接字(Socket)、绑定地址、监听连接、接受连接、发送和接收数据等操作。
下图是一个典型的网络编程流程图:
server client
+----------+ +----------+
| socket | | socket |
+----------+ +----------+
| |
| |
| |
v |
+----------+ |
| bind | |
+----------+ |
| |
| |
| |
v |
+----------+ |
| listen | |
+----------+ v
| +----------+
| <------------ | connect |
| +----------+
v |
+----------+ |
| accept | |
+----------+ |
| |
| |
| |
v v
+----------+ +----------+
| send/recv| <------ | send/recv|
+----------+ +----------+
创建套接字¶
使用 socket()
系统调用来创建一个套接字。
参数说明:
- domain:指定协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)或 AF_UNIX(本地通信)。
- type:指定套接字类型,如 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)或 SOCK_RAW(原始套接字)。
- protocol:指定协议,通常设置为 0,表示使用默认协议。
返回值:
- 成功时返回一个套接字描述符(非负整数)。
- 失败时返回 -1,并设置 errno。
绑定地址¶
使用 bind()
系统调用来将套接字绑定到一个本地地址和端口。
参数说明:
- sockfd:套接字描述符。
- addr:指向 struct sockaddr 的指针,包含要绑定的地址和端口信息。
- addrlen:地址结构的长度。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
监听连接¶
使用 listen()
系统调用来将套接字转换为被动套接字,使其能够接收连接请求。
参数说明:
- sockfd:套接字描述符。
- backlog:指定未完成连接队列的最大长度。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
接受连接¶
使用 accept()
系统调用来接受一个连接请求,返回一个新的套接字描述符用于与客户端通信。
参数说明:
- sockfd:监听套接字描述符。
- addr:指向 struct sockaddr 的指针,用于存储客户端地址信息。
- addrlen:地址结构的长度。
返回值:
- 成功时返回一个新的套接字描述符。
- 失败时返回 -1,并设置 errno。
发送和接收数据¶
使用 send()
和 recv()
系统调用来发送和接收数据。
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
参数说明:
- sockfd:套接字描述符。
- buf:指向数据缓冲区的指针。
- len:缓冲区的长度。
- flags:控制选项,如 0(默认)、MSG_OOB(带外数据)等。
返回值:
- send():成功时返回发送的字节数,失败时返回 -1。
- recv():成功时返回接收的字节数,失败时返回 -1,到达文件末尾时返回 0。
关闭套接字¶
使用 close()
系统调用来关闭套接字。
参数说明:
- sockfd:套接字描述符。
返回值:
- 成功时返回 0。
- 失败时返回 -1,并设置 errno。
示例代码:TCP 服务器¶
以下是一个简单的 TCP 服务器示例,展示了如何使用上述 API。
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "config.h"
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(TCP_PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) ==
-1) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 10) == -1) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", TCP_PORT);
// 接受连接
client_fd =
accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected.\n");
// 接收数据
ssize_t nread = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (nread == -1) {
perror("recv");
} else {
buffer[nread] = '\0';
printf("Received: %s\n", buffer);
// 发送数据
send(client_fd, buffer, strlen(buffer), 0);
}
// 关闭套接字
close(client_fd);
close(server_fd);
return 0;
}
示例代码:TCP 客户端¶
以下是一个简单的 TCP 客户端示例,展示了如何使用上述 API。
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "config.h"
int main() {
int client_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建套接字
client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(TCP_PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接到服务器
if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) ==
-1) {
perror("connect");
close(client_fd);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 发送数据
const char* message = "Hello, server!";
send(client_fd, message, strlen(message), 0);
// 接收数据
ssize_t nread = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
if (nread == -1) {
perror("recv");
} else {
buffer[nread] = '\0';
printf("Received: %s\n", buffer);
}
// 关闭套接字
close(client_fd);
return 0;
}
示例代码:UDP 服务器¶
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "config.h"
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 初始化服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(UDP_PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字到地址
if (bind(sockfd, (const struct sockaddr*)&server_addr, sizeof(server_addr)) <
0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP server is listening on port %d...\n", UDP_PORT);
while (1) {
// 接收数据
ssize_t nread = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_addr, &client_addr_len);
if (nread < 0) {
perror("recvfrom failed");
continue;
}
buffer[nread] = '\0'; // 确保字符串结尾
printf("Received message: %s\n", buffer);
// 回显数据给客户端
if (sendto(sockfd, buffer, nread, 0, (const struct sockaddr*)&client_addr,
client_addr_len) < 0) {
perror("sendto failed");
}
}
// 关闭套接字
close(sockfd);
return 0;
}
示例代码:UDP 客户端¶
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "config.h"
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 初始化服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(UDP_PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 发送数据到服务器
const char* message = "Hello, server!";
if (sendto(sockfd, message, strlen(message), 0,
(const struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Message sent: %s\n", message);
// 接收服务器的回显数据
socklen_t server_addr_len = sizeof(server_addr);
ssize_t nread = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&server_addr, &server_addr_len);
if (nread < 0) {
perror("recvfrom failed");
close(sockfd);
exit(EXIT_FAILURE);
}
buffer[nread] = '\0'; // 确保字符串结尾
printf("Received echo: %s\n", buffer);
// 关闭套接字
close(sockfd);
return 0;
}
libuv 的网络编程¶
libuv 的网络编程 API 并不与 POSIX 网络 API 一一对应,而是对其进行了封装和抽象,提供了更高层次的异步 I/O 接口。libuv 的设计目标是跨平台和事件驱动,因此它的 API 更注重异步操作和事件循环,而不是直接映射 POSIX 的同步 API。
TCP 相关 API¶
uv_tcp_init¶
uv_tcp_init
用于初始化 TCP 句柄,相当于 POSIX 的 socket
函数。
参数说明:
- loop: 事件循环对象(uv_loop_t)。
- handle: TCP 句柄(uv_tcp_t)。
uv_tcp_bind¶
将 TCP 句柄绑定到指定的地址和端口,相当于 POSIX 的 bind
函数。
参数说明:
- handle: TCP 句柄(uv_tcp_t)。
- addr: 地址信息(struct sockaddr)。
- flags: 绑定标志(如 UV_TCP_IPV6ONLY)。
uv_listen¶
开始监听连接请求,相当于 POSIX 的 listen
函数。
参数说明:
- stream: 流句柄(uv_stream_t,可以是 TCP 或 Pipe)。
- backlog: 连接队列的最大长度。
- cb: 连接回调函数(uv_connection_cb)。
uv_accept¶
接受连接请求。
参数说明:
- server: 服务器流句柄(uv_stream_t)。
- client: 客户端流句柄(uv_stream_t)。
uv_tcp_connect¶
发起 TCP 连接。
int uv_tcp_connect(uv_connect_t* req, uv_tcp_t* handle, const struct sockaddr* addr, uv_connect_cb cb);
参数说明:
- req: 连接请求句柄(uv_connect_t)。
- handle: TCP 句柄(uv_tcp_t)。
- addr: 目标地址(struct sockaddr)。
- cb: 连接完成回调函数(uv_connect_cb)。
UDP 相关 API¶
uv_udp_init¶
初始化 UDP 句柄。
参数说明:
- loop: 事件循环对象(uv_loop_t)。
- handle: UDP 句柄(uv_udp_t)。
uv_udp_bind¶
将 UDP 句柄绑定到指定的地址和端口。
参数说明:
- handle: UDP 句柄(uv_udp_t)。
- addr: 地址信息(struct sockaddr)。
- flags: 绑定标志(如 UV_UDP_REUSEADDR)。
uv_udp_recv_start¶
开始接收 UDP 数据。
参数说明:
- handle: UDP 句柄(uv_udp_t)。
- alloc_cb: 分配缓冲区的回调函数(uv_alloc_cb)。
- recv_cb: 接收数据的回调函数(uv_udp_recv_cb)。
uv_udp_recv_stop¶
停止接收 UDP 数据。
参数说明:
- handle: UDP 句柄(uv_udp_t)。
uv_udp_send¶
发送 UDP 数据。
int uv_udp_send(uv_udp_send_t* req, uv_udp_t* handle, const uv_buf_t* bufs, unsigned int nbufs, const struct sockaddr* addr, uv_udp_send_cb cb);
参数说明:
- req: 发送请求句柄(uv_udp_send_t)。
- handle: UDP 句柄(uv_udp_t)。
- bufs: 数据缓冲区数组(uv_buf_t)。
- nbufs: 缓冲区数量。
- addr: 目标地址(struct sockaddr)。
- cb: 发送完成回调函数(uv_udp_send_cb)。
通用 API¶
uv_read_start¶
开始读取数据。此 API 在 文件系统 中也有介绍。
参数说明:
- stream: 流句柄(uv_stream_t)。
- alloc_cb: 分配缓冲区的回调函数(uv_alloc_cb)。
- read_cb: 读取数据的回调函数(uv_read_cb)。
uv_write¶
写入数据。此 API 在 文件系统 中也有介绍。
int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t* bufs, unsigned int nbufs, uv_write_cb cb);
参数说明:
- req: 写请求句柄(uv_write_t)。
- handle: 流句柄(uv_stream_t)。
- bufs: 数据缓冲区数组(uv_buf_t)。
- nbufs: 缓冲区数量。
- cb: 写入完成回调函数(uv_write_cb)。
uv_close¶
关闭句柄。
参数说明:
- handle: 需要关闭的句柄(uv_handle_t)。
- close_cb: 关闭完成回调函数(uv_close_cb)。
uv_ip4_addr¶
将字符串形式的 IPv4 地址和端口转换为 struct sockaddr_in
。
参数说明:
- ip: 字符串形式的 IPv4 地址(如 "127.0.0.1")。
- port: 端口号(主机字节序)。
- addr: 输出的 struct sockaddr_in 结构。
uv_getaddrinfo¶
异步解析域名或主机名。
int uv_getaddrinfo(uv_loop_t* loop, uv_getaddrinfo_t* req, uv_getaddrinfo_cb cb, const char* node, const char* service, const struct addrinfo* hints);
参数说明:
- loop: 事件循环对象(uv_loop_t)。
- req: 请求句柄(uv_getaddrinfo_t)。
- cb: 解析完成后的回调函数(uv_getaddrinfo_cb)。
- node: 要解析的主机名或 IP 地址(如 "example.com" 或 "127.0.0.1")。
- service: 服务名或端口号(如 "http" 或 "80")。
- hints: 提示信息(struct addrinfo),用于指定解析的地址类型(如 AF_INET 或 AF_INET6)。
返回值:
- 成功返回 0,失败返回错误码。
uv_freeaddrinfo¶
释放 uv_getaddrinfo
返回的地址信息。
参数说明:
- ai: 由
uv_getaddrinfo
返回的地址信息(struct addrinfo*
)。
回调函数类型¶
uv_connection_cb¶
连接回调函数。
参数说明:
- server: 服务器流句柄(uv_stream_t)。
- status: 连接状态(0 表示成功,负数表示错误)。
uv_connect_cb¶
连接完成回调函数。
参数说明:
- req: 连接请求句柄(uv_connect_t)。
- status: 连接状态(0 表示成功,负数表示错误)。
uv_read_cb¶
读取数据回调函数。
参数说明:
- stream: 流句柄(uv_stream_t)。
- nread: 读取的字节数(>0 表示成功,0 表示 空回调,<0 表示错误)。
- buf: 数据缓冲区(uv_buf_t)。
nread
可能的值:
- nread > 0 :读取到 nread 字节的数据,数据存储在 buf->base 中。
- nread == 0 :libuv 可能给了一个空的读取回调,这种情况一般可以安全地忽略。
- nread < 0 :发生错误或者流关闭:
- UV_EOF:流结束(客户端或服务器关闭了连接)。
- 其他负值:读取错误(如 UV_ECONNRESET 表示连接被重置)。
当 nread
等于 0 时,表示 libuv 成功调用了回调,但是 没有读取到任何数据。可能的原因:
-
流量控制:
在某些情况下,libuv 可能会调用
on_read
但不返回数据(例如,缓冲区准备好但暂时没有数据)。 -
分配的缓冲区为空:
alloc_buffer 提供的缓冲区可能未正确初始化,导致
uv_read_start
仍然触发回调,但数据长度为 0。 -
非阻塞 I/O 机制:
libuv 是基于事件驱动的非阻塞 I/O,某些情况下
on_read
可能会被调用但没有数据到达,因此 nread == 0。
使用示例:
// 设置read回调
uv_read_start((uv_stream_t *)client, alloc_buffer, echo_read);
// 回调处理函数,直接回略掉nread == 0的情况
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread > 0) {
write_req_t *req = (write_req_t *)malloc(sizeof(write_req_t));
printf("Received: %.*s\n", (int)nread, buf->base);
req->buf = uv_buf_init(buf->base, nread);
uv_write((uv_write_t *)req, client, &req->buf, 1, NULL);
return;
}
if (nread < 0) {
if (nread != UV_EOF) fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t *)client, NULL);
}
if (buf->base) free(buf->base);
}
uv_write_cb¶
写入完成回调函数。
参数说明:
- req: 写请求句柄(uv_write_t)。
- status: 写入状态(0 表示成功,负数表示错误)。
uv_getaddrinfo_cb¶
域名解析完成后的回调函数。
参数说明:
- req: 请求句柄(uv_getaddrinfo_t)。
- status: 解析状态(0 表示成功,负数表示错误)。
- res: 解析结果(struct addrinfo*),包含地址信息。
POSIX socket API 与 libuv network API 对应关系总结¶
功能 | POSIX API | libuv API | 区别 |
---|---|---|---|
创建套接字 | socket() |
uv_tcp_init() / uv_udp_init() |
libuv 封装了套接字创建,直接操作句柄。 |
绑定地址 | bind() |
uv_tcp_bind() / uv_udp_bind() |
libuv 的绑定操作是异步的。 |
监听连接 | listen() |
uv_listen() |
libuv 的监听操作是异步的。 |
接受连接 | accept() |
uv_accept() |
libuv 的接受操作是异步的。 |
发起连接 | connect() |
uv_tcp_connect() |
libuv 的连接操作是异步的。 |
读取数据 | read() / recv() |
uv_read_start() |
libuv 的读取操作是异步的,数据通过回调传递。 |
写入数据 | write() / send() |
uv_write() |
libuv 的写入操作是异步的。 |
关闭套接字 | close() |
uv_close() |
libuv 的关闭操作是异步的。 |
事件循环 | select() / poll() |
uv_run() |
libuv 提供了统一的事件循环机制。 |
DNS 解析 | getaddrinfo() |
uv_getaddrinfo() |
libuv 的 DNS 解析是异步的。 |
缓冲区管理 | char[] / malloc() |
uv_buf_t / uv_buf_init() |
libuv 提供了统一的缓冲区管理接口。 |
示例代码:TCP 服务器¶
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
#define DEFAULT_PORT 7000
#define DEFAULT_BACKLOG 128
struct sockaddr_in addr;
uv_loop_t *loop;
typedef struct {
uv_write_t req;
uv_buf_t buf;
} write_req_t;
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = (char *)malloc(suggested_size);
buf->len = suggested_size;
}
void on_close(uv_handle_t *handle) { free(handle); }
void echo_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread > 0) {
write_req_t *req = (write_req_t *)malloc(sizeof(write_req_t));
printf("Received: %.*s\n", (int)nread, buf->base);
req->buf = uv_buf_init(buf->base, nread);
uv_write((uv_write_t *)req, client, &req->buf, 1, NULL);
return;
}
if (nread < 0) {
if (nread != UV_EOF) fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t *)client, NULL);
}
if (buf->base) free(buf->base);
}
void on_new_connection(uv_stream_t *server, int status) {
if (status < 0) {
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
}
uv_tcp_t *client = malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, client);
if (uv_accept(server, (uv_stream_t *)client) == 0) {
uv_read_start((uv_stream_t *)client, alloc_buffer, echo_read);
} else {
uv_close((uv_handle_t *)client, on_close);
}
}
int main(int argc, char *argv[]) {
loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
uv_tcp_bind(&server, (const struct sockaddr *)&addr, 0);
int r = uv_listen((uv_stream_t *)&server, DEFAULT_BACKLOG, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
示例代码:TCP 客户端¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
#define PORT 7000
#define MESSAGE "Hello, libuv!"
uv_loop_t *loop;
uv_tcp_t client;
uv_connect_t connect_req;
uv_write_t write_req;
void on_write(uv_write_t *req, int status) {
if (status) {
fprintf(stderr, "Write error %s\n", uv_strerror(status));
}
free(req->data);
uv_close((uv_handle_t *)req->handle, NULL); // 发送完成后关闭连接
}
void on_connect(uv_connect_t *req, int status) {
if (status < 0) {
fprintf(stderr, "Connect error %s\n", uv_strerror(status));
return;
}
uv_buf_t buffer = uv_buf_init(strdup(MESSAGE), strlen(MESSAGE));
write_req.data = buffer.base;
uv_write(&write_req, req->handle, &buffer, 1, on_write);
}
int main() {
loop = uv_default_loop();
uv_tcp_init(loop, &client);
struct sockaddr_in server_addr;
uv_ip4_addr("127.0.0.1", PORT, &server_addr);
uv_tcp_connect(&connect_req, &client, (const struct sockaddr *)&server_addr,
on_connect);
return uv_run(loop, UV_RUN_DEFAULT);
}
示例代码:DHCP 客户端¶
DHCP 介绍¶
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议) 是一种网络协议,用于自动分配IP地址和其他网络配置参数(如子网掩码、默认网关、DNS服务器等)给网络中的设备。通过DHCP,设备无需手动配置即可加入网络并通信。
DHCP 的工作原理¶
DHCP
的工作过程分为四个主要步骤,通常称为 DORA
过程:
-
DHCP Discover:
客户端启动时,发送一个DHCP Discover广播消息,寻找可用的DHCP服务器。
-
DHCP Offer:
收到Discover消息的DHCP服务器会回应一个DHCP Offer消息,提供可用的IP地址及其他配置信息。
-
DHCP Request:
客户端选择一个Offer,并发送DHCP Request消息,请求使用该IP地址。
-
DHCP Acknowledgment:
服务器确认请求,发送DHCP Acknowledgment消息,正式分配IP地址和配置信息。
DHCP 进行分配IP地址过程中,参与角色有:
-
DHCP 客户端:
通常是网络设备(如电脑、手机),负责发送请求并接收服务器分配的IP地址和配置信息。
-
DHCP 服务端:
负责管理IP地址池,响应客户端请求,分配IP地址和配置信息,并确保地址的唯一性。
以下是 DHCP 客户端与服务端交互的时序图:
+---------+ +---------+
| Client | | Server |
+---------+ +---------+
| |
| DHCP Discover |
| Src: 0.0.0.0:68 |
| Dst: 255.255.255.255:67 |
| ------------------------>|
| |
| DHCP Offer |
| Src: ServerIP:67 |
| Dst: 255.255.255.255:68 |
| <------------------------|
| |
| DHCP Request |
| Src: 0.0.0.0:68 |
| Dst: 255.255.255.255:67 |
| ------------------------>|
| |
| DHCP Acknowledgment |
| Src: ServerIP:67 |
| Dst: 255.255.255.255:68 |
| <------------------------|
| |
+---------+ +---------+
| Client | | Server |
+---------+ +---------+
在上面时序图中,ServerIP
是指 DHCP
服务端的 IP
地址。在局域网中,DHCP
服务器通常是一个路由器或专门的服务器设备,它的 IP
地址是固定的(静态 IP
地址),时序图中使用 ServerIP
代指了。
示例代码¶
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
uv_loop_t *loop;
uv_udp_t send_socket;
uv_udp_t recv_socket;
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = malloc(suggested_size);
buf->len = suggested_size;
}
void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf,
const struct sockaddr *addr, unsigned flags) {
if (nread < 0) {
fprintf(stderr, "Read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t *)&req, NULL);
if (buf->base) free(buf->base);
return;
}
char sender[17] = {0};
uv_ip4_name((const struct sockaddr_in *)addr, sender, 16);
fprintf(stderr, "Recv from %s\n", sender);
unsigned int *as_integer = (unsigned int *)buf->base;
unsigned int ipbin = ntohl(as_integer[4]);
unsigned char ip[4] = {0};
int i;
for (i = 0; i < 4; i++) ip[i] = (ipbin >> i * 8) & 0xff;
fprintf(stderr, "Offered IP %d.%d.%d.%d\n", ip[3], ip[2], ip[1], ip[0]);
free(buf->base);
uv_udp_recv_stop(req);
}
// DHCP报文格式见:https://support.huawei.com/enterprise/zh/doc/EDOC1100174722/2b689419
uv_buf_t make_discover_msg() {
uv_buf_t buffer;
alloc_buffer(NULL, 256, &buffer);
memset(buffer.base, 0, buffer.len);
// BOOTREQUEST
buffer.base[0] = 0x1;
// HTYPE ethernet
buffer.base[1] = 0x1;
// HLEN
buffer.base[2] = 0x6;
// HOPS
buffer.base[3] = 0x0;
// XID 4 bytes
if (uv_random(NULL, NULL, &buffer.base[4], 4, 0, NULL)) abort();
// SECS
buffer.base[8] = 0x0;
// FLAGS
buffer.base[10] = 0x80;
// CIADDR 12-15 is all zeros
// YIADDR 16-19 is all zeros
// SIADDR 20-23 is all zeros
// GIADDR 24-27 is all zeros
// CHADDR 28-43 is the MAC address, use your own
buffer.base[28] = 0xe4;
buffer.base[29] = 0xce;
buffer.base[30] = 0x8f;
buffer.base[31] = 0x13;
buffer.base[32] = 0xf6;
buffer.base[33] = 0xd4;
// SNAME 64 bytes zero
// FILE 128 bytes zero
// OPTIONS
// - magic cookie
buffer.base[236] = 99;
buffer.base[237] = 0x82;
buffer.base[238] = 83;
buffer.base[239] = 99;
// DHCP Message type
buffer.base[240] = 53;
buffer.base[241] = 1;
buffer.base[242] = 1; // DHCPDISCOVER
// DHCP Parameter request list
buffer.base[243] = 55;
buffer.base[244] = 4;
buffer.base[245] = 1;
buffer.base[246] = 3;
buffer.base[247] = 15;
buffer.base[248] = 6;
return buffer;
}
void on_send(uv_udp_send_t *req, int status) {
if (status) {
fprintf(stderr, "Send error %s\n", uv_strerror(status));
return;
}
}
int main() {
loop = uv_default_loop();
uv_udp_init(loop, &recv_socket);
struct sockaddr_in recv_addr;
uv_ip4_addr("0.0.0.0", 68, &recv_addr);
uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr,
UV_UDP_REUSEADDR);
uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);
uv_udp_init(loop, &send_socket);
struct sockaddr_in broadcast_addr;
uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);
uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);
uv_udp_set_broadcast(&send_socket, 1);
uv_udp_send_t send_req;
uv_buf_t discover_msg = make_discover_msg();
struct sockaddr_in send_addr;
uv_ip4_addr("255.255.255.255", 67, &send_addr);
uv_udp_send(&send_req, &send_socket, &discover_msg, 1,
(const struct sockaddr *)&send_addr, on_send);
return uv_run(loop, UV_RUN_DEFAULT);
}
示例代码:DNS 解析器¶
DNS 介绍¶
DNS(Domain Name System,域名系统) 是互联网中用于将 域名(如 www.example.com
)解析为 IP 地址(如 93.184.216.34
)的系统。
DNS 解析的组成部分¶
-
域名空间(Domain Name Space):
- 域名是一个层次化的结构,从右到左依次是:
- 根域名(
.
,通常省略) - 顶级域名(如
.com
、.org
) - 二级域名(如
example
) - 子域名(如
www
)
- 根域名(
- 域名是一个层次化的结构,从右到左依次是:
-
DNS 服务器:
- 根域名服务器:全球共有 13 组根域名服务器,存储顶级域名服务器的地址。
- 顶级域名服务器(TLD):存储二级域名服务器的地址(如
.com
、.org
)。 - 权威域名服务器:存储具体域名的 IP 地址(如
example.com
)。 - 本地 DNS 服务器:由 ISP(互联网服务提供商)提供,负责缓存和转发 DNS 查询。
-
DNS 解析器(Resolver):
- 客户端设备(如电脑、手机)上的软件,负责发起 DNS 查询并接收结果。
DNS 记录类型¶
DNS 数据库中存储了多种类型的记录,常见的包括:
- A 记录:将域名映射到 IPv4 地址。
- AAAA 记录:将域名映射到 IPv6 地址。
- CNAME 记录:将域名映射到另一个域名(别名)。
- MX 记录:指定邮件服务器的地址。
- NS 记录:指定域名的权威域名服务器。
- TXT 记录:存储文本信息(如 SPF 记录)。
DNS 解析的过程¶
DNS 解析是一个递归和迭代结合的过程。以下是详细步骤:
-
客户端发起查询
- 用户在浏览器中输入域名(如
www.example.com
)。 - 客户端(如操作系统或浏览器)向 本地 DNS 服务器 发送 DNS 查询请求。
- 用户在浏览器中输入域名(如
-
本地 DNS 服务器查询
- 如果本地 DNS 服务器缓存了该域名的 IP 地址,则直接返回结果。
- 如果没有缓存,则本地 DNS 服务器开始递归查询。
-
递归查询
- 本地 DNS 服务器向 根域名服务器 查询
.com
顶级域名服务器的地址。 - 根域名服务器返回
.com
顶级域名服务器的地址。
- 本地 DNS 服务器向 根域名服务器 查询
-
迭代查询
- 本地 DNS 服务器向 .com 顶级域名服务器 查询
example.com
的权威域名服务器地址。 .com
顶级域名服务器返回example.com
的权威域名服务器地址。
- 本地 DNS 服务器向 .com 顶级域名服务器 查询
-
获取最终结果
- 本地 DNS 服务器向
example.com
的权威域名服务器 查询www.example.com
的 IP 地址。 - 权威域名服务器返回
www.example.com
的 IP 地址。
- 本地 DNS 服务器向
-
返回结果
- 本地 DNS 服务器将 IP 地址返回给客户端。
- 客户端使用该 IP 地址与目标服务器建立连接。
DNS 解析过程的图示如下:
+---------+ +-----------------+ +-----------------+ +-----------------+
| Client | | Local DNS Server| | Root DNS Server | | TLD DNS Server |
+---------+ +-----------------+ +-----------------+ +-----------------+
| | | |
| 1. 查询 www.example.com | | |
| --------------------> | | |
| | 2. 查询 .com 的 TLD 服务器 | |
| | --------------------> | |
| | | 3. 返回 .com TLD 服务器地址 |
| | <-------------------- | |
| | 4. 查询 example.com 的权威服务器 | |
| | -----------------------------------------> |
| | | 5. 返回 example.com 的权威服务器地址 |
| | <----------------------------------------- |
| | 6. 查询 www.example.com 的 IP 地址 | |
| | -----------------------------------------> |
| | | 7. 返回 www.example.com 的 IP 地址 |
| | <----------------------------------------- |
| 8. 返回 IP 地址 | | |
| <------------------- | | |
+---------+ +-----------------+ +-----------------+ +-----------------+
为了提高解析效率,DNS 查询结果会被缓存:
- 客户端缓存:操作系统或浏览器会缓存 DNS 查询结果。
- 本地 DNS 服务器缓存:ISP 的 DNS 服务器会缓存查询结果。
- TTL(Time to Live):每条 DNS 记录都有一个 TTL 值,表示缓存的有效时间。
示例代码¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>
uv_loop_t *loop;
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
buf->base = malloc(suggested_size);
buf->len = suggested_size;
}
void on_read(uv_stream_t *client, ssize_t nread, const uv_buf_t *buf) {
if (nread < 0) {
if (nread != UV_EOF) fprintf(stderr, "read error %s\n", uv_err_name(nread));
uv_close((uv_handle_t *)client, NULL);
if (buf->base) free(buf->base);
free(client);
return;
}
if (nread == 0) {
if (buf->base) free(buf->base);
return;
}
char *data = (char *)malloc(sizeof(char) * (nread + 1));
data[nread] = '\0';
strncpy(data, buf->base, nread);
fprintf(stderr, "%s", data);
free(data);
free(buf->base);
}
void on_connect(uv_connect_t *req, int status) {
if (status < 0) {
fprintf(stderr, "connect failed error: %s\n", uv_err_name(status));
free(req);
return;
}
uv_read_start((uv_stream_t *)req->handle, alloc_buffer, on_read);
}
void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {
if (status < 0) {
fprintf(stderr, "getaddrinfo error: %s\n", uv_strerror(status));
return;
}
char addr[17] = {0};
uv_ip4_name((struct sockaddr_in *)res->ai_addr, addr, 16);
fprintf(stderr, "%s\n", addr);
uv_connect_t *connect_req = (uv_connect_t *)malloc(sizeof(uv_connect_t));
uv_tcp_t *socket = (uv_tcp_t *)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);
uv_tcp_connect(connect_req, socket, res->ai_addr, on_connect);
uv_freeaddrinfo(res);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <domain>\n", argv[0]);
return EXIT_FAILURE;
}
loop = uv_default_loop();
struct addrinfo hints;
hints.ai_family = PF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
uv_getaddrinfo_t resolver;
fprintf(stderr, "Resolving...\n");
int r = uv_getaddrinfo(loop, &resolver, on_resolved, argv[1], "80", &hints);
if (r) {
fprintf(stderr, "getaddrinfo error: %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}