inet_ntoa 函数深度解析

inet_ntoa函数深度解析

<摘要>
inet_ntoa 是网络编程中将32位网络字节序IPv4地址转换为点分十进制字符串的关键函数。本文通过生动的比喻、详细的代码示例和可视化图表,全面解析了这个经典但存在线程安全问题的函数。从基本用法到内部实现机制,从简单示例到实际应用场景,深入浅出地讲解了inet_ntoa的方方面面,并提供了现代替代方案inet_ntop的使用指南。


<解析>

1. 函数的基本介绍与用途:IP地址的"翻译官"

想象一下,你有一个外国朋友给你寄明信片,上面写着一串神秘的数字:“0x4A7D2B3C”。这对普通人来说就像天书一样难懂。这时候,inet_ntoa就像一位专业的翻译官,能够把这串十六进制的"外星语"翻译成我们熟悉的"74.125.43.60"这样的点分十进制格式。

生活中的比喻

  • inet_ntoa = IP地址的"同声传译"
  • 它把计算机理解的"机器语言"(二进制IP)翻译成人类能看懂的"日常语言"
  • 就像把"2024年1月15日"翻译成"二零二四年一月十五日"一样自然

常见使用场景

// 当你在调试网络程序时,看到这样的输出:
客户端连接来自: 192.168.1.100:54321
// 而不是令人困惑的:客户端连接来自: 0xC0A80164:54321

2. 函数的声明与来源:inet_ntoa的"身份证"

2.1 函数声明

#include <arpa/inet.h>

char *inet_ntoa(struct in_addr in);

2.2 来源背景

inet_ntoa是伯克利套接字API家族的一员,诞生于20世纪80年代的BSD Unix系统。它属于POSIX标准的一部分,现在几乎所有的类Unix系统(包括Linux、macOS)都支持这个函数。

历史小故事
在互联网的早期,程序员们需要频繁地在二进制IP地址和可读格式之间转换。当时没有统一的函数,每个程序员都要自己写转换代码。inet_ntoa的出现就像给整个行业制定了一个"翻译标准",让大家都能用同一种方式"说IP地址的语言"。

3. 返回值含义:一把"双刃剑"

3.1 正常返回值

char *result = inet_ntoa(ip_address);
// result指向一个静态缓冲区,包含如"192.168.1.1"的字符串

3.2 返回值的特点(重要!)

好消息:函数总是成功,不会返回NULL(因为转换过程很简单,几乎不会失败)

坏消息:返回值指向一个静态缓冲区,这意味着:

  1. 非线程安全:在多线程环境中,如果两个线程同时调用inet_ntoa,第二个调用会覆盖第一个的结果
  2. 不可重入:连续调用会覆盖之前的结果
  3. 生命周期短暂:返回值指向的内存在下次调用时会被重用
// 危险示例!
struct in_addr ip1, ip2;
ip1.s_addr = inet_addr("192.168.1.1");
ip2.s_addr = inet_addr("10.0.0.1");

char *str1 = inet_ntoa(ip1);
char *str2 = inet_ntoa(ip2);

printf("IP1: %s\n", str1); // 可能输出"10.0.0.1"!
printf("IP2: %s\n", str2); // 输出"10.0.0.1"

4. 参数详解:struct in_addr的"内心世界"

4.1 参数类型解剖

struct in_addr {
    in_addr_t s_addr;  // 32位的IPv4地址(网络字节序)
};

in_addr_t的真面目

  • 实际上就是uint32_t(32位无符号整数)
  • 使用网络字节序(大端序)存储

4.2 参数取值示例

特殊地址十六进制值点分十进制含义
INADDR_ANY0x000000000.0.0.0监听所有接口
INADDR_LOOPBACK0x7F000001127.0.0.1回环地址
INADDR_BROADCAST0xFFFFFFFF255.255.255.255广播地址

5. 使用示例三部曲:从新手到专家

5.1 示例一:基础转换("Hello World"版)

/**
 * @brief inet_ntoa基础演示
 * 
 * 最简单的使用示例,展示如何将二进制IP转换为可读字符串
 * 就像学习外语时第一个学会的"Hello World"
 */

#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main() {
    printf("=== inet_ntoa基础演示 ===\n");
    
    // 创建一个IPv4地址结构
    struct in_addr ip_addr;
    
    // 设置IP地址(使用网络字节序)
    // 192.168.1.100 的十六进制是 0xC0A80164
    ip_addr.s_addr = htonl(0xC0A80164);
    
    // 使用inet_ntoa进行转换
    char *ip_str = inet_ntoa(ip_addr);
    
    printf("二进制IP: 0x%08X\n", ip_addr.s_addr);
    printf("点分十进制: %s\n", ip_str);
    printf("转换完成!\n");
    
    return 0;
}

编译运行

gcc -o basic_demo basic_demo.c
./basic_demo

预期输出

=== inet_ntoa基础演示 ===
二进制IP: 0xC0A80164
点分十进制: 192.168.1.100
转换完成!

5.2 示例二:网络编程实战(“迷你网络侦探”)

/**
 * @brief 网络连接信息分析器
 * 
 * 模拟真实的网络编程场景,展示如何从sockaddr_in中提取IP信息
 * 就像一个网络侦探,能够分析连接来自哪里
 */

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// 模拟接收到的客户端连接信息
void analyze_connection(struct sockaddr_in *client_addr) {
    printf("\n🔍 发现新的网络连接!\n");
    
    // 提取IP地址信息
    char *client_ip = inet_ntoa(client_addr->sin_addr);
    int client_port = ntohs(client_addr->sin_port);
    
    printf("📍 客户端位置: %s:%d\n", client_ip, client_port);
    
    // 分析IP地址类型
    if (client_addr->sin_addr.s_addr == htonl(INADDR_LOOPBACK)) {
        printf("💻 这是本地回环连接(自己连接自己)\n");
    } else if ((ntohl(client_addr->sin_addr.s_addr) & 0xFF000000) == 0x0A000000) {
        printf("🏠 这是私有A类地址(10.x.x.x)\n");
    } else if ((ntohl(client_addr->sin_addr.s_addr) & 0xFFFF0000) == 0xC0A80000) {
        printf("🏠 这是私有C类地址(192.168.x.x)\n");
    } else {
        printf("🌐 这是公网地址\n");
    }
}

int main() {
    printf("=== 网络连接分析器 ===\n");
    
    // 模拟几个不同的客户端连接
    struct sockaddr_in conn1, conn2, conn3;
    
    // 连接1:本地回环
    memset(&conn1, 0, sizeof(conn1));
    conn1.sin_family = AF_INET;
    conn1.sin_port = htons(12345);
    inet_pton(AF_INET, "127.0.0.1", &conn1.sin_addr);
    
    // 连接2:家庭路由器常见地址
    memset(&conn2, 0, sizeof(conn2));
    conn2.sin_family = AF_INET;
    conn2.sin_port = htons(54321);
    inet_pton(AF_INET, "192.168.0.100", &conn2.sin_addr);
    
    // 连接3:公网地址(示例)
    memset(&conn3, 0, sizeof(conn3));
    conn3.sin_family = AF_INET;
    conn3.sin_port = htons(8080);
    inet_pton(AF_INET, "8.8.8.8", &conn3.sin_addr);
    
    // 分析每个连接
    analyze_connection(&conn1);
    analyze_connection(&conn2);
    analyze_connection(&conn3);
    
    printf("\n✅ 所有连接分析完成!\n");
    return 0;
}

编译运行

gcc -o network_detective network_detective.c
./network_detective

预期输出

=== 网络连接分析器 ===

🔍 发现新的网络连接!
📍 客户端位置: 127.0.0.1:12345
💻 这是本地回环连接(自己连接自己)

🔍 发现新的网络连接!
📍 客户端位置: 192.168.0.100:54321
🏠 这是私有C类地址(192.168.x.x)

🔍 发现新的网络连接!
📍 客户端位置: 8.8.8.8:8080
🌐 这是公网地址

✅ 所有连接分析完成!

5.3 示例三:线程安全问题演示(“危险的舞蹈”)

/**
 * @brief inet_ntoa线程安全问题演示
 * 
 * 通过多线程环境展示inet_ntoa的潜在危险
 * 就像两个人在同一个舞台上跳舞,容易踩到对方的脚
 */

#include <stdio.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

#define NUM_THREADS 3

// 线程参数结构
struct thread_data {
    int thread_id;
    struct in_addr ip_addr;
    char ip_name[20];
};

// 线程函数
void *convert_ip(void *threadarg) {
    struct thread_data *data = (struct thread_data *)threadarg;
    
    printf("线程%d: 开始转换IP %s\n", data->thread_id, data->ip_name);
    
    // 模拟一些工作延迟
    usleep(100000 * data->thread_id);  // 100ms的倍数
    
    // 危险操作:在多线程中调用inet_ntoa
    char *ip_str = inet_ntoa(data->ip_addr);
    
    printf("线程%d: 转换结果 - %s\n", data->thread_id, ip_str);
    
    // 再次模拟工作延迟
    usleep(100000);
    
    // 再次调用,看看结果是否被其他线程修改了
    char *ip_str_again = inet_ntoa(data->ip_addr);
    printf("线程%d: 再次检查 - %s\n", data->thread_id, ip_str_again);
    
    pthread_exit(NULL);
}

// 安全的替代方案(使用inet_ntop)
void *convert_ip_safe(void *threadarg) {
    struct thread_data *data = (struct thread_data *)threadarg;
    char buffer[INET_ADDRSTRLEN];
    
    printf("线程%d[安全]: 开始转换IP %s\n", data->thread_id, data->ip_name);
    
    usleep(100000 * data->thread_id);
    
    // 安全操作:使用inet_ntop
    inet_ntop(AF_INET, &(data->ip_addr), buffer, INET_ADDRSTRLEN);
    
    printf("线程%d[安全]: 转换结果 - %s\n", data->thread_id, buffer);
    
    usleep(100000);
    
    // 再次检查,结果应该是稳定的
    inet_ntop(AF_INET, &(data->ip_addr), buffer, INET_ADDRSTRLEN);
    printf("线程%d[安全]: 再次检查 - %s\n", data->thread_id, buffer);
    
    pthread_exit(NULL);
}

int main() {
    printf("=== inet_ntoa线程安全演示 ===\n");
    printf("⚠️  注意:在多线程环境中,inet_ntoa可能产生不可预期的结果!\n\n");
    
    pthread_t threads[NUM_THREADS];
    struct thread_data td[NUM_THREADS];
    int rc;
    
    // 准备测试数据
    const char *test_ips[NUM_THREADS] = {"192.168.1.100", "10.0.0.50", "172.16.0.25"};
    
    printf("🎯 演示1: 危险的inet_ntoa多线程使用\n");
    
    for (int i = 0; i < NUM_THREADS; i++) {
        td[i].thread_id = i + 1;
        inet_pton(AF_INET, test_ips[i], &td[i].ip_addr);
        strcpy(td[i].ip_name, test_ips[i]);
        
        printf("创建线程%d,处理IP: %s\n", i+1, test_ips[i]);
        rc = pthread_create(&threads[i], NULL, convert_ip, (void *)&td[i]);
        if (rc) {
            printf("错误:无法创建线程,返回码:%d\n", rc);
            return -1;
        }
    }
    
    // 等待所有线程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    
    printf("\n🛡️  演示2: 安全的inet_ntop多线程使用\n");
    
    for (int i = 0; i < NUM_THREADS; i++) {
        td[i].thread_id = i + 1;
        inet_pton(AF_INET, test_ips[i], &td[i].ip_addr);
        strcpy(td[i].ip_name, test_ips[i]);
        
        printf("创建安全线程%d,处理IP: %s\n", i+1, test_ips[i]);
        rc = pthread_create(&threads[i], NULL, convert_ip_safe, (void *)&td[i]);
        if (rc) {
            printf("错误:无法创建线程,返回码:%d\n", rc);
            return -1;
        }
    }
    
    // 等待所有线程完成
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    
    printf("\n✅ 演示完成!建议在多线程程序中使用inet_ntop代替inet_ntoa\n");
    return 0;
}

编译运行

gcc -o thread_demo thread_demo.c -lpthread
./thread_demo

预期输出(可能因调度顺序不同而略有差异)

=== inet_ntoa线程安全演示 ===
⚠️  注意:在多线程环境中,inet_ntoa可能产生不可预期的结果!

🎯 演示1: 危险的inet_ntoa多线程使用
创建线程1,处理IP: 192.168.1.100
创建线程2,处理IP: 10.0.0.50
创建线程3,处理IP: 172.16.0.25
线程1: 开始转换IP 192.168.1.100
线程2: 开始转换IP 10.0.0.50
线程3: 开始转换IP 172.16.0.25
线程1: 转换结果 - 192.168.1.100
线程2: 转换结果 - 10.0.0.50
线程1: 再次检查 - 172.16.0.25  # 注意:这里被线程3覆盖了!
线程3: 转换结果 - 172.16.0.25
线程2: 再次检查 - 172.16.0.25  # 也被覆盖了!
线程3: 再次检查 - 172.16.0.25

🛡️  演示2: 安全的inet_ntop多线程使用
创建安全线程1,处理IP: 192.168.1.100
创建安全线程2,处理IP: 10.0.0.50
创建安全线程3,处理IP: 172.16.0.25
线程1[安全]: 开始转换IP 192.168.1.100
线程2[安全]: 开始转换IP 10.0.0.50
线程3[安全]: 开始转换IP 172.16.0.25
线程1[安全]: 转换结果 - 192.168.1.100
线程2[安全]: 转换结果 - 10.0.0.50
线程1[安全]: 再次检查 - 192.168.1.100  # 安全:结果稳定
线程3[安全]: 转换结果 - 172.16.0.25
线程2[安全]: 再次检查 - 10.0.0.50      # 安全:结果稳定
线程3[安全]: 再次检查 - 172.16.0.25    # 安全:结果稳定

✅ 演示完成!建议在多线程程序中使用inet_ntop代替inet_ntoa

6. 编译与运行指南

6.1 编译命令汇总

# 基础编译
gcc -o program program.c

# 包含调试信息
gcc -g -o program program.c

# 多线程程序编译
gcc -lpthread -o program program.c

# 严格编译(推荐)
gcc -Wall -Wextra -std=c99 -o program program.c

6.2 Makefile完整示例

# inet_ntoa演示程序Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -g
LDFLAGS = -lpthread
TARGETS = basic_demo network_detective thread_demo

# 默认目标
all: $(TARGETS)

# 基础演示程序
basic_demo: basic_demo.c
	$(CC) $(CFLAGS) -o $@ $<

# 网络侦探程序
network_detective: network_detective.c
	$(CC) $(CFLAGS) -o $@ $<

# 线程安全演示程序
thread_demo: thread_demo.c
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<

# 清理编译结果
clean:
	rm -f $(TARGETS) *.o

# 运行所有测试
test: all
	@echo "=== 运行基础演示 ==="
	./basic_demo
	@echo ""
	@echo "=== 运行网络侦探 ==="
	./network_detective
	@echo ""
	@echo "=== 运行线程安全演示 ==="
	./thread_demo

.PHONY: all clean test

7. 执行结果深度分析

7.1 为什么会出现线程安全问题?

inet_ntoa的内部实现大致是这样的:

// 模拟inet_ntoa的内部实现(简化版)
static char buffer[16];  // 静态缓冲区!

char *inet_ntoa(struct in_addr in) {
    unsigned char *bytes = (unsigned char *)&in.s_addr;
    
    // 将4个字节格式化为点分十进制
    snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", 
             bytes[0], bytes[1], bytes[2], bytes[3]);
    
    return buffer;  // 返回指向静态缓冲区的指针
}

问题所在:所有线程共享同一个静态缓冲区,就像多个人共用一支笔写字,后面的人会擦掉前面人写的内容。

7.2 字节序的魔法

inet_ntoa会自动处理字节序问题,但理解这个过程很重要:

// 假设我们要转换 192.168.1.100
// 内存中的网络字节序:0xC0 (192) 0xA8 (168) 0x01 (1) 0x64 (100)
// inet_ntoa会按正确的顺序提取这些字节

struct in_addr addr;
addr.s_addr = htonl((192 << 24|168 << 16|1 << 8| 100;
// inet_ntoa(addr) 会得到 "192.168.1.100"

8. 现代替代方案:inet_ntop

8.1 为什么需要替代品?

特性inet_ntoainet_ntop
线程安全❌ 不安全✅ 安全
IPv6支持❌ 仅IPv4✅ 支持IPv4/IPv6
缓冲区控制❌ 使用静态缓冲区✅ 用户提供缓冲区
错误处理❌ 无错误返回✅ 有错误返回值

8.2 inet_ntop使用示例

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr ipv4_addr;
    struct in6_addr ipv6_addr;
    char buffer[INET6_ADDRSTRLEN];  // 足够存放IPv6地址
    
    // IPv4转换
    inet_pton(AF_INET, "192.168.1.1", &ipv4_addr);
    if (inet_ntop(AF_INET, &ipv4_addr, buffer, sizeof(buffer))) {
        printf("IPv4: %s\n", buffer);
    }
    
    // IPv6转换
    inet_pton(AF_INET6, "2001:db8::1", &ipv6_addr);
    if (inet_ntop(AF_INET6, &ipv6_addr, buffer, sizeof(buffer))) {
        printf("IPv6: %s\n", buffer);
    }
    
    return 0;
}

9. 可视化总结:inet_ntoa的工作原理

graph TD
    A[“32位网络字节序IP地址”] --> B{“inet_ntoa转换过程”}
    
    B --> C[“提取字节0”]
    B --> D[“提取字节1”] 
    B --> E[“提取字节2”]
    B --> F[“提取字节3”]
    
    C --> G[“转换为十进制”]
    D --> H[“转换为十进制”]
    E --> I[“转换为十进制”]
    F --> J[“转换为十进制”]
    
    G --> K[“添加点号分隔符”]
    H --> K
    I --> K
    J --> K
    
    K --> L[“写入静态缓冲区”]
    L --> M[“返回缓冲区指针”]
    M --> N[“点分十进制字符串”]
    
    style A fill:#e1f5fe
    style N fill:#c8e6c9
    style B fill:#fff3e0

转换过程详解

  1. 输入:32位网络字节序的IP地址(如0xC0A80164)
  2. 字节提取:按顺序提取4个字节:[0xC0, 0xA8, 0x01, 0x64]
  3. 十进制转换:将每个字节转为十进制:[192, 168, 1, 100]
  4. 格式化:用点号连接成"192.168.1.100"
  5. 输出:返回指向结果字符串的指针

10. 实用技巧与最佳实践

10.1 什么时候可以使用inet_ntoa?

尽管有线程安全问题,但在以下情况下还是可以使用的:

  1. 单线程程序:简单的命令行工具或脚本
  2. 调试代码:临时打印IP地址信息
  3. 学习目的:理解网络地址转换的基本概念
  4. 遗留代码维护:不想修改现有稳定代码

10.2 安全使用inet_ntoa的变通方案

如果必须在多线程环境中使用inet_ntoa,可以这样做:

// 方案1:使用互斥锁保护
pthread_mutex_t inet_mutex = PTHREAD_MUTEX_INITIALIZER;

char *thread_safe_ntoa(struct in_addr in) {
    char *result;
    pthread_mutex_lock(&inet_mutex);
    result = inet_ntoa(in);
    // 立即复制结果到线程本地存储
    static __thread char local_buffer[16];  // 线程本地存储
    strcpy(local_buffer, result);
    pthread_mutex_unlock(&inet_mutex);
    return local_buffer;
}

// 方案2:直接使用snprintf手动转换
char *manual_ntoa(struct in_addr in) {
    static __thread char buffer[16];
    unsigned char *bytes = (unsigned char *)&in.s_addr;
    snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", 
             bytes[0], bytes[1], bytes[2], bytes[3]);
    return buffer;
}

总结:inet_ntoa的遗产与未来

inet_ntoa就像网络编程世界的一位"老前辈"——它简单易用,为无数程序员解决了IP地址显示的难题,但它的设计理念已经跟不上现代编程的需求。正如我们不会用打字机来写今天的程序一样,在新的项目中,我们应该优先选择更安全、更强大的inet_ntop。

关键要点回顾

  • ✅ inet_ntoa适合简单的单线程程序
  • ❌ 避免在多线程程序中使用
  • 🔄 考虑使用inet_ntop作为现代替代方案
  • 📚 理解其工作原理有助于调试网络程序

inet_ntoa的故事告诉我们:技术在不断进步,作为程序员,我们既要尊重历史遗产,也要勇于拥抱更好的解决方案。

分析下这段多线程服务器代码:#include "web_server.h" // 线程处理函数 void *thread_func(void *arg) { int client_fd = *(int *)arg; free(arg); pthread_detach(pthread_self()); handle_client(client_fd); return NULL; } // 多线程服务器 void thread_server() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); pthread_t thread_id; // 创建socket if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(EXIT_FAILURE); } // 设置socket选项 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))){ perror("setsockopt"); exit(EXIT_FAILURE); } // 绑定地址 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = INADDR_ANY; memset(&(server_addr.sin_zero), '\0', 8); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) ){ perror("bind"); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, BACKLOG) == -1) { perror("listen"); exit(EXIT_FAILURE); } printf("Thread server listening on port %d...\n", PORT); // 主循环 while (1) { client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd == -1) { perror("accept"); continue; } printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); int *new_client_fd = malloc(sizeof(int)); *new_client_fd = client_fd; if (pthread_create(&thread_id, NULL, thread_func, new_client_fd) != 0) { perror("pthread_create"); close(client_fd); free(new_client_fd); } } close(server_fd); } int main() { thread_server(); return 0; }
08-12
帮我解读这六段代码,我是初学者,尽量详细并补充相关知识 #ifndef _ARP_RECV_H #define _ARP_RECV_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/if_ether.h> #include <net/if_arp.h> #include <net/ethernet.h> /* 以太网帧首部长度 / #define ETHER_HEADER_LEN sizeof(struct ether_header) / 整个arp结构长度 / #define ETHER_ARP_LEN sizeof(struct ether_arp) / 以太网 + 整个arp结构长度 / #define ETHER_ARP_PACKET_LEN ETHER_HEADER_LEN + ETHER_ARP_LEN / IP地址长度 */ #define IP_ADDR_LEN 4 void err_exit(const char *err_msg); #ifndef LOCAL #define LOCAL static #endif #endif。。#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/if_ether.h> #include <net/if_arp.h> #include <net/ethernet.h> #include"arp_recv.h" #include “nvmp_utils.h” #include “nsd_common.h” #include “libds.h” #include “libdms.h” #include “flashio.h” #include “dms_tool.h” LOCAL void err_exit(const char *err_msg) { perror(err_msg); exit(1); } LOCAL void arp_recv_main() { struct ether_arp *arp_packet; char buf[ETHER_ARP_PACKET_LEN]; int sock_raw_fd, ret_len, i; if ((sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1) { err_exit(“socket()”); } while (1) { bzero(buf, ETHER_ARP_PACKET_LEN); ret_len = recv(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0); if (ret_len > 0) { /* 剥去以太头部 */ arp_packet = (struct ether_arp )(buf + ETHER_HEADER_LEN); / arp操作码为代表arp应答 */ if (ntohs(arp_packet->arp_op) == 2) { printf(“====ARP replay\n”); printf(“Sender IP address: “); for (i = 0; i < IP_ADDR_LEN; i++) { printf(”%u”, arp_packet->arp_spa[i]); if(i != (IP_ADDR_LEN-1)) { printf(“.”); } } printf(“\nSender MAC address: “); for (i = 0; i < ETH_ALEN; i++) { printf(”%02x”, arp_packet->arp_sha[i]); if(i != (ETH_ALEN-1)) { printf(“😊; } } printf(”\nTarget IP address: “); for (i = 0; i < IP_ADDR_LEN; i++) { printf(”%u", arp_packet->arp_tpa[i]); if(i != (IP_ADDR_LEN-1)) { printf(“.”); } } printf(“\nTarget MAC address: “); for (i = 0; i < ETH_ALEN; i++) { printf(”%02x”, arp_packet->arp_tha[i]); if(i != (ETH_ALEN-1)) { printf(“😊; } } printf(”\n"); } } } close(sock_raw_fd); } NSD_INIT(arp_recv_main);。。#ifndef _ARP_REQUEST_H #define _ARP_REQUEST_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/if_ether.h> #include <net/ethernet.h> #include <net/if_arp.h> #include <net/if.h> #include <netpacket/packet.h> /* 以太网帧首部长度 / #define ETHER_HEADER_LEN sizeof(struct ether_header) / 整个arp结构长度 / #define ETHER_ARP_LEN sizeof(struct ether_arp) / 以太网 + 整个arp结构长度 / #define ETHER_ARP_PACKET_LEN ETHER_HEADER_LEN + ETHER_ARP_LEN / IP地址长度 / #define IP_ADDR_LEN 4 / 广播地址 */ #define BROADCAST_ADDR void err_exit(const char *err_msg); struct ether_arp *fill_arp_packet(const unsigned char *src_mac_addr, const char *src_ip, const char *dst_ip); void arp_request(const char *if_name, const char *dst_ip); #endif。。/* Copyright© * file arp_request.c brief This is a work of sending arp request. author Zhou Shijun version 1.0.1 date 24Aug28 history */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/if_ether.h> #include <net/ethernet.h> #include <net/if_arp.h> #include <net/if.h> #include <netpacket/packet.h> #include"arp_request.h" void err_exit(const char *err_msg) { perror(err_msg); exit(1); } /* 填充arp包 */ struct ether_arp *fill_arp_packet(const unsigned char *src_mac_addr, const char *src_ip, const char *dst_ip) { struct ether_arp *arp_packet; struct in_addr src_in_addr, dst_in_addr; unsigned char dst_mac_addr[ETH_ALEN] = BROADCAST_ADDR; /* IP地址转换 / inet_pton(AF_INET, src_ip, &src_in_addr); inet_pton(AF_INET, dst_ip, &dst_in_addr); / 整个arp包 */ arp_packet = (struct ether_arp *)malloc(ETHER_ARP_LEN); arp_packet->arp_hrd = htons(ARPHRD_ETHER); arp_packet->arp_pro = htons(ETHERTYPE_IP); arp_packet->arp_hln = ETH_ALEN; arp_packet->arp_pln = IP_ADDR_LEN; arp_packet->arp_op = htons(ARPOP_REQUEST); memcpy(arp_packet->arp_sha, src_mac_addr, ETH_ALEN); memcpy(arp_packet->arp_tha, dst_mac_addr, ETH_ALEN); memcpy(arp_packet->arp_spa, &src_in_addr, IP_ADDR_LEN); memcpy(arp_packet->arp_tpa, &dst_in_addr, IP_ADDR_LEN); return arp_packet; } /* arp请求 */ void arp_request(const char *if_name, const char *dst_ip) { struct sockaddr_ll saddr_ll; struct ether_header *eth_header; struct ether_arp *arp_packet; struct ifreq ifr; char buf[ETHER_ARP_PACKET_LEN]; unsigned char src_mac_addr[ETH_ALEN]; unsigned char dst_mac_addr[ETH_ALEN] = BROADCAST_ADDR; char *src_ip; int sock_raw_fd, ret_len, i; if ((sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1) { err_exit(“socket()”); } bzero(&saddr_ll, sizeof(struct sockaddr_ll)); bzero(&ifr, sizeof(struct ifreq)); /* 网卡接口名 / memcpy(ifr.ifr_name, if_name, strlen(if_name)); / 获取网卡接口索引 / if (ioctl(sock_raw_fd, SIOCGIFINDEX, &ifr) == -1) { err_exit(“ioctl() get ifindex”); } saddr_ll.sll_ifindex = ifr.ifr_ifindex; saddr_ll.sll_family = PF_PACKET; / 获取网卡接口IP */ if (ioctl(sock_raw_fd, SIOCGIFADDR, &ifr) == -1) { err_exit(“ioctl() get ip”); } src_ip = inet_ntoa(((struct sockaddr_in )&(ifr.ifr_addr))->sin_addr); printf(“local ip:%s\n”, src_ip); / 获取网卡接口MAC地址 / if (ioctl(sock_raw_fd, SIOCGIFHWADDR, &ifr)) { err_exit(“ioctl() get mac”); } memcpy(src_mac_addr, ifr.ifr_hwaddr.sa_data, ETH_ALEN); printf(“local mac”); for (i = 0; i < ETH_ALEN; i++) { printf(“:%0x”, src_mac_addr[i]); } printf(“\n”); bzero(buf, ETHER_ARP_PACKET_LEN); / 填充以太首部 */ eth_header = (struct ether_header )buf; memcpy(eth_header->ether_shost, src_mac_addr, ETH_ALEN); memcpy(eth_header->ether_dhost, dst_mac_addr, ETH_ALEN); eth_header->ether_type = htons(ETHERTYPE_ARP); / arp包 / arp_packet = fill_arp_packet(src_mac_addr, src_ip, dst_ip); memcpy(buf + ETHER_HEADER_LEN, arp_packet, ETHER_ARP_LEN); / 发送请求 */ ret_len = sendto(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll)); if (ret_len > 0) { printf(“Send successfully!\n”); } close(sock_raw_fd); } LOCAL void arp_request_main(int argc, const char *argv[]) { if (argc != 3) { printf(“usage:%s device_name dst_ip\n”, argv[0]); exit(1); } arp_request(argv[1], argv[2]); return 0; } NSD_INIT(arp_request_main);。。#ifndef _ARP_SCAN_H #define _ARP_SCAN_H #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/if_ether.h> #include <net/if_arp.h> #include <netpacket/packet.h> #include <sys/ioctl.h> #include <net/if.h> #include <time.h> #include <pthread.h> /* — 宏定义: 参数默认值 — / #define ETHER_HEADER_LEN sizeof(struct ether_header) #define ETHER_ARP_LEN sizeof(struct ether_arp) #define ETHER_ARP_PACKET_LEN (ETHER_HEADER_LEN + ETHER_ARP_LEN) #define BROADCAST_ADDR {0xff, 0xff, 0xff, 0xff, 0xff, 0xff} #define IP_ADDR_LEN 4 #define MAX_ARP_ENTRIES 100 / — 数据结构定义 — */ typedef struct { unsigned char mac[ETH_ALEN]; char ip[INET_ADDRSTRLEN]; time_t last_seen; } arp_entry_t; typedef struct { int enabled; /* 功能开关*/ int scan_interval; /* 扫描周期(秒)/ int entry_lifetime; / 有效期(秒)/ int packet_interval; / 发包间隔(毫秒)/ char start_ip[INET_ADDRSTRLEN]; char end_ip[INET_ADDRSTRLEN]; arp_entry_t arp_entries[MAX_ARP_ENTRIES]; int entry_count; / 当前ARP条目数量*/ pthread_mutex_t lock; /* 互斥锁*/ } arp_config_t; arp_config_t arp_config = { .enabled = 1, /* 功能开关开启*/ .scan_interval = 60, /* 扫描周期为60秒*/ .entry_lifetime = 300, /* 有效期为300秒*/ .packet_interval = 100, /* 发包间隔为100毫秒*/ .start_ip = “192.168.1.100”, /* 起始IP*/ .end_ip = “192.168.1.200”, /* 结束IP*/ .entry_count = 0 /初始化ARP条目数量/ }; /* — 函数定义 — */ void err_exit(const char *err_msg); struct ether_arp *fill_arp_packet(const unsigned char *src_mac_addr, const char *src_ip, const char *dst_ip); void add_arp_entry(const char *ip, const unsigned char *mac); void cleanup_arp_entries(); void arp_scan(const char *if_name); LOCAL int arp_scan_init(); LOCAL int arp_scan_check(); LOCAL int arp_scan_start(); LOCAL int arp_scan_reload(DS_MSG *msg); LOCAL void arp_scan_main(); #ifndef LOCAL #define LOCAL static #endif #endif。。#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/if_ether.h> #include <net/if_arp.h> #include <netpacket/packet.h> #include <sys/ioctl.h> #include <net/if.h> #include <time.h> #include <pthread.h> #include “arp_scan.h” void err_exit(const char *err_msg) { perror(err_msg); exit(1); } struct ether_arp *fill_arp_packet(const unsigned char *src_mac_addr, const char *src_ip, const char *dst_ip) { struct ether_arp *arp_packet = (struct ether_arp *)malloc(ETHER_ARP_LEN); unsigned char dst_mac_addr[ETH_ALEN] = BROADCAST_ADDR; struct in_addr src_in_addr, dst_in_addr; inet_pton(AF_INET, src_ip, &src_in_addr); inet_pton(AF_INET, dst_ip, &dst_in_addr); arp_packet->arp_hrd = htons(ARPHRD_ETHER); arp_packet->arp_pro = htons(ETHERTYPE_IP); arp_packet->arp_hln = ETH_ALEN; arp_packet->arp_pln = IP_ADDR_LEN; arp_packet->arp_op = htons(ARPOP_REQUEST); memcpy(arp_packet->arp_sha, src_mac_addr, ETH_ALEN); memcpy(arp_packet->arp_tha, dst_mac_addr, ETH_ALEN); memcpy(arp_packet->arp_spa, &src_in_addr, IP_ADDR_LEN); memcpy(arp_packet->arp_tpa, &dst_in_addr, IP_ADDR_LEN); return arp_packet; } void add_arp_entry(const char *ip, const unsigned char *mac) { pthread_mutex_lock(&arp_config.lock); if (arp_config.entry_count < MAX_ARP_ENTRIES) { strcpy(arp_config.arp_entries[arp_config.entry_count].ip, ip); memcpy(arp_config.arp_entries[arp_config.entry_count].mac, mac, ETH_ALEN); arp_config.arp_entries[arp_config.entry_count].last_seen = time(NULL); arp_config.entry_count++; } pthread_mutex_unlock(&arp_config.lock); } void cleanup_arp_entries() { pthread_mutex_lock(&arp_config.lock); time_t now = time(NULL); for (int i = 0; i < arp_config.entry_count; i++) { if (now - arp_config.arp_entries[i].last_seen > arp_config.entry_lifetime) { // 删除过期条目 for (int j = i; j < arp_config.entry_count - 1; j++) { arp_config.arp_entries[j] = arp_config.arp_entries[j + 1]; } arp_config.entry_count–; i–; // 调整索引 } } pthread_mutex_unlock(&arp_config.lock); } void arp_scan(const char *if_name) { struct sockaddr_ll saddr_ll; struct ether_header *eth_header; struct ether_arp *arp_packet; struct ifreq ifr; char buf[ETHER_ARP_PACKET_LEN]; unsigned char src_mac_addr[ETH_ALEN]; char src_ip[INET_ADDRSTRLEN]; int sock_raw_fd; if ((sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP))) == -1) err_exit(“socket()”); memset(&saddr_ll, 0, sizeof(struct sockaddr_ll)); memset(&ifr, 0, sizeof(struct ifreq)); memcpy(ifr.ifr_name, “eth0”, strlen(“eth0”)); // 使用默认接口名 if (ioctl(sock_raw_fd, SIOCGIFINDEX, &ifr) == -1) err_exit(“ioctl() get ifindex”); saddr_ll.sll_ifindex = ifr.ifr_ifindex; saddr_ll.sll_family = PF_PACKET; if (ioctl(sock_raw_fd, SIOCGIFADDR, &ifr) == -1) err_exit(“ioctl() get ip”); strcpy(src_ip, inet_ntoa(((struct sockaddr_in *)&(ifr.ifr_addr))->sin_addr)); if (ioctl(sock_raw_fd, SIOCGIFHWADDR, &ifr) == -1) err_exit(“ioctl() get mac”); memcpy(src_mac_addr, ifr.ifr_hwaddr.sa_data, ETH_ALEN); while (arp_config.enabled) { char target_ip[INET_ADDRSTRLEN]; for (int i = inet_addr(arp_config.start_ip); i <= inet_addr(arp_config.end_ip); i++) { struct in_addr addr; addr.s_addr = i; strcpy(target_ip, inet_ntoa(addr)); memset(buf, 0, ETHER_ARP_PACKET_LEN); eth_header = (struct ether_header *)buf; memcpy(eth_header->ether_shost, src_mac_addr, ETH_ALEN); memcpy(eth_header->ether_dhost, BROADCAST_ADDR, ETH_ALEN); eth_header->ether_type = htons(ETHERTYPE_ARP); arp_packet = fill_arp_packet(src_mac_addr, src_ip, target_ip); memcpy(buf + ETHER_HEADER_LEN, arp_packet, ETHER_ARP_LEN); sendto(sock_raw_fd, buf, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&saddr_ll, sizeof(struct sockaddr_ll)); free(arp_packet); printf(“ARP request sent to %s\n”, target_ip); usleep(arp_config.packet_interval * 1000); // 发包间隔 } cleanup_arp_entries(); // 清理过期的ARP条目 sleep(arp_config.scan_interval); // 等待下一次扫描 } close(sock_raw_fd); } LOCAL int arp_scan_init() { if (0 == ds_read(ARP_DATA_PATH, &arp_config_t, sizeof(arp_config_t))) { return SLP_ESYSTEM; } /* Initialize socket / if ((sockfd = socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, htons(ETH_P_ARP))) < 0) { perror(“socket”); return ERROR; } / Initialize ip_mac_table */ U8 table_len = arp_config_t.end_ip[3] - arp_config_t.start_ip[3] + 1; ip_mac_table = (ARP_IPMAC_TABLE *)malloc(sizeof(ARP_IPMAC_TABLE) * table_len); struct timeval cur_time; gettimeofday(&cur_time, NULL); for (int i = 0; i < table_len; i++) { ip_mac_table[i].renew_time = cur_time; ip_mac_table[i].device_is_exist = 0; memset(ip_mac_table[i].mac, 0, ETH_ALEN); } msg_attach_handler(MSGID_DMS_CMD, arp_call_handle); return OK; } LOCAL int arp_scan_check() { if (arp_config_t.end_ip[2] != sender_ip[2] || arp_config_t.start_ip[2] != sender_ip[2] || arp_config_t.end_ip[3] <= arp_config_t.start_ip[3]) { ARP_DEBUG(“Invalid IP address range, please check.\n”); return ERROR; } return OK; } LOCAL int arp_scan_start() { /* Start address expiration check thread */ pthread_create(&time_tid, NULL, check_table_renew_time, NULL); pthread_detach(time_tid); /* Scan loop */ while (scanning_flag) { scan_once(); usleep(arpco.scan_interval); } return OK; } LOCAL int arp_scan_reload(DS_MSG msg) { / Stop scanning */ scanning_flag = 0; if (ds_path_id_exist(msg->id, msg->num, ARP_DATA_PATH)) { arp_config_t arp_data; memset(&arp_data, 0, sizeof(arp_config_t)); if (0 == ds_read(ARP_DATA_PATH, (U8 )&arp_data, sizeof(arp_config_t))) { ARP_ERROR(“Read arp data ERROR”); return ERROR; } / Reload params / memcpy(arp_config_t.start_ip, arp_data.start_ip, ARP_IPV4_LEN); memcpy(arp_config_t.end_ip, arp_data.end_ip, ARP_IPV4_LEN); arp_config_t.scan_interval = arp_data.scan_interval; arp_config_t.packet_interval = arp_data.packet_interval; arp_config_t.entry_lifetime = arp_data.entry_lifetime; / Cancel old checking thread / pthread_cancel(time_tid); / Realloc ip_mac_table */ U8 table_len = arp_config_t.end_ip[3] - arp_config_t.start_ip[3] + 1; ARP_IPMAC_TABLE *new_table; new_table = (ARP_IPMAC_TABLE )realloc(ip_mac_table, sizeof(ARP_IPMAC_TABLE) * table_len); if (NULL == new_table) { ARP_ERROR(“Realloc ipmac_table ERROR”); free(ip_mac_table); ip_mac_table = NULL; return ERROR; } ip_mac_table = new_table; struct timeval cur_time; gettimeofday(&cur_time, NULL); for (int i = 0; i < table_len; i++) { ip_mac_table[i].renew_time = cur_time; ip_mac_table[i].device_is_exist = 0; memset(ip_mac_table[i].mac, 0, ETH_ALEN); } / Restart checking thread / / Start scanning */ scanning_flag = 1; arp_scan_start(); return OK; } } LOCAL void arp_scan_main() { DS_OPT_DESC main_options[] = { DS_SWITCH_OPT(arp_config_t, enabled, OPT_FLAG_NORM), DS_STR_OPT(arp_config_t, start_ip, OPT_FLAG_NORM), DS_STR_OPT(arp_config_t, end_ip, OPT_FLAG_NORM), DS_S32_OPT(arp_config_t, scan_interval, OPT_FLAG_NORM), DS_S32_OPT(arp_config_t, packet_interval, OPT_FLAG_NORM), DS_S32_OPT(arp_config_t, entry_lifetime, OPT_FLAG_NORM), }; DS_SEG_DESC main_segments[] = { DS_STRUCT_SEG(“config”, SEG_LIM_RW, SEG_GROUP_ROOT, arp_config_t, main_options), }; DS_SECT_DESC arp_scan_sections[] = { DS_STRUCT_SECT(“config”, main_segments), }; DS_TBL_DESC arp_scan_tables[] = { DS_STRUCT_TBL(“arp_scan”, TBL_ATTR_STC, arp_scan_sections), }; DS_DAT_MON_DESC arp_data_monitor[] = { DS_DAT_MON(ARP_DATA_PATH, DATA_ATTRI_NOTIFY), }; DS_MOD_DESC arp_scan_module = DS_STRUCT_MOD(“arp_scan”, arp_scan_init, arp_scan_check, arp_scan_reload, arp_scan_start, arp_scan_tables, arp_data_monitor); MODULE *module_node = ds_register_module(“arp_scan”, &arp_module); NSD_ASSERT(NULL != module_node); } NSD_INIT(arp_scan_main);
08-27
<think>好的,我现在需要回答用户关于inet_ntoa函数的功能和用法的问题。用户提到他们想了解这个函数在C语言网络编程中的用途以及如何使用它。首先,我得回忆一下这个函数的基本信息。 根据之前提供的引用资料,特别是引用[3]和引用[5],inet_ntoa的作用是将网络字节序的IPv4地址转换为点分十进制的字符串。这通常用于将结构体in_addr中的二进制地址转换成人类可读的格式。例如,当从网络接收到的数据中包含IP地址时,可能需要用这个函数来转换以便显示或处理。 接下来,关于函数的使用方法,用户可能需要知道函数的原型、参数和返回值。根据引用[3],函数原型是char *inet_ntoa(struct in_addr inp); 参数是一个in_addr结构体,返回值是指向字符串的指针。需要注意的是,引用[4]提到这个函数是线程不安全的,因为返回的指针指向静态缓冲区,多次调用会被覆盖。这可能在使用多线程时导致问题,需要提醒用户注意。 然后,我需要考虑如何组织这些信息。首先解释功能,然后给出函数原型,接着是参数和返回值的说明,再提供一个使用示例。同时,需要指出线程安全的问题,并建议替代方案如inet_ntop,如引用[4]所述。 用户可能需要一个代码示例来理解如何在C语言中使用这个函数。比如,声明一个in_addr结构体,设置其s_addr字段为网络字节序的IP地址,然后调用inet_ntoa转换。示例代码应该包括必要的头文件,如<arpa/inet.h>,并展示如何打印结果。需要注意的是,由于返回值是静态缓冲区,连续调用可能会覆盖之前的结果,这点在示例中最好体现出来。 此外,用户可能关心常见错误和使用场景。例如,处理网络数据时如何正确转换地址,或者在多线程环境下应避免使用inet_ntoa而选择其他函数。引用[5]提到大小端问题,但inet_ntoa处理的是网络字节序(大端),所以转换时会自动处理字节序,不需要用户手动调整。 总结需要强调inet_ntoa的基本功能和使用方法,同时指出其局限性,如线程不安全,并推荐更安全的替代方法。这样用户不仅能了解如何正确使用该函数,还能意识到潜在的问题和解决方案。 最后,生成相关问题,可能涉及线程安全、替代函数、错误处理等,这些问题可以帮助用户进一步深入学习。</think>### inet_ntoa函数的功能与使用方法 #### 功能说明 `inet_ntoa`函数用于将网络字节序的IPv4地址(`struct in_addr`类型)转换为点分十进制字符串格式(如`"192.168.1.1"`)。它在网络编程中常用于将二进制IP地址转换为人类可读形式[^3][^5]。 #### 函数原型与参数 ```c #include <arpa/inet.h> char *inet_ntoa(struct in_addr inp); ``` - **参数**:`struct in_addr`类型的结构体,存储网络字节序的IPv4地址。 - **返回值**:指向静态缓冲区的字符串指针,存储转换后的点分十进制地址。 **注意**:由于该缓冲区是静态的,多次调用会覆盖结果,因此它是**线程不安全**的[^4][^5]。 --- #### 使用示例 ```c #include <stdio.h> #include <arpa/inet.h> int main() { struct in_addr addr; addr.s_addr = htonl(0xC0A80101); // 设置网络字节序的IP(192.168.1.1) char *ip_str = inet_ntoa(addr); printf("IP地址:%s\n", ip_str); // 输出:IP地址:192.168.1.1 // 注意:连续调用会覆盖结果 struct in_addr addr2; addr2.s_addr = htonl(0x7F000001); // 127.0.0.1 printf("第二个IP:%s\n", inet_ntoa(addr2)); // 输出:第二个IP:127.0.0.1 return 0; } ``` --- #### 注意事项 1. **线程不安全**:在多线程环境中,建议改用线程安全的`inet_ntop`函数[^4]。 ```c inet_ntop(AF_INET, &addr, buffer, INET_ADDRSTRLEN); ``` 2. **字节序处理**:输入地址需为**网络字节序**(大端),通常使用`htonl`或`inet_addr`转换原始值。 3. **返回值生命周期**:返回的字符串指针指向静态内存,不可长期保存,需立即使用或复制。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青草地溪水旁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值