跳转至

网络编程

传统的网络编程

在 Linux 下进行 C 语言网络编程时,主要依赖于 POSIX 标准定义的系统调用和库函数。这些 API 提供了对网络通信的底层支持,包括创建套接字(Socket)、绑定地址、监听连接、接受连接、发送和接收数据等操作。

下图是一个典型的网络编程流程图:

       server               client
    +----------+         +----------+
    |  socket  |         |  socket  |
    +----------+         +----------+
        |                     |     
        |                     |     
        |                     |     
        v                     |     
    +----------+              |     
    |   bind   |              |     
    +----------+              |     
        |                     |     
        |                     |     
        |                     |     
        v                     |     
    +----------+              |     
    |  listen  |              |     
    +----------+              v     
        |               +----------+
        | <------------ |  connect |
        |               +----------+
        v                     |     
    +----------+              |     
    |  accept  |              |     
    +----------+              |     
        |                     |     
        |                     |     
        |                     |     
        v                     v     
    +----------+         +----------+
    | send/recv| <------ | send/recv|
    +----------+         +----------+

创建套接字

使用 socket() 系统调用来创建一个套接字。

int socket(int domain, int type, int protocol);

参数说明:

  • domain:指定协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)或 AF_UNIX(本地通信)。
  • type:指定套接字类型,如 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)或 SOCK_RAW(原始套接字)。
  • protocol:指定协议,通常设置为 0,表示使用默认协议。

返回值:

  • 成功时返回一个套接字描述符(非负整数)。
  • 失败时返回 -1,并设置 errno。

绑定地址

使用 bind() 系统调用来将套接字绑定到一个本地地址和端口。

int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数说明:

  • sockfd:套接字描述符。
  • addr:指向 struct sockaddr 的指针,包含要绑定的地址和端口信息。
  • addrlen:地址结构的长度。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno。

监听连接

使用 listen() 系统调用来将套接字转换为被动套接字,使其能够接收连接请求。

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:套接字描述符。
  • backlog:指定未完成连接队列的最大长度。

返回值:

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno。

接受连接

使用 accept() 系统调用来接受一个连接请求,返回一个新的套接字描述符用于与客户端通信。

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

参数说明:

  • 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() 系统调用来关闭套接字。

int close(int sockfd);

参数说明:

  • 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 函数。

int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* handle);

参数说明:

  • loop: 事件循环对象(uv_loop_t)。
  • handle: TCP 句柄(uv_tcp_t)。

uv_tcp_bind

将 TCP 句柄绑定到指定的地址和端口,相当于 POSIX 的 bind 函数。

int uv_tcp_bind(uv_tcp_t* handle, const struct sockaddr* addr, unsigned int flags);

参数说明:

  • handle: TCP 句柄(uv_tcp_t)。
  • addr: 地址信息(struct sockaddr)。
  • flags: 绑定标志(如 UV_TCP_IPV6ONLY)。

uv_listen

开始监听连接请求,相当于 POSIX 的 listen 函数。

int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb);

参数说明:

  • stream: 流句柄(uv_stream_t,可以是 TCP 或 Pipe)。
  • backlog: 连接队列的最大长度。
  • cb: 连接回调函数(uv_connection_cb)。

uv_accept

接受连接请求。

int uv_accept(uv_stream_t* server, uv_stream_t* client);

参数说明:

  • 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 句柄。

int uv_udp_init(uv_loop_t* loop, uv_udp_t* handle);

参数说明:

  • loop: 事件循环对象(uv_loop_t)。
  • handle: UDP 句柄(uv_udp_t)。

uv_udp_bind

将 UDP 句柄绑定到指定的地址和端口。

int uv_udp_bind(uv_udp_t* handle, const struct sockaddr* addr, unsigned int flags);

参数说明:

  • handle: UDP 句柄(uv_udp_t)。
  • addr: 地址信息(struct sockaddr)。
  • flags: 绑定标志(如 UV_UDP_REUSEADDR)。

uv_udp_recv_start

开始接收 UDP 数据。

int uv_udp_recv_start(uv_udp_t* handle, uv_alloc_cb alloc_cb, uv_udp_recv_cb recv_cb);

参数说明:

  • handle: UDP 句柄(uv_udp_t)。
  • alloc_cb: 分配缓冲区的回调函数(uv_alloc_cb)。
  • recv_cb: 接收数据的回调函数(uv_udp_recv_cb)。

uv_udp_recv_stop

停止接收 UDP 数据。

int uv_udp_recv_stop(uv_udp_t* handle);

参数说明:

  • 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 在 文件系统 中也有介绍。

int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb);

参数说明:

  • 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

关闭句柄。

void uv_close(uv_handle_t* handle, uv_close_cb close_cb);

参数说明:

  • handle: 需要关闭的句柄(uv_handle_t)。
  • close_cb: 关闭完成回调函数(uv_close_cb)。

uv_ip4_addr

将字符串形式的 IPv4 地址和端口转换为 struct sockaddr_in

int uv_ip4_addr(const char* ip, int port, struct sockaddr_in* addr);

参数说明:

  • 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 返回的地址信息。

void uv_freeaddrinfo(struct addrinfo* ai);

参数说明:

  • ai: 由 uv_getaddrinfo 返回的地址信息(struct addrinfo*)。

回调函数类型

uv_connection_cb

连接回调函数。

typedef void (*uv_connection_cb)(uv_stream_t* server, int status);

参数说明:

  • server: 服务器流句柄(uv_stream_t)。
  • status: 连接状态(0 表示成功,负数表示错误)。

uv_connect_cb

连接完成回调函数。

typedef void (*uv_connect_cb)(uv_connect_t* req, int status);

参数说明:

  • req: 连接请求句柄(uv_connect_t)。
  • status: 连接状态(0 表示成功,负数表示错误)。

uv_read_cb

读取数据回调函数。

typedef void (*uv_read_cb)(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf);

参数说明:

  • 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

写入完成回调函数。

typedef void (*uv_write_cb)(uv_write_t* req, int status);

参数说明:

  • req: 写请求句柄(uv_write_t)。
  • status: 写入状态(0 表示成功,负数表示错误)。

uv_getaddrinfo_cb

域名解析完成后的回调函数。

typedef void (*uv_getaddrinfo_cb)(uv_getaddrinfo_t* req, int status, struct addrinfo* res);

参数说明:

  • 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 解析的组成部分
  1. 域名空间(Domain Name Space):

    • 域名是一个层次化的结构,从右到左依次是:
      • 根域名(.,通常省略)
      • 顶级域名(如 .com.org
      • 二级域名(如 example
      • 子域名(如 www
  2. DNS 服务器:

    • 根域名服务器:全球共有 13 组根域名服务器,存储顶级域名服务器的地址。
    • 顶级域名服务器(TLD):存储二级域名服务器的地址(如 .com.org)。
    • 权威域名服务器:存储具体域名的 IP 地址(如 example.com)。
    • 本地 DNS 服务器:由 ISP(互联网服务提供商)提供,负责缓存和转发 DNS 查询。
  3. DNS 解析器(Resolver):

    • 客户端设备(如电脑、手机)上的软件,负责发起 DNS 查询并接收结果。
DNS 记录类型

DNS 数据库中存储了多种类型的记录,常见的包括:

  1. A 记录:将域名映射到 IPv4 地址。
  2. AAAA 记录:将域名映射到 IPv6 地址。
  3. CNAME 记录:将域名映射到另一个域名(别名)。
  4. MX 记录:指定邮件服务器的地址。
  5. NS 记录:指定域名的权威域名服务器。
  6. TXT 记录:存储文本信息(如 SPF 记录)。
DNS 解析的过程

DNS 解析是一个递归和迭代结合的过程。以下是详细步骤:

  1. 客户端发起查询

    • 用户在浏览器中输入域名(如 www.example.com)。
    • 客户端(如操作系统或浏览器)向 本地 DNS 服务器 发送 DNS 查询请求。
  2. 本地 DNS 服务器查询

    • 如果本地 DNS 服务器缓存了该域名的 IP 地址,则直接返回结果。
    • 如果没有缓存,则本地 DNS 服务器开始递归查询。
  3. 递归查询

    • 本地 DNS 服务器向 根域名服务器 查询 .com 顶级域名服务器的地址。
    • 根域名服务器返回 .com 顶级域名服务器的地址。
  4. 迭代查询

    • 本地 DNS 服务器向 .com 顶级域名服务器 查询 example.com 的权威域名服务器地址。
    • .com 顶级域名服务器返回 example.com 的权威域名服务器地址。
  5. 获取最终结果

    • 本地 DNS 服务器向 example.com 的权威域名服务器 查询 www.example.com 的 IP 地址。
    • 权威域名服务器返回 www.example.com 的 IP 地址。
  6. 返回结果

    • 本地 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 查询结果会被缓存:

  1. 客户端缓存:操作系统或浏览器会缓存 DNS 查询结果。
  2. 本地 DNS 服务器缓存:ISP 的 DNS 服务器会缓存查询结果。
  3. 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);
}

参考资料