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"));
// ... 更安全!
}
学习要点总结
必须掌握的概念:
-
Socket编程三部曲:创建→连接→通信
-
HTTP协议结构:请求行、请求头、空行
-
字节序转换:htons/ntohs, inet_addr/inet_ntoa
-
字符串处理:strstr查找,strchr截断
扩展知识:
-
HTTP状态码:
-
200:成功
-
404:未找到
-
500:服务器错误
-
-
JSON解析:考虑使用jansson或cJSON库
-
HTTPS支持:需要SSL/TLS加密
-
多线程/异步:同时查询多个城市
实战练习
练习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;
}
}
总结
这个程序展示了:
-
TCP Socket编程的基础应用
-
HTTP客户端的实现方法
-
API调用的基本流程
-
JSON数据解析的简单技巧
核心价值:
-
理解网络编程的基本原理
-
掌握HTTP协议的实际应用
-
学会如何调用第三方API
-
实践字符串处理和解析技术