应用——HTTP天气查询

HTTP天气查询客户端 - 详细解析

完整代码(带详细注释)

cpp 复制代码
#include <arpa/inet.h>     // 网络地址转换
#include <netinet/in.h>    // 网络地址结构
#include <netinet/ip.h>    // IP协议相关
#include <stdio.h>         // 标准输入输出
#include <stdlib.h>        // 标准库函数
#include <string.h>        // 字符串处理
#include <sys/socket.h>    // Socket编程
#include <sys/types.h>     // 数据类型
#include <time.h>          // 时间函数
#include <unistd.h>        // Unix标准函数

// 简化类型定义,方便使用
typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    /* ==================== 第一部分:建立TCP连接 ==================== */
    
    // 1. 创建TCP套接字(相当于买手机)
    // AF_INET: IPv4地址族
    // SOCK_STREAM: TCP协议(流式套接字)
    // 0: 自动选择合适的协议
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");  // 输出错误信息:socket: 错误描述
        return 1;          // 程序异常退出
    }
    
    // 2. 设置服务器地址信息(知道打给谁)
    struct sockaddr_in ser;        // IPv4地址结构
    bzero(&ser, sizeof(ser));      // 清空内存,防止随机值干扰
    ser.sin_family = AF_INET;      // 地址族:IPv4
    ser.sin_port = htons(80);      // 端口号80(HTTP默认端口),htons转换字节序
    ser.sin_addr.s_addr = inet_addr("8.129.233.227");  // API服务器IP地址
    
    // 3. 连接服务器(相当于拨打电话)
    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {
        perror("connect");  // 连接失败
        return 1;
    }
    
    /* ==================== 第二部分:构造HTTP请求 ==================== */
    
    // 创建HTTP请求头部数组
    char *http_cmd[7] = {NULL};  // 7个请求头字段
    
    // 请求行:GET方法 + API路径 + HTTP版本
    http_cmd[0] =
        "GET "  // HTTP方法:获取数据
        "/?app=weather.today&cityNm=%E8%A5%BF%E5%AE%89&appkey=36397&sign="
        "41451f2bb7b779f8366f4312f18dfdab&format=json HTTP/1.1\r\n";
    // 说明:
    // /?app=weather.today        → API功能:查询今日天气
    // &cityNm=%E8%A5%BF%E5%AE%89  → 城市名称:"西安"的URL编码
    // &appkey=36397              → API密钥
    // &sign=41451f2bb...         → 签名(验证身份)
    // &format=json               → 返回格式:JSON
    // HTTP/1.1                   → HTTP协议版本
    // \r\n                       → 换行符(HTTP标准要求)
    
    // Host:指定服务器域名
    http_cmd[1] = "Host: api.k780.com\r\n";
    // 作用:告诉服务器要访问哪个网站(虚拟主机)
    
    // Connection:连接控制
    http_cmd[2] = "Connection: keep-alive\r\n";
    // keep-alive:保持TCP连接,可以发送多个请求
    
    // User-Agent:客户端信息
    http_cmd[3] =
        "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 "
        "Safari/537.36\r\n";
    // 伪装成Chrome浏览器,避免被服务器拒绝
    
    // Accept:可接受的内容类型
    http_cmd[4] =
        "Accept: "
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/"
        "avif,image/webp,image/apng,*/*;q=0.8,application/"
        "signed-exchange;v=b3;q=0.7\r\n";
    // q=0.9表示权重,数字越大优先级越高
    
    // Accept-Encoding:可接受的压缩方式
    http_cmd[5] = "Accept-Encoding: gzip, deflate\r\n";
    // 告诉服务器可以用gzip压缩数据,减少传输量
    
    // Accept-Language:可接受的语言
    http_cmd[6] = "Accept-Language: zh-CN,zh;q=0.9\r\n\r\n";
    // 优先接收简体中文,其次是中文
    // \r\n\r\n:两个换行,表示HTTP头部结束
    
    /* ==================== 第三部分:发送HTTP请求 ==================== */
    
    // 循环发送7个HTTP头部字段
    int i = 0;
    for (i = 0; i < 7; i++)
    {
        // send函数:发送数据
        // sockfd: socket描述符
        // http_cmd[i]: 要发送的数据
        // strlen(http_cmd[i]): 数据长度
        // 0: 发送标志(正常发送)
        send(sockfd, http_cmd[i], strlen(http_cmd[i]), 0);
    }
    
    /* ==================== 第四部分:接收HTTP响应 ==================== */
    
    char buf[1024] = {0};  // 接收缓冲区,初始化为0
    
    // recv函数:接收服务器响应
    // sockfd: socket描述符
    // buf: 接收缓冲区
    // sizeof(buf): 缓冲区大小
    // 0: 接收标志
    recv(sockfd, buf, sizeof(buf), 0);
    
    // 打印原始HTTP响应
    printf("%s", buf);
    fflush(stdout);  // 立即刷新输出缓冲区
    
    /* ==================== 第五部分:解析JSON数据 ==================== */
    
    // 定义指针,用于提取天气信息
    char *days = NULL;   // 日期
    char *week = NULL;   // 星期
    char *name = NULL;   // 城市名称
    char *temp = NULL;   // 温度
    char *wea = NULL;    // 天气状况
    char *end = NULL;    // 结束位置
    
    // 使用strstr查找JSON字段
    // strstr(源字符串, 要查找的子串) → 返回子串第一次出现的位置
    
    days = strstr(buf, "days");      // 查找"days"字段
    week = strstr(days, "week");     // 从days位置继续查找"week"
    name = strstr(week, "citynm");   // 查找"citynm"(城市名)
    temp = strstr(week, "temperature");  // 查找"temperature"(温度)
    wea = strstr(temp, "weather");   // 查找"weather"(天气)
    
    /* JSON响应格式示例:
    {
        "success": "1",
        "result": {
            "days": "2025-12-25",
            "week": "星期四",
            "citynm": "西安",
            "temperature": "3℃/-5℃",
            "weather": "晴"
        }
    }
    */
    
    /* 提取日期字段 */
    days += 7;                // 跳过"days": "这7个字符
    end = strchr(days, '"');  // 找到下一个双引号(字段结束)
    *end = '\0';              // 替换为字符串结束符
    
    /* 提取星期字段 */
    week += 7;                // 跳过"week": "
    end = strchr(week, '"');  // 找到结束引号
    *end = '\0';              // 替换为结束符
    
    /* 提取城市名字段 */
    name += 9;                // 跳过"citynm": "
    end = strchr(name, '"');  // 找到结束引号
    *end = '\0';              // 替换为结束符
    
    /* 提取温度字段 */
    temp += 14;               // 跳过"temperature": "
    end = strchr(temp, '"');  // 找到结束引号
    *end = '\0';              // 替换为结束符
    
    /* 提取天气字段 */
    wea += 10;                // 跳过"weather": "
    end = strchr(wea, '"');   // 找到结束引号
    *end = '\0';              // 替换为结束符
    
    /* ==================== 第六部分:输出结果 ==================== */
    
    // 格式化输出天气信息
    printf("%s %s %s %s %s\n", days, week, name, temp, wea);
    // 示例输出:2025-12-25 星期四 西安 3℃/-5℃ 晴
    
    /* ==================== 第七部分:清理资源 ==================== */
    
    // 关闭socket连接(相当于挂电话)
    close(sockfd);
    
    return 0;  // 程序正常退出
}

核心知识点详解

1. Socket编程基本概念

Socket是什么?

比喻: Socket就像打电话

  • socket() = 买手机

  • connect() = 拨号

  • send() = 说话

  • recv() = 听话

  • close() = 挂电话

TCP vs UDP
复制代码
// TCP:可靠连接,像打电话
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// UDP:不可靠无连接,像发短信  
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

2. HTTP协议详解

HTTP请求结构
复制代码
GET /path?参数 HTTP/1.1\r\n      ← 请求行
Header1: Value1\r\n               ← 请求头开始
Header2: Value2\r\n
...
\r\n                             ← 空行表示头部结束
[请求正文]                        ← GET通常没有
本程序中的HTTP头解析
请求头 作用 示例
Host 指定虚拟主机 Host: api.k780.com
User-Agent 客户端信息 伪装成Chrome浏览器
Accept 可接受格式 优先HTML,其次XML
Accept-Encoding 压缩方式 支持gzip压缩
Accept-Language 语言偏好 优先中文
Connection 连接控制 keep-alive保持连接

3. 网络地址处理

字节序转换
复制代码
// 网络字节序(大端) vs 主机字节序(小端)
ser.sin_port = htons(80);   // 主机→网络(Host TO Network Short)
ser.sin_port = ntohs(80);   // 网络→主机(Network TO Host Short)

// IP地址转换
ser.sin_addr.s_addr = inet_addr("8.129.233.227");  // 字符串→网络字节序
char* ip_str = inet_ntoa(ser.sin_addr);            // 网络字节序→字符串

4. 字符串处理函数

strstr() - 查找子串
复制代码
// 在buf中查找"days"出现的位置
char *pos = strstr(buf, "days");
if (pos != NULL) {
    printf("找到了!位置:%p\n", pos);
}

// 工作方式:
// buf: "..."days":"2025-12-25"..."
// ↑返回这个位置
strchr() - 查找字符
复制代码
// 从days位置开始查找双引号
char *end = strchr(days, '"');
// days: "2025-12-25"..."
// ↑找到这个位置

*end = '\0';  // 截断字符串
// 现在days = "2025-12-25"(以\0结尾)

代码执行流程

图表

调试与改进建议

1. 常见问题及解决

问题1:连接失败
复制代码
// 可能原因:
// 1. IP地址错误
// 2. 端口错误
// 3. 服务器关闭
// 4. 防火墙阻挡

// 解决方案:
// 1. 使用ping测试连通性
// 2. 检查端口是否开放
// 3. 使用telnet测试
//    telnet 8.129.233.227 80
问题2:接收数据不全
复制代码
// 原因:recv可能一次收不完所有数据
char buf[4096];  // 增大缓冲区
int total = 0;

// 循环接收直到收完
while ((n = recv(sockfd, buf + total, sizeof(buf) - total - 1, 0)) > 0) {
    total += n;
}
buf[total] = '\0';

2. 代码改进建议

改进版:更健壮的接收
复制代码
// 改进点1:检查send返回值
for (i = 0; i < 7; i++) {
    int sent = send(sockfd, http_cmd[i], strlen(http_cmd[i]), 0);
    if (sent < 0) {
        perror("send failed");
        close(sockfd);
        return 1;
    }
}

// 改进点2:解析JSON使用专门库
#include <jansson.h>  // JSON解析库
json_t *root;
json_error_t error;

root = json_loads(buf, 0, &error);
if (root) {
    json_t *result = json_object_get(root, "result");
    const char *days = json_string_value(json_object_get(result, "days"));
    // ... 更安全!
}

学习要点总结

必须掌握的概念:

  1. Socket编程三部曲:创建→连接→通信

  2. HTTP协议结构:请求行、请求头、空行

  3. 字节序转换:htons/ntohs, inet_addr/inet_ntoa

  4. 字符串处理:strstr查找,strchr截断

扩展知识:

  1. HTTP状态码

    • 200:成功

    • 404:未找到

    • 500:服务器错误

  2. JSON解析:考虑使用jansson或cJSON库

  3. HTTPS支持:需要SSL/TLS加密

  4. 多线程/异步:同时查询多个城市

实战练习

练习1:修改城市

复制代码
// 将西安改为北京
// 北京 = %E5%8C%97%E4%BA%AC
http_cmd[0] =
    "GET "
    "/?app=weather.today&cityNm=%E5%8C%97%E4%BA%AC&appkey=36397&sign="
    "41451f2bb7b779f8366f4312f18dfdab&format=json HTTP/1.1\r\n";

练习2:添加更多天气信息

复制代码
// 查找并解析更多字段
char *humidity = strstr(wea, "humidity");    // 湿度
char *wind = strstr(humidity, "wind");       // 风向
char *winp = strstr(wind, "winp");           // 风力

// 解析方法同上...

练习3:封装成函数

复制代码
// 创建查询天气的函数
char* query_weather(const char* city) {
    // 1. 建立连接
    // 2. 构造请求(动态生成城市参数)
    // 3. 发送接收
    // 4. 解析返回
    // 5. 返回格式化字符串
}

进阶扩展

1. 支持命令行参数

复制代码
int main(int argc, char **argv)
{
    if (argc != 2) {
        printf("用法: %s <城市名>\n", argv[0]);
        return 1;
    }
    
    // 将城市名转为URL编码
    // 构造动态HTTP请求
    // ...
}

2. 多城市查询

复制代码
char *cities[] = {"北京", "上海", "广州", "深圳"};
for (int i = 0; i < 4; i++) {
    query_weather(cities[i]);
    sleep(1);  // 避免请求太快
}

3. 加入错误处理

复制代码
// 检查HTTP响应状态码
char *status = strstr(buf, "HTTP/1.1");
if (status) {
    status += 9;  // 跳过"HTTP/1.1 "
    if (strncmp(status, "200", 3) != 0) {
        printf("请求失败: %.*s\n", 10, status);
        return 1;
    }
}

总结

这个程序展示了:

  1. TCP Socket编程的基础应用

  2. HTTP客户端的实现方法

  3. API调用的基本流程

  4. JSON数据解析的简单技巧

核心价值:

  • 理解网络编程的基本原理

  • 掌握HTTP协议的实际应用

  • 学会如何调用第三方API

  • 实践字符串处理和解析技术

相关推荐
程序猿编码2 小时前
手动清理 TCP TIME-WAIT 套接字:Linux 内核模块的实现与原理
linux·网络·tcp/ip·linux内核·套接字
智航GIS2 小时前
6.1 for循环
开发语言·python·算法
HERR_QQ2 小时前
【cpp tool】GDB coredump vscode GUI 和多线程常用笔记
ide·笔记·vscode
爱学大树锯2 小时前
353 · 最大字母」
算法
YGGP2 小时前
【Golang】LeetCode 416. 分割等和子集
算法·leetcode
wjykp2 小时前
part4 反向传播算法(BP算法)
人工智能·算法·机器学习
AndrewHZ2 小时前
【图像处理基石】图像处理领域还有哪些核心挑战与难题?
图像处理·人工智能·算法·计算机视觉·噪声·图像增强·画质增强
极客范儿2 小时前
从快手“12·22”事故出发:AI时代,如何构建对抗自动化攻击的动态免疫体系?
网络·人工智能·自动化
啊阿狸不会拉杆2 小时前
《数字图像处理》实验8-图像识别与分类
图像处理·人工智能·算法·分类·数据挖掘·数字图像处理