云计算百科
云计算领域专业知识百科平台

基于C语言和HTTP API的天气查询客户端实现与分析

引言

在嵌入式开发或网络编程学习中,通过Socket编程与Web API交互是一项基础而重要的技能。本文基于一段实际编写的天气查询客户端代码,详细分析其实现原理、技术要点,并探讨其中存在的问题与优化方向。该客户端通过TCP连接向远程服务器发送HTTP请求,获取JSON格式的天气数据,并通过字符串操作提取关键信息打印显示。

涉及的技术点

  • Socket网络编程:创建TCP套接字,连接到指定IP和端口。

  • HTTP协议:构造HTTP GET请求,解析响应(含状态行、头部和实体)。

  • 字符串处理:使用strstr定位JSON字段,并通过指针修改字符串以提取数据。

  • JSON数据格式:服务器返回的数据为JSON,但客户端并未使用JSON解析库。

  • 标准I/O与错误处理:printf输出,perror报告错误。

  • 潜在安全问题:使用gets读取用户输入,存在缓冲区溢出风险。

  • 程序整体框架

    程序主要分为以下几个模块:

    • TCP连接建立:CreatTcpConnect函数封装了socket、connect操作。

    • HTTP请求发送:SendHttpRequest构造并发送HTTP GET请求。

    • HTTP响应接收:RecvHttpRespone接收服务器返回的数据。

    • 数据解析与打印:PrintfCurWeather和PrintfFigWeather通过字符串操作从响应体中提取天气信息。

    • 主函数流程:

    • 提示用户输入城市名;

    • 构造当前天气API的URL,建立TCP连接,发送请求,接收响应,关闭连接;

    • 构造未来天气API的URL,重复上述过程;

    • 调用两个打印函数分别解析并显示天气信息。

    代码详细分析

    1. TCP连接创建

    c

    int CreatTcpConnect(char *pserip, int port)
    {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveaddr;
    serveaddr.sin_family = AF_INET;
    serveaddr.sin_port = htons(port);
    serveaddr.sin_addr.s_addr = inet_addr(pserip);
    connect(sockfd, (struct sockaddr *)&serveaddr, sizeof(serveaddr));
    return sockfd;
    }

    该函数创建TCP套接字并连接到指定服务器。使用了IPv4、流式套接字。记得对socket和connect返回值做完整错误处理(原代码中有检查,但这里简化描述)。inet_addr将点分十进制IP转换为网络字节序的整数。

    2. HTTP请求构造

    c

    int SendHttpRequest(int sockfd, char *purl)
    {
    char tmpbuff[4096] = {0};
    sprintf(tmpbuff, "GET %s HTTP/1.1\\r\\n", purl);
    sprintf(tmpbuff, "%sHost: api.k780.com\\r\\n", tmpbuff);
    // … 添加其他头部
    send(sockfd, tmpbuff, strlen(tmpbuff), 0);
    }

    通过多次sprintf拼接HTTP请求,注意这里使用了tmpbuff作为源和目标,虽然能工作,但存在缓冲区重叠的风险。请求包含了必要的头部(Host、User-Agent、Accept等)。

    3. 响应接收

    c

    int RecvHttpRespone(int sockfd, char *ptmpbuff, int maxlen)
    {
    recv(sockfd, ptmpbuff, maxlen, 0);
    }

    简单调用recv一次接收数据,未考虑数据可能分多次到达,也未处理粘包问题。实际网络传输中,HTTP响应可能超过一次recv的大小,且需要解析头部找到内容长度。

    4. 数据解析(核心问题)

    解析函数采用了一种极其脆弱的字符串处理方式:通过strstr查找特定字段名,然后手动修改指针位置并插入字符串结束符,从而“提取”字段值。例如:

    c

    phead = strstr(ptmp_cur, "days");
    phead -= 1;
    ptail = strstr(ptmp_cur, "week");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    这种做法依赖于服务器返回的JSON格式严格固定,且字段顺序不变。但实际上:

    • 如果JSON字段值中包含目标字段名(如"days"出现在值中),会导致错误定位。

    • 指针向前移动(-1、-2)可能越界访问,引发未定义行为。

    • 直接修改原始缓冲区破坏了数据,后续解析可能出错。

    • 未对strstr返回NULL做处理,若字段缺失则程序崩溃。

    更好的做法是使用JSON解析库(如cJSON),但代码中虽然包含了cJSON.h,却并未使用。

    5. 用户输入

    c

    char city[32] = {0};
    gets(city);

    gets是C11标准已废弃的函数,因为它无法限制输入长度,当用户输入超过31个字符时会造成缓冲区溢出,覆盖栈上其他数据,导致程序崩溃或被利用。应改用fgets。

    6. 硬编码问题

    服务器IP地址(103.205.5.206)和端口(80)直接写在代码中,API的域名(api.k780.com)也硬编码在Host头部。若服务器变更IP或域名,需要重新编译。建议通过配置文件或命令行参数传入。

    程序流程图

    text

    +——————-+
    | 输入城市名 |
    +——————-+
    |
    v
    +——————-+
    | 构建当前天气URL |
    +——————-+
    |
    v
    +——————-+
    | 建立TCP连接 |
    +——————-+
    |
    v
    +——————-+
    | 发送HTTP请求 |
    +——————-+
    |
    v
    +——————-+
    | 接收响应 |
    +——————-+
    |
    v
    +——————-+
    | 关闭连接 |
    +——————-+
    |
    v (重复上述步骤获取未来天气)
    |
    v
    +——————-+
    | 解析并打印当前天气|
    +——————-+
    |
    v
    +——————-+
    | 解析并打印未来天气|
    +——————-+

    存在的问题与优化建议

    1. 安全性问题

    • 缓冲区溢出:gets必须替换为fgets(city, sizeof(city), stdin)并处理换行符。

    • 指针越界:phead -= 1、ptail -= 2等操作应检查指针是否仍在有效范围内,避免访问非法内存。

    2. 网络通信可靠性

    • TCP粘包与分段:recv一次可能接收不全,应循环接收直到读取完整HTTP响应。可以通过解析响应头中的Content-Length字段或遇到\\r\\n\\r\\n后继续读取指定长度。

    • 错误处理:send和recv返回值需要全面检查,特别是部分发送或接收的情况。

    • 连接复用:代码中每次请求都新建连接,浪费资源。可以复用TCP连接(Keep-Alive),但当前请求中已包含Connection: keep-alive,需要正确处理。

    3. 数据解析

    • JSON解析:应使用cJSON库解析响应体,避免字符串硬编码。示例:

      c

      cJSON *root = cJSON_Parse(ptmpbuff);
      cJSON *result = cJSON_GetObjectItem(root, "result");
      cJSON *days = cJSON_GetObjectItem(result, "days");
      printf("days: %s\\n", days->valuestring);
      cJSON_Delete(root);

    • 响应格式检查:需要先解析HTTP状态码,确保请求成功(200 OK),再处理JSON。

    4. 代码健壮性

    • 内存管理:所有指针操作需确保不会越界,尤其在修改字符串时。

    • 字符串拼接:使用snprintf替代sprintf防止缓冲区溢出。

    • 头文件与宏:#define _GNU_SOURCE用于启用某些GNU扩展,但代码中未使用相关扩展(如memmem注释掉了),可移除。

    5. 可配置性

    • 将服务器IP、端口、API域名、appkey等放入配置文件或环境变量,提高灵活性。

    • 城市名URL编码:当前直接拼接,若城市包含中文或特殊字符,可能导致请求失败。应进行URL编码。

    6. 其他

    • 错误恢复:若某个请求失败,应给用户提示,而不是继续解析空缓冲区。

    • 代码结构:可以将API请求封装成函数,减少重复代码。

    改进后的示例片段

    c

    // 安全的用户输入
    fgets(city, sizeof(city), stdin);
    city[strcspn(city, "\\n")] = '\\0'; // 去除换行符

    // 复用连接
    int sockfd = CreatTcpConnect(SERVER_IP, SERVER_PORT);
    SendHttpRequest(sockfd, url_cur);
    RecvHttpFull(sockfd, tmpbuff_cur, sizeof(tmpbuff_cur)); // 循环接收
    // 解析当前天气…
    SendHttpRequest(sockfd, url_fig); // 复用同一连接
    RecvHttpFull(sockfd, tmpbuff_fig, sizeof(tmpbuff_fig));
    close(sockfd);

    总结

    本文通过一个简单的天气查询客户端,回顾了C语言网络编程的基础知识,包括Socket使用、HTTP协议交互、字符串处理等。同时揭示了代码中存在的安全风险、健壮性问题和可维护性缺陷。在实际开发中,应遵循安全编码规范,使用合适的库函数,并充分考虑网络通信的复杂性和数据的可变性。通过重构和优化,可以将这个简陋的示例转化为可靠、可扩展的应用程序。

    希望这篇博客能帮助读者加深对C网络编程的理解,并在实践中写出更高质量的代码。

    源码:

    #define _GNU_SOURCE
    #include "cJSON.h"
    #include "head.h"

    int PrintfCurWeather(char *ptmp_cur)
    {
    char *phead = NULL;
    char *ptail = NULL;

    printf("—————- 今天天气 ————–\\n");
    phead = strstr(ptmp_cur, "days");
    phead -= 1;
    ptail = strstr(ptmp_cur, "week");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "week");
    phead -= 1;
    ptail = strstr(ptail, "cityno");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "citynm");
    phead -= 1;
    ptail = strstr(ptail, "cityid");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "temperature");
    phead -= 1;
    ptail = strstr(ptail, "temperature_curr");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "temperature_curr");
    phead -= 1;
    ptail = strstr(ptail, "humidity");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "humidity");
    phead -= 1;
    ptail = strstr(ptail, "aqi");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "weather");
    phead -= 1;
    ptail = strstr(ptail, "weather_curr");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "weather_curr");
    phead -= 1;
    ptail = strstr(ptail, "weather_icon");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "wind");
    phead -= 1;
    ptail = strstr(ptail, "winp");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    return 0;
    }

    int PrintfFigWeather(char *ptmp_fig)
    {
    char *phead = NULL;
    char *ptail = NULL;

    ptail = ptmp_fig;

    printf("—————- 未来几天天气 ————–\\n");

    while(1)
    {
    phead = strstr(ptail, "days");
    if(NULL == phead)
    {
    break;
    }
    phead -= 1;
    ptail = strstr(ptail, "week");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "week");
    phead -= 1;
    ptail = strstr(ptail, "cityno");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "citynm");
    phead -= 1;
    ptail = strstr(ptail, "cityid");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "temperature");
    phead -= 1;
    ptail = strstr(ptail, "humidity");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);

    ptail += 1;
    phead = strstr(ptail, "weather");
    phead -= 1;
    ptail = strstr(ptail, "weather_icon");
    ptail -= 2;
    *ptail = '\\0';
    printf("%s\\n", phead);
    ptail += 1;

    printf("\\n\\n");
    }

    return 0;
    }

    int CreatTcpConnect(char *pserip, int port)
    {
    int ret = 0;
    int sockfd = 0;
    struct sockaddr_in serveaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
    perror("fail to socket");
    return -1;
    }

    serveaddr.sin_family = AF_INET;
    serveaddr.sin_port = htons(port);
    serveaddr.sin_addr.s_addr = inet_addr(pserip);
    ret = connect(sockfd, (struct sockaddr *)&serveaddr, sizeof(serveaddr));
    if(-1 == ret)
    {
    perror("fail to connect");
    return -1;
    }

    return sockfd;
    }

    int SendHttpRequest(int sockfd, char *purl)
    {
    ssize_t nret = 0;
    char tmpbuff[4096] = {0};

    sprintf(tmpbuff, "GET %s HTTP/1.1\\r\\n", purl);
    sprintf(tmpbuff, "%sHost: api.k780.com\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sAccept-Language: en-US,en;q=0.5\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sAccept-Encoding: gzip, deflate\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sConnection: keep-alive\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%sUpgrade-Insecure-Requests: 1\\r\\n", tmpbuff);
    sprintf(tmpbuff, "%s\\r\\n", tmpbuff);

    nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
    if(-1 == nret)
    {
    perror("fail to send");
    return -1;
    }

    return 0;
    }

    int RecvHttpRespone(int sockfd, char *ptmpbuff, int maxlen)
    {
    ssize_t nret = 0;

    nret = recv(sockfd, ptmpbuff, maxlen, 0);
    if(-1 == nret)
    {
    perror("fail to recv");
    return -1;
    }

    return 0;
    }

    int main(void)
    {
    int sockfd = 0;
    char city[32] = {0};
    char url_cur[1024] = {0};
    char url_fig[1024] = {0};
    char tmpbuff_cur[4096] = {0};
    char tmpbuff_fig[8192] = {0};
    char *ptmp = NULL;

    printf("请输入要查询的城市:\\n");
    gets(city);

    sprintf(url_cur, "/?app=weather.today&weaid=%s&appkey=78642&sign=9d7c0f5b3bf9aa6b1b9a29d23ae66215&format=json",city);

    sockfd = CreatTcpConnect("103.205.5.206", 80);

    SendHttpRequest(sockfd, url_cur);

    RecvHttpRespone(sockfd, tmpbuff_cur, sizeof(tmpbuff_cur));

    close(sockfd);

    sprintf(url_fig, "http://api.k780.com/?app=weather.future&weaid=%s&appkey=78642&sign=9d7c0f5b3bf9aa6b1b9a29d23ae66215&format=json",city);

    sockfd = CreatTcpConnect("103.205.5.206", 80);

    SendHttpRequest(sockfd, url_fig);

    RecvHttpRespone(sockfd, tmpbuff_fig, sizeof(tmpbuff_fig));

    close(sockfd);

    PrintfCurWeather(tmpbuff_cur);

    PrintfFigWeather(tmpbuff_fig);

    return 0;
    }

    结果展示:

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基于C语言和HTTP API的天气查询客户端实现与分析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!