嵌入式Linux网络编程实战:基于DNS解析的HTTP客户端实现


嵌入式Linux网络编程实战:基于DNS解析的HTTP客户端实现

【本文代码已在树莓派4B(Linux内核5.10)平台验证通过,适用于物联网设备数据上报等场景】


一、需求场景与功能亮点

1.1 典型物联网通信场景

嵌入式设备 DNS服务器 云服务器 域名解析请求 返回目标IP 发送传感器数据 返回HTTP响应 嵌入式设备 DNS服务器 云服务器

1.2 代码核心功能

  • DNS智能解析:支持域名自动转换为IPv4地址
  • 协议合规性:严格遵循HTTP/1.1标准规范
  • 灵活配置:通过IS_IP宏切换DNS解析/直连IP模式
  • 资源友好:内存占用<50KB(BUFFER_SIZE=4096时)

二、代码解析与关键技术

2.1 DNS解析实现(resolve_dns函数)

char* resolve_dns(const char *hostname)
{
    struct addrinfo hints, *result;
    // 配置查询参数:仅IPv4 + TCP协议
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    
    // 执行DNS查询(关键系统调用)
    int status = getaddrinfo(hostname, NULL, &hints, &result);
    // 错误处理与结果解析...
}

技术要点

  • 使用getaddrinfo获取地址信息链表
  • 遍历链表筛选首个IPv4地址
  • 必须调用freeaddrinfo释放资源

2.2 HTTP请求发送流程(send_http_ask函数)

创建Socket
配置服务器地址
建立TCP连接
构造HTTP报文
发送请求
接收响应

关键代码段

// 构造符合RFC标准的HTTP请求
snprintf(request, sizeof(request),
        "POST %s HTTP/1.1\r\n"                      // 请求行
        "Host: jsonplaceholder.typicode.com\r\n"    // 虚拟主机标识(必须与证书域名匹配)
        "Content-Type: application/json\r\n"        // JSON数据类型
        "Content-Length: %zu\r\n"                   // 必须准确的数据长度
        "Connection: keep-alive\r\n\r\n"            // 保持连接
        "%s",                                       // 请求正文
        path,  strlen(data), data);                 // 重要:Host头必须用域名

注: Host 必须与域名匹配 填写的是域名 更换IP记得改变此处


三、代码使用方法

3.1 编译与运行

# 编译命令
gcc http_client.c -o http_client -Wall -O2

# 运行示例(DNS解析模式)
./http_client
[输出] 域名解析成功:jsonplaceholder.typicode.com → 104.21.32.1
	   已连接到服务器: 104.21.32.1:80
	   服务器响应:
	   HTTP/1.1 201 Created
	   ......
	   {
		  "sensor_id": 1,
		  "value": 25.5,
		  "id": 101
	  }

3.2 参数配置说明

宏定义可选值作用
IS_IPfalse(默认)启用DNS解析
true直连IP模式
SERVER_IP域名或IP字符串目标服务地址
BUFFER_SIZE推荐512-4096网络缓冲区大小

四、环境增强建议

4.1 增加SSL/TLS支持

// 示例:使用wolfSSL库初始化
wolfSSL_CTX* ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method());
wolfSSL* ssl = wolfSSL_new(ctx);
wolfSSL_set_fd(ssl, sockfd);
// 替代send/recv为wolfSSL_write/wolfSSL_read

4.2 添加重试机制

int retries = 3;
while(retries--) {
    if(connect(sockfd, ...) == 0) break;
    sleep(1 << (3 - retries)); // 指数退避
}

4.3 实现完整响应接收

char *response = malloc(BUFFER_SIZE);
size_t total = 0;
while((n = recv(sockfd, response+total, BUFFER_SIZE-total, 0)) > 0) {
    total += n;
    if(total >= BUFFER_SIZE) break;
}

五、常见问题排查指南

5.1 连接超时问题

# 检查网络连通性
ping jsonplaceholder.typicode.com

# 查看路由表
route -n

5.2 DNS解析失败

// 添加详细错误日志
fprintf(stderr, "DNS错误码[%d]: %s\n", 
    status, gai_strerror(status));

5.3 协议兼容性问题

使用Wireshark抓包验证请求格式:

POST /posts HTTP/1.1
Host: jsonplaceholder.typicode.com
Content-Length: 23
Content-Type: application/json

{"sensor_id":1,"value":25.5}

六、在线测试工具验证

推荐使用 HTTPie Online 验证服务端行为:

POST https://jsonplaceholder.typicode.com/posts
Content-Type: application/json

{"sensor_id":1, "value":25.5}

预期响应

{
  "id": 101
}

七、完整代码示例

/*-----------------------------------------------------------
 * 基于DNS解析的HTTP客户端实现
 * 功能:支持域名解析,发送HTTP POST请求到指定接口
 * 特点:
 *   - 根据IS_IP宏选择直接使用IP或DNS解析
 *   - 完整的错误处理机制
 *   - 符合HTTP/1.1协议标准
 *-----------------------------------------------------------*/

// 头文件区域
#include <stdio.h>      // 标准输入输出
#include <stdlib.h>     // 动态内存管理、系统命令
#include <string.h>     // 字符串操作
#include <sys/socket.h> // 套接字编程接口
#include <arpa/inet.h>  // IP地址转换函数
#include <netdb.h>      // DNS解析相关函数
#include <unistd.h>     // 文件描述符操作
#include <stdbool.h>    // 布尔类型支持

// 配置宏定义
#define IS_IP       false                           // 是否直接使用IP模式(true-跳过DNS解析)
#define SERVER_IP   "jsonplaceholder.typicode.com"  // 目标服务器域名/IP
#define SERVER_PORT 80                              // HTTP协议默认端口
#define BUFFER_SIZE 4096                            // 网络缓冲区大小

/*-----------------------------------------------------------
 * 函数:resolve_dns
 * 功能:DNS域名解析
 * 参数:
 *   hostname - 需要解析的域名(如"example.com")
 * 返回值:
 *   成功 - 动态分配的IP地址字符串(需要调用者释放)
 *   失败 - NULL
 * 实现原理:
 *   1. 使用getaddrinfo获取地址信息链表
 *   2. 遍历链表找到第一个IPv4地址
 *   3. 转换二进制地址为字符串格式
 *-----------------------------------------------------------*/
static char* resolve_dns(const char *hostname)
{
    struct addrinfo hints, *result, *rp;
    char *ip = malloc(INET_ADDRSTRLEN);     // 存储 IPv4 地址的缓冲区

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;              // 仅获取 IPv4 地址
    hints.ai_socktype = SOCK_STREAM;        // 指定TCP协议类型

    // 执行DNS查询(关键系统调用)
    int status = getaddrinfo(hostname, NULL, &hints, &result);
    if (status != 0) {
        fprintf(stderr, "DNS解析失败: %s\n", gai_strerror(status));
        free(ip);
        return NULL;
    }

    // 遍历地址信息链表,取第一个有效 IPv4 地址
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        if (rp->ai_family == AF_INET) {     // 筛选IPv4地址
            struct sockaddr_in *ipv4 = (struct sockaddr_in *)rp->ai_addr;
            // 转换网络字节序到字符串
            inet_ntop(AF_INET, &(ipv4->sin_addr), ip, INET_ADDRSTRLEN);
            break;
        }
    }

    freeaddrinfo(result); // 必须释放DNS查询结果内存
    return ip;            // 返回动态分配的IP字符串
}

/*-----------------------------------------------------------
 * 函数:send_http_ask
 * 功能:发送HTTP POST请求
 * 参数:
 *   ip   - 服务器IP地址(字符串格式)
 *   path - 请求路径(如"/api/data")
 *   data - POST数据内容
 * 技术要点:
 *   1. 完整的TCP连接生命周期管理
 *   2. 符合HTTP协议规范的请求构造
 *   3. 基础网络错误处理
 *-----------------------------------------------------------*/
static void send_http_ask(char *ip, const char *path, const char *data)
{
    // ========== 步骤 2: 创建 TCP Socket ==========
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);   // AF_INET: IPv4, SOCK_STREAM: TCP
    if (sockfd < 0) {
        perror("socket");
        return;
    }

    // ========== 步骤 3: 配置服务器地址结构 ==========
    struct sockaddr_in server_addr = {
        .sin_family = AF_INET,                      // IPv4 地址族
        .sin_port = htons(SERVER_PORT)              // 端口号转网络字节序
    };
    if (inet_pton(AF_INET, ip, &server_addr.sin_addr) < 0){  // 将IP字符串转为二进制格式
        perror("inet_pton");
        close(sockfd);
        return;
    }

    // ========== 步骤 4: 建立 TCP 连接 ==========
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr))) {
        perror("connect");
        close(sockfd);
        return;
    }
    printf("已连接到服务器: %s:%d\n", ip, SERVER_PORT);

    // ========== 步骤 5: 构造 HTTP 请求报文 ==========
    char request[BUFFER_SIZE] = {0};
    snprintf(request, sizeof(request),
        "POST %s HTTP/1.1\r\n"                      // 请求行
        "Host: jsonplaceholder.typicode.com\r\n"    // 虚拟主机标识(必须与证书域名匹配)
        "Content-Type: application/json\r\n"        // JSON数据类型
        "Content-Length: %zu\r\n"                   // 必须准确的数据长度
        "Connection: keep-alive\r\n\r\n"            // 保持连接
        "%s",                                       // 请求正文
        path,  strlen(data), data);                 // 重要:Host头必须用域名

    // ========== 步骤 6: 发送请求数据 ==========
    if (send(sockfd, request, strlen(request), 0) == -1) {
        perror("send");
        close(sockfd);
        return;
    }

    // ========== 步骤 7: 接收响应数据 ==========
    char response[BUFFER_SIZE] = {0};
    ssize_t recv_bytes = recv(sockfd, response, BUFFER_SIZE-1, 0);
    if (recv_bytes > 0) {
        response[recv_bytes] = '\0'; // 确保字符串终止
        printf("服务器响应:\n%s\n", response);
    } else if (recv_bytes == 0) {
        printf("连接被服务器关闭\n");
    } else {
        perror("数据接收错误");
    }

    close(sockfd);  // 关闭套接字
}

/*-----------------------------------------------------------
 * 主函数
 * 执行流程:
 *   1. 根据IS_IP模式选择获取IP方式
 *   2. 发送HTTP请求
 *   3. 清理资源
 *-----------------------------------------------------------*/
int main(void)
{
    // 示例数据(JSON 格式)
    const char *sensor_data = "{\"sensor_id\": 1, \"value\": 25.5}";
    char *server_ip = NULL;

    // ====== 步骤1: IP获取逻辑分支 ======
    #if (IS_IP == false)
        // DNS解析模式
        server_ip = resolve_dns(SERVER_IP);
        if (!server_ip) {
            fprintf(stderr, "[致命错误] 域名解析失败:%s\n", SERVER_IP);
            return EXIT_FAILURE;
        }
        printf("域名解析成功:%s → %s\n", SERVER_IP, server_ip);
    #else
        // 直接使用IP模式
        server_ip = (char*)SERVER_IP; 
        printf("跳过DNS解析, 直接使用IP: %s\n", server_ip);
    #endif

    // ====== 发送HTTP请求 ======
    send_http_ask(server_ip, "/posts", sensor_data);

    // ====== 资源清理 ======
    #if (IS_IP == false)
        free(server_ip); // 释放DNS解析分配的内存
    #endif

    return EXIT_SUCCESS;
}

本文从嵌入式场景出发,实现了支持DNS解析的轻量级HTTP客户端,读者可根据实际需求扩展SSL加密、数据压缩等功能。建议在关键业务场景中添加心跳机制和双缓冲队列提升可靠性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

银河码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值