这个基于HTTP协议的天气预报查询系统是我在Linux环境下用纯C语言开发的一个网络编程练习项目。作为一个嵌入式开发者,我经常需要处理各种网络通信协议,而HTTP作为应用最广泛的协议之一,掌握其底层实现原理对理解整个网络栈非常有帮助。
这个项目最吸引我的地方在于它完整地展示了一个网络应用从底层连接到数据展示的全过程。不同于直接调用现成的HTTP库,这里我们从最基础的socket开始,手动构造HTTP请求,处理响应,解析数据,整个过程就像亲手拆解一台精密的机器,能清晰地看到每个齿轮是如何咬合的。
系统采用经典的三层架构设计:
这种分层设计使得各模块职责清晰,耦合度低。比如,如果要更换数据展示方式(如改用GUI),只需修改用户界面层,其他部分几乎不用改动。
项目中定义了一个精心设计的Weather结构体来组织天气数据:
c复制typedef struct {
char city[32]; // 城市名称
char date[32]; // 日期
char week[32]; // 星期
char weather[32]; // 天气状况
char max_temperature[4]; // 最高温度
char min_temperature[4]; // 最低温度
char wind_direction[32]; // 风向
char wind_power[8]; // 风力
char Humidity[8]; // 湿度
} Weather;
这个结构体的设计考虑了以下几点:
提示:在实际项目中,可以考虑使用枚举类型来定义风向(如N、NE、E等),这样既节省空间又便于后续处理。
网络通信的核心是socket()系统调用,我们创建一个IPv4的TCP套接字:
c复制int CreateTcpConnection(const char *pip, int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
perror("fail to socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(port);
seraddr.sin_addr.s_addr = inet_addr(pip);
if (-1 == connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr))) {
perror("fail to connect");
return -1;
}
return sockfd;
}
这里有几个关键点:
AF_INET指定使用IPv4协议SOCK_STREAM表示面向连接的TCP套接字htons()将端口号转换为网络字节序inet_addr()将点分十进制IP转换为二进制形式我们手动构造了一个符合HTTP/1.1标准的GET请求:
c复制int SendHttpRequest(int sockfd, const char *purl) {
char sendbuf[4096] = {0};
sprintf(sendbuf, "GET %s HTTP/1.1\r\n", purl);
sprintf(sendbuf, "%sHost: api.k780.com\r\n", sendbuf);
sprintf(sendbuf, "%sUser-Agent: WeatherClient/1.0\r\n", sendbuf);
sprintf(sendbuf, "%sAccept: */*\r\n", sendbuf);
sprintf(sendbuf, "%sConnection: close\r\n", sendbuf);
sprintf(sendbuf, "%s\r\n", sendbuf);
if (-1 == send(sockfd, sendbuf, strlen(sendbuf), 0)) {
perror("fail to send");
return -1;
}
return 0;
}
特别注意:
\r\nConnection: close让服务器在响应后关闭连接项目使用了轻量级的cJSON库来解析API返回的JSON数据。解析流程如下:
cJSON_Parse()解析JSON字符串cJSON_GetObjectItem()获取特定字段值cJSON_Delete()释放内存c复制int Analys_today(char* json_string) {
cJSON *root = cJSON_Parse(json_string);
if (!root) {
printf("JSON解析失败\n");
return -1;
}
cJSON *success = cJSON_GetObjectItem(root, "success");
if(0 == strcmp(success->valuestring,"0")) {
printf("该地点未收到天气数据\n");
exit(0);
}
cJSON *result = cJSON_GetObjectItem(root, "result");
strcpy(Today.city, cJSON_GetObjectItem(result, "citynm")->valuestring);
// 其他字段解析...
cJSON_Delete(root);
return 0;
}
API返回的JSON中包含一个success字段,值为"1"表示成功,"0"表示失败。我们在解析前先检查这个字段,确保数据有效:
c复制if(0 == strcmp(success->valuestring,"0")) {
printf("该地点未收到天气数据\n");
exit(0); // 优雅退出
}
注意:实际项目中应该给用户重新输入的机会,而不是直接退出程序。
程序通过简单的scanf()获取用户输入的城市名称:
c复制printf("请输入您要查看的城市:\n");
scanf("%s", City);
在实际应用中,这有几个可以改进的地方:
天气信息通过精心格式化的方式输出,增强可读性:
c复制int ShowToday() {
printf("----------------------今日天气-------------------\n");
printf("------------------------------------------------\n");
printf("城市 : %s\n",Today.city);
printf("日期 : %s\n",Today.date);
// 其他信息输出...
printf("------------------------------------------------\n");
return 0;
}
这种表格形式的输出使得各项天气数据一目了然。
用户输入阶段
当天天气查询
未来天气查询
资源清理
天气API的URL构造是关键,它决定了我们能获取哪些数据。以当天天气查询为例:
c复制sprintf(URL,"/?app=weather.today&weaid=%s&appkey=78692&sign=4fc12179d774cba5500d9d7c56e92f74&format=json",City);
这个URL包含以下参数:
app=weather.today:指定查询当天天气weaid=城市名:指定查询城市appkey和sign:API认证信息format=json:指定返回JSON格式从HTTP响应中提取JSON数据需要跳过响应头。我们使用字符串查找定位JSON起始位置:
c复制j_start = strstr(tmpbuff, "\r\n\r\n"); // 找到头结束标记
j_start += 4; // 跳过"\r\n\r\n"
for(;*j_start != '{';j_start++); // 找到JSON开始的大括号
这种方法简单但脆弱,更健壮的做法是:
项目中有几处需要注意内存安全:
tmpbuff要足够大(这里用了40KB)strcpy()而非strncpy()cJSON_Delete()在实际项目中,建议:
当前的错误处理还比较基础,主要通过返回值判断和perror()输出错误。更完善的方案应包括:
目前每次查询都新建TCP连接,实际上可以复用连接:
c复制// 第一次查询
sockfd = CreateTcpConnection("103.205.5.206", 80);
SendHttpRequest(sockfd, today_url);
// ...处理响应
// 第二次查询重用sockfd
SendHttpRequest(sockfd, future_url);
// ...处理响应
close(sockfd); // 最后统一关闭
这避免了重复的三次握手过程,显著提升性能。
对于不常变化的天气数据,可以添加缓存机制:
当前实现是同步的,会阻塞用户输入。可以考虑:
当前城市名输入直接使用scanf("%s"),存在安全隐患:
改进方案:
c复制fgets(City, sizeof(City), stdin);
City[strcspn(City, "\n")] = '\0'; // 去除换行符
目前使用HTTP明文传输,建议升级到HTTPS:
代码中硬编码了API密钥和签名,这不利于安全:
可以扩展系统以接收和显示天气预警信息:
不依赖单一API,增加数据源冗余:
添加查询历史天气的功能:
要使代码在Windows上运行,需要修改:
在资源受限的嵌入式系统中:
转换为移动应用的思路:
在Ubuntu/Debian上安装所需依赖:
bash复制sudo apt update
sudo apt install build-essential git
sudo apt install libcjson-dev
项目编译步骤:
bash复制git clone https://github.com/example/weather-cli.git
cd weather-cli
make
./weather
调试网络程序的常用方法:
bash复制sudo tcpdump -i any port 80 -w weather.pcap
天气API通常有调用频率限制,解决方案:
有些API要求城市ID而非名称,需要:
天气数据更新可能有延迟,可以:
当前main函数较长,建议:
添加测试保证代码质量:
好的项目需要完整文档:
相比手动处理HTTP,使用libcurl等库的优势:
但学习底层实现对理解网络原理很有帮助。
除了cJSON,还有其他选择:
选择依据:
除了JSON,还可以考虑:
JSON在可读性和效率间取得了良好平衡。
通过这个项目的开发,我深刻理解了HTTP协议的实际运作方式,而不仅仅是理论上的了解。手动构造HTTP请求让我对协议细节有了更直观的认识,比如为什么头字段后面要有CRLF,为什么需要Host头等等。
在JSON解析过程中,我学会了如何高效地处理树形结构数据,这对我后续处理各种API响应有很大帮助。同时,这个项目也让我更加重视错误处理和内存管理,这些都是C语言项目中容易出问题的地方。
最大的收获是认识到网络编程中"细节决定成败"——一个字节的顺序错误、一个头字段的缺失都可能导致整个请求失败。这种对细节的把握能力是成为一名优秀开发者的关键。