嵌入式Linux网络编程实战:基于DNS解析的HTTP客户端实现
【本文代码已在树莓派4B(Linux内核5.10)平台验证通过,适用于物联网设备数据上报等场景】
一、需求场景与功能亮点
1.1 典型物联网通信场景
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函数)
关键代码段:
// 构造符合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_IP | false(默认) | 启用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加密、数据压缩等功能。建议在关键业务场景中添加心跳机制和双缓冲队列提升可靠性。