天气查询小程序项目报告
天气查询小程序项目报告
一、项目背景
在日常生活中,天气状况对人们的出行、穿衣、活动安排等诸多方面都有着重要影响。为了方便用户快速、准确地获取所需城市的天气信息,我们开发了这款天气查询小程序。该程序基于 C 语言开发,借助第三方天气 API 实现数据获取,通过模块化设计确保各功能高效协同,为用户提供实时天气和未来天气预报查询服务。
二、项目概述
本项目是一款基于 C 语言开发的天气查询小程序,核心功能是通过调用第三方天气 API(api.k780.com),实现城市实时天气和未来天气预报的查询。程序采用 TCP 网络通信方式与 API 服务器进行数据交互,获取 JSON 格式的天气数据后,利用 cJSON 库对数据进行解析,最终以清晰、友好的界面将天气信息展示给用户。
三、项目结构与模块说明
项目整体采用模块化设计,各文件分工明确,共同完成天气查询的全流程操作。具体文件及其核心功能如下表所示:
文件名称 | 核心功能 |
---|---|
main.c | 作为程序入口,负责协调 UI 展示模块与用户输入处理模块的工作,把控程序整体流程 |
show.c | 专注于用户交互界面的展示,通过固定格式的提示信息,引导用户完成操作 |
print.c | 承担用户输入处理任务,获取用户输入的城市名和查询类型,并根据用户选择将请求分发到对应的天气查询函数 |
weather.c | 核心功能模块,整合了网络通信、数据处理与解析等关键功能,是实现天气查询的核心 |
cJSON.c | 提供 JSON 解析库支持,实现 JSON 字符串的解析、字段提取以及内存释放等操作 |
url_encoder.c | 提供字节转十六进制的工具函数,预留 URL 编码支持,为解决城市名含特殊字符的问题做准备 |
四、核心模块实现思路
1. 界面展示模块(show.c
)
功能定位
该模块的主要功能是向用户展示程序的交互界面,清晰告知用户操作步骤,降低用户使用难度,提升用户体验。
关键代码
void showUI() {  printf("---------------------------------------------\n");  printf("|| 天氣查詢小程序 ||\n");  printf("|| 第一步:請輸入你想要查詢的城市名 ||\n");  printf("|| 第二步:請選擇一下查詢類型編號 ||\n");  printf("|| 輸入 1 :查詢該城市當前實時天氣 ||\n");  printf("|| 輸入 2 :查詢該城市未來幾天的天氣預報 ||\n");  printf("---------------------------------------------\n");}
实现思路
通过printf
函数直接打印固定格式的交互提示信息,界面设计简洁明了。提示内容明确划分了操作步骤,先让用户输入查询城市名,再选择查询类型(实时天气或天气预报),使用户能快速理解并掌握操作方法,顺利开启天气查询流程。
2. 用户输入处理模块(print.c
)
功能定位
负责获取用户输入的关键信息(城市名和查询类型),对输入信息进行有效性判断,并根据用户的查询类型选择,将请求分发到对应的天气查询函数,是连接用户与核心查询功能的桥梁。
关键代码
int print() {  char cityname\[1024];   // 读取用户输入的城市名  if (fgets(cityname, sizeof(cityname), stdin) == NULL) {  printf("输入错误!\n");  return -1;  }  cityname\[strcspn(cityname, "\n")] = '\0'; // 去除换行符  // 读取用户选择的查询类型  char input\[32]={0};  scanf("%s",input);  // 根据选择调用对应函数  if(strcmp(input,"1")==0) {  Real\_time\_weather(cityname); // 调用实时天气查询函数  } else if(strcmp(input,"2")==0) {  weather\_forecast(cityname); // 调用天气预报查询函数  } else {  printf("無效命令,請重新輸入\n");  print(); // 递归处理无效输入,重新获取用户输入  }  return 0;}
实现思路
该模块分两步获取用户输入:首先使用fgets
函数读取用户输入的城市名,fgets
函数支持读取包含空格的字符串,能满足用户输入各类城市名的需求,同时通过strcspn
函数去除输入字符串中的换行符,保证城市名的准确性;接着使用scanf
函数读取用户选择的查询类型。
在获取用户输入的查询类型后,通过字符串比较函数strcmp
判断用户选择。若用户输入 “1”,则调用实时天气查询函数Real_time_weather
;若输入 “2”,则调用天气预报查询函数weather_forecast
;若输入其他无效内容,会提示用户输入错误,并通过递归调用自身,重新获取用户输入,确保程序能正常响应用户的有效请求,保证操作流程的连续性。
3. 网络通信模块(weather.c
)
功能定位
该模块是程序与第三方天气 API 服务器进行数据交互的核心,主要实现 TCP 连接建立、HTTP 请求发送以及服务器响应接收等功能,为获取天气数据提供网络支持。
(1)TCP 连接建立
关键代码
int create\_tcp\_connect() {  int sockfd=socket(AF\_INET,SOCK\_STREAM,0); // 创建TCP套接字  if(sockfd<0) { perror("socket error"); return -1; }  struct sockaddr\_in seraddr;  seraddr.sin\_family=AF\_INET;  seraddr.sin\_port=htons(SER\_PORT); // 服务器端口(需提前定义,如80)  seraddr.sin\_addr.s\_addr=inet\_addr(SER\_IP); // 服务器IP(api.k780.com的IP地址)  int ret=connect(sockfd,(struct sockaddr \*)\&seraddr,sizeof(seraddr)); // 与服务器建立连接  if(ret<0) { perror("connect error"); return -1; }  return sockfd;}
实现思路
首先调用socket
函数创建 TCP 套接字,socket
函数的参数AF_INET
指定使用 IPv4 地址族,SOCK_STREAM
表示创建面向连接的 TCP 套接字,0
表示使用默认的协议。若套接字创建失败,通过perror
函数输出错误信息并返回 - 1。
接着定义struct sockaddr_in
类型的结构体seraddr
,用于存储服务器的地址信息。其中sin_family
设置为AF_INET
,与套接字的地址族保持一致;sin_port
通过htons
函数将主机字节序的端口号转换为网络字节序;sin_addr.s_addr
通过inet_addr
函数将点分十进制的 IP 地址转换为二进制整数形式的网络字节序 IP 地址。
最后调用connect
函数,将创建的套接字与服务器的地址信息进行绑定,实现与天气 API 服务器的 TCP 连接。若连接失败,同样输出错误信息并返回 - 1;连接成功则返回创建的套接字文件描述符,为后续的 HTTP 请求发送做好准备。
(2)HTTP 请求发送
实时天气查询和天气预报查询分别对应两个不同的 HTTP 请求发送函数,即RLsend_http_request
和FTsend_http_request
,以下以实时天气查询的 HTTP 请求发送函数为例进行说明。
关键代码
int RLsend\_http\_request(int sockfd,char \*cityname) {  // 构造HTTP GET请求的前缀部分,包含API的基础路径和部分固定参数  const char \*prefix = "GET /?app=weather.today\&cityNm=";  // 构造HTTP GET请求的后缀部分,包含API的appkey、签名、数据格式以及HTTP请求头信息  const char \*suffix = "\&appkey=77271\&sign=xxx\&format=json HTTP/1.1\r\n"  "Host: api.k780.com\r\n"  "User-Agent: Mozilla/5.0\r\n"  "Connection: close\r\n\r\n";  // 计算完整HTTP请求的长度  size\_t total\_len = strlen(prefix) + strlen(cityname) + strlen(suffix) + 1;  // 动态分配内存存储完整的HTTP请求  char \*preq = (char \*)malloc(total\_len);  // 拼接前缀、城市名和后缀,生成完整的HTTP请求  snprintf(preq, total\_len, "%s%s%s", prefix, cityname, suffix);  // 发送HTTP请求到服务器  send(sockfd, preq, strlen(preq), 0);  // 释放动态分配的内存,避免内存泄漏  free(preq);  return 0;}
实现思路
HTTP 请求采用 GET 方法,按照 HTTP 协议的规范构造请求内容。请求内容分为前缀、城市名和后缀三部分:前缀包含 API 的基础访问路径和表示实时天气查询的参数app=weather.today
以及城市名参数的键cityNm
;后缀包含 API 所需的appkey
(用于身份验证)、sign
(签名信息,确保请求的合法性)、数据格式参数format=json
(指定返回数据为 JSON 格式),同时还包含 HTTP 请求头信息,如Host
(指定服务器域名)、User-Agent
(模拟浏览器发送请求)、Connection: close
(表示请求完成后关闭连接)。
通过strlen
函数分别计算前缀、城市名和后缀的长度,进而确定完整 HTTP 请求的总长度。然后使用malloc
函数动态分配内存,存储完整的 HTTP 请求。利用snprintf
函数将前缀、城市名和后缀拼接起来,生成符合要求的完整 HTTP 请求字符串。最后调用send
函数,将构造好的 HTTP 请求通过之前建立的 TCP 套接字发送到天气 API 服务器,完成天气数据查询请求的提交。请求发送完成后,及时使用free
函数释放动态分配的内存,避免出现内存泄漏问题。
(3)响应接收与处理
关键代码
int recv\_http\_response(int sockfd) {  // 创建临时文件,用于存储服务器返回的HTTP响应  int fd=open("./temporaryfile.txt",O\_RDWR|O\_CREAT|O\_TRUNC,0664);  char buff\[10240] = {0};  while (1) {  // 接收服务器返回的响应数据  ssize\_t cnt = recv(sockfd, buff, sizeof(buff), 0);  // 若接收数据长度小于等于0,说明数据接收完成或出现错误,退出循环  if (cnt <= 0) break;  // 将接收的数据写入临时文件  write(fd, buff, cnt);  }  // 关闭临时文件  close(fd);  return 0;}
实现思路
首先调用open
函数创建一个临时文件./temporaryfile.txt
,open
函数的参数O_RDWR
表示以读写方式打开文件,O_CREAT
表示若文件不存在则创建,O_TRUNC
表示若文件已存在则清空文件内容,0664
表示设置文件的权限为所有者可读可写,组用户和其他用户可读。
然后定义一个大小为 10240 字节的缓冲区buff
,用于临时存储接收的服务器响应数据。通过while
循环不断调用recv
函数从 TCP 套接字中接收服务器返回的响应数据,每次接收的数据存储到缓冲区buff
中。若recv
函数返回的接收数据长度cnt
小于等于 0,说明服务器响应数据已接收完成或接收过程中出现错误,此时退出循环。
在每次成功接收数据后,调用write
函数将缓冲区buff
中的数据写入到创建的临时文件中。待数据接收完成后,调用close
函数关闭临时文件,完成服务器响应数据的接收与存储,为后续从响应数据中提取 JSON 格式的天气数据做好准备。
4. 数据处理模块(weather.c
)
功能定位
从服务器返回的 HTTP 响应中提取出纯 JSON 格式的天气数据,去除 HTTP 响应头信息等无关内容,为后续的 JSON 数据解析提供干净、规范的数据来源。
关键代码
int filemodify() {  // 打开存储原始HTTP响应的临时文件  FILE \*op = fopen("./temporaryfile.txt", "r");  // 创建新文件,用于存储提取出的纯JSON数据  FILE \*cg = fopen("./file.txt", "w+");  int c;  int found = 0; // 标记是否找到JSON数据的起始位置(第一个'{')  int brace\_count = 0; // 跟踪JSON数据中花括号的嵌套层级,确保提取完整的JSON数据  while ((c = fgetc(op)) != EOF) {  if (found) {  // 若已找到JSON起始位置,将当前字符写入新文件  fputc(c, cg);  // 统计花括号的数量,用于判断JSON数据的结束位置  if (c == '{') brace\_count++;  else if (c == '}') brace\_count--;  // 当花括号嵌套层级为0时,说明已找到JSON数据的结束位置,退出循环  if (brace\_count == 0) break;  } else if (c == '{') {  // 找到JSON数据的起始位置,标记并开始写入  found = 1;  fputc(c, cg);  brace\_count = 1;  }  }  // 关闭文件  fclose(op);  fclose(cg);  return 0;}
实现思路
首先调用fopen
函数分别打开存储原始 HTTP 响应的临时文件./temporaryfile.txt
(只读模式)和用于存储纯 JSON 数据的新文件./file.txt
(读写模式,若文件不存在则创建)。
定义变量c
用于存储从文件中读取的单个字符,found
作为标记变量,初始值为 0,用于标识是否找到 JSON 数据的起始位置(即第一个 ‘{’),brace_count
用于跟踪 JSON 数据中花括号的嵌套层级,确保能准确提取出完整的 JSON 数据。
通过while
循环调用fgetc
函数从原始 HTTP 响应文件中逐个读取字符,直到读取到文件结束符(EOF)。在循环过程中,若found
为 0(尚未找到 JSON 起始位置),则判断当前读取的字符是否为 ‘{’,若为 ‘{’,则将found
设为 1,标记已找到 JSON 起始位置,将该字符写入新文件,并将brace_count
设为 1(表示当前处于一层花括号嵌套中)。
若found
为 1(已找到 JSON 起始位置),则将当前读取的字符写入新文件,并根据字符是否为 ‘{’ 或 ‘}’ 来更新brace_count
:遇到 ‘{’ 则brace_count
加 1,遇到 ‘}’ 则brace_count
减 1。当brace_count
减至 0 时,说明已读取到 JSON 数据的最后一个 ‘}’,即 JSON 数据提取完成,此时退出循环。
最后调用fclose
函数关闭两个文件,完成从 HTTP 响应中提取纯 JSON 数据的操作,生成的./file.txt
文件中仅包含 JSON 格式的天气数据,可直接用于后续的解析处理。
5. JSON 解析模块(weather.c
+ cJSON.c
)
功能定位
利用 cJSON 库提供的功能,对提取出的纯 JSON 格式天气数据进行解析,提取出用户所需的天气信息(如城市名、日期、温度、天气状况等),并以合适的方式展示给用户。该模块分为实时天气解析和天气预报解析两部分。
(1)实时天气解析
关键代码
void parse\_weather\_json(const char \*json\_str) {  // 解析JSON字符串,生成JSON对象  cJSON \*root = cJSON\_Parse(json\_str);  if (root == NULL) {  // 若解析失败,输出错误信息  printf("JSON解析失败: %s\n", cJSON\_GetErrorPtr());  return;  }  // 提取success字段,判断API请求是否成功  cJSON \*success = cJSON\_GetObjectItem(root, "success");  if (success && cJSON\_IsString(success)) {  printf("请求结果: %s\n", success->valuestring);  }  // 提取result对象,该对象中包含具体的天气信息  cJSON \*result = cJSON\_GetObjectItem(root, "result");  if (result && cJSON\_IsObject(result)) {  // 提取城市名字段  cJSON \*citynm = cJSON\_GetObjectItem(result, "citynm");  if (citynm && cJSON\_IsString(citynm)) {  printf("城市: %s\n", citynm->valuestring);  }  // 提取日期字段  cJSON \*date = cJSON\_GetObjectItem(result, "date");  if (date && cJSON\_IsString(date)) {  printf("日期: %s\n", date->valuestring);  }  // 提取温度字段  cJSON \*temp = cJSON\_GetObjectItem(result, "temp");  if (temp && cJSON\_IsString(temp)) {  printf("温度: %s\n", temp->valuestring);  }  // 提取天气状况字段(可根据实际API返回字段扩展其他天气信息)  cJSON \*weather = cJSON\_GetObjectItem(result, "weather");  if (weather && cJSON\_IsString(weather)) {  printf("天气状况: %s\n", weather->valuestring);  }  // 省略其他字段(如风力、湿度等)的提取...  }  // 释放JSON对象占用的内存,避免内存泄漏  cJSON\_Delete(root);}
实现思路
首先调用 cJSON 库中的cJSON_Parse
函数,将传入的纯 JSON 字符串json_str
解析为一个 cJSON 类型的根对象root
。若解析失败(root
为 NULL),则通过cJSON_GetErrorPtr
函数获取解析错误的位置信息,并输出错误提示,随后返回,终止解析过程。
解析成功后,首先提取 JSON 数据中的success
字段,该字段用于判断 API 请求是否成功。通过cJSON_GetObjectItem
函数从根对象root
中获取success
字段对应的 cJSON 对象,然后使用cJSON_IsString
函数判断该字段的值是否为字符串类型。若字段存在且为字符串类型,则输出请求结果(如 “success” 表示请求成功)。
接着提取result
字段,该字段是一个 JSON 对象,包含了具体的天气信息(如城市名、日期、温度等)。同样通过cJSON_GetObjectItem
函数获取result
对象,并使用cJSON_IsObject
函数判断其是否为对象类型。若result
对象存在且为对象类型,则进一步从该对象中提取各个具体的天气信息字段,如城市名(citynm
)、日期(date
)、温度(temp
)、天气状况(weather
)等。
对于每个具体的天气信息字段,都先通过cJSON_GetObjectItem
函数从result
对象中获取对应的 cJSON 对象,再判断该对象的值是否为预期的数据类型(如字符串类型),若满足条件,则输出该字段的名称和对应的值,将天气信息展示给用户。
最后,调用cJSON_Delete
函数释放根对象root
及其下属所有 cJSON 对象占用的内存,避免出现内存泄漏问题,确保程序的内存使用安全。
(2)天气预报解析
关键代码
// 定义结构体,用于存储每天的天气预报信息typedef struct {  char days\[20]; // 日期  char week\[10]; // 星期  char citynm\[20]; // 城市名称  char weather\[20]; // 天气状况  int temp\_high; // 最高温度  int temp\_low; // 最低温度  // 可根据实际API返回字段扩展其他信息,如风力、湿度等} WeatherInfo;// 解析多天天气预报的JSON数据,返回存储天气信息的结构体数组WeatherInfo\* parse\_weather\_jsonFT(const char\* json\_str, int\* count) {  // 解析JSON字符串,生成根对象  cJSON\* root = cJSON\_Parse(json\_str);  if (!root) {  printf("JSON解析失败: %s\n", cJSON\_GetErrorPtr());  return NULL;  }  // 提取result数组,该数组包含多天的天气预报信息  cJSON\* result = cJSON\_GetObjectItem(root, "result");  if (!result || !cJSON\_IsArray(result)) {  // 若result字段不存在或不是数组类型,释放内存并返回NULL  cJSON\_Delete(root);  return NULL;  }  // 获取天气预报的天数(数组长度)  \*count = cJSON\_GetArraySize(result);  // 动态分配内存,存储多天的天气预报信息  WeatherInfo\* weather\_data = malloc(\*count \* sizeof(WeatherInfo));  if (!weather\_data) {  printf("内存分配失败\n");  cJSON\_Delete(root);  return NULL;  }  // 遍历result数组,提取每天的天气预报信息  for (int i = 0; i < \*count; i++) {  // 获取数组中的第i个元素(每天的天气信息对象)  cJSON\* item = cJSON\_GetArrayItem(result, i);  if (!item || !cJSON\_IsObject(item)) {  continue; // 若当前元素无效,跳过继续处理下一个  }  // 提取日期字段,并存储到结构体中  cJSON\* days = cJSON\_GetObjectItem(item, "days");  if (days && cJSON\_IsString(days)) {  strncpy(weather\_data\[i].days, days->valuestring, sizeof(weather\_data\[i].days)-1);  }  // 提取星期字段  cJSON\* week = cJSON\_GetObjectItem(item, "week");  if (week && cJSON\_IsString(week)) {  strncpy(weather\_data\[i].week, week->valuestring, sizeof(weather\_data\[i].week)-1);  }  // 提取城市名字段  cJSON\* citynm = cJSON\_GetObjectItem(item, "citynm");  if (citynm && cJSON\_IsString(citynm)) {  strncpy(weather\_data\[i].citynm, citynm->valuestring, sizeof(weather\_data\[i].citynm)-1);  }  // 提取天气状况字段  cJSON\* weather = cJSON\_GetObjectItem(item, "weather");  if (weather && cJSON\_IsString(weather)) {  strncpy(weather\_data\[i].weather, weather->valuestring, sizeof(weather\_data\[i].weather)-1);  }  // 提取最高温度字段(假设为数值类型)  cJSON\* temp\_high = cJSON\_GetObjectItem(item, "temp\_high");  if (temp\_high && cJSON\_IsNumber(temp\_high)) {  weather\_data\[i].temp\_high = temp\_high->valueint;  }  // 提取最低温度字段(假设为数值类型)  cJSON\* temp\_low = cJSON\_GetObjectItem(item, "temp\_low");  if (temp\_low && cJSON\_IsNumber(temp\_low)) {  weather\_data\[i].temp\_low = temp\_low->valueint;  }  // 省略其他字段(如风力、湿度等)的提取...  }  // 释放根对象内存  cJSON\_Delete(root);  // 返回存储天气预报信息的结构体数组  return weather\_data;}
实现思路
首先定义WeatherInfo
结构体,用于存储每天的天气预报信息,结构体中的字段根据 API 返回的天气预报数据内容进行定义,包括日期(days
)、星期(week
)、城市名(citynm
)、天气状况(weather
)、最高温度(temp_high
)、最低温度(temp_low
)等,可根据实际需求扩展其他字段。
在parse_weather_jsonFT
函数中,首先调用cJSON_Parse
函数解析传入的 JSON 字符串,生成根对象root
。若解析失败,输出错误信息并返回 NULL。接着提取result
字段,该字段在天气预报的 JSON 数据中是一个数组类型,包含了多天的天气预报信息。通过cJSON_GetObjectItem
函数获取result
对象后,使用cJSON_IsArray
函数判断其是否为数组类型,若不是数组类型或result
对象不存在,则释放根对象内存并返回 NULL。
获取result
数组的长度(即天气预报的天数),并通过malloc
函数动态分配内存,创建一个WeatherInfo
类型的结构体数组weather_data
,用于存储多天的天气预报信息。若内存分配失败,输出错误信息,释放根对象内存并返回 NULL。
然后通过for
循环遍历result
数组,循环次数为数组的长度。在每次循环中,调用cJSON_GetArrayItem
函数获取数组中的第i
个元素(即当天的天气预报信息对象),并判断该元素是否为有效的对象类型。若元素无效,则跳过当前循环,继续处理下一个元素。
对于有效的当天天气预报信息对象,逐个提取其中的字段(如日期、星期、城市名、天气状况、最高温度、最低温度等),并将提取到的值存储到weather_data
结构体数组的对应位置。在提取字段时,先通过cJSON_GetObjectItem
函数获取对应的 cJSON 对象,再根据字段的数据类型(如字符串类型、数值类型)使用相应的判断函数(cJSON_IsString
、cJSON_IsNumber
)进行类型判断,确保数据类型正确后,将值赋给结构体中的对应字段。例如,对于字符串类型的字段(如日期、星期),使用strncpy
函数将字符串值复制到结构体的字符数组中;对于数值类型的字段(如最高温度、最低温度),直接获取其数值并赋给结构体中的整型变量。
循环结束后,调用cJSON_Delete
函数释放根对象root
占用的内存,最后返回存储了多天天气预报信息的结构体数组weather_data
。在后续的代码中,可以遍历该结构体数组,将每天的天气预报信息以清晰、直观的方式展示给用户。
6. 工具支持模块(cJSON.c
与url_encoder.c
)
(1)cJSON.c
功能定位
cJSON 库是一个轻量级的 JSON 解析库,为整个项目的 JSON 数据解析提供核心支持。该库实现了 JSON 数据的解析、生成、字段提取、内存管理等功能,是连接原始 JSON 数据与用户可读天气信息的关键工具。
核心功能与作用
-
JSON 解析:提供
cJSON_Parse
函数,能够将 JSON 格式的字符串解析为 cJSON 类型的对象树,方便后续对 JSON 数据的操作和字段提取。 -
字段提取:通过
cJSON_GetObjectItem
函数可以从指定的 cJSON 对象中根据字段名提取对应的子对象,支持对嵌套 JSON 对象的字段提取;cJSON_GetArrayItem
函数可从 cJSON 数组对象中根据索引提取对应的元素,满足天气预报数组数据的解析需求。 -
数据类型判断:提供一系列数据类型判断函数,如
cJSON_IsString
(判断是否为字符串类型)、cJSON_IsNumber
(判断是否为数值类型)、cJSON_IsObject
(判断是否为对象类型)、cJSON_IsArray
(判断是否为数组类型)等,确保在提取字段值时能正确处理不同数据类型的数据,避免类型错误。 -
内存管理:提供
cJSON_Delete
函数,用于释放 cJSON 对象及其下属所有对象占用的内存,有效避免内存泄漏问题,保证程序的内存使用安全。
(2)url_encoder.c
功能定位
该模块提供字节转十六进制的工具函数byte_to_hex
,主要用于将字符转换为%XX
格式的十六进制字符串(即 URL 编码格式),预留支持 URL 编码功能。在实际应用中,用户输入的城市名可能包含空格、中文、特殊符号等字符,这些字符直接拼接到 HTTP 请求 URL 中会导致请求格式错误,服务器无法正确解析请求。通过 URL 编码,可以将这些特殊字符转换为符合 HTTP 协议规范的编码格式,确保 HTTP 请求的合法性和正确性。
关键代码(示例)
// 将单个字节转换为%XX格式的十六进制字符串void byte\_to\_hex(unsigned char byte, char \*hex) {  // 定义十六进制字符集  const char hex\_chars\[] = "0123456789ABCDEF";  // 第一个字符为%  hex\[0] = '%';  // 第二个字符为字节高4位对应的十六进制字符  hex\[1] = hex\_chars\[byte >> 4];  // 第三个字符为字节低4位对应的十六进制字符  hex\[2] = hex\_chars\[byte & 0x0F];  // 添加字符串结束符  hex\[3] = '\0';}
实现思路
byte_to_hex
函数接收一个无符号字符类型的字节(byte
)和一个字符指针(hex
),用于存储转换后的%XX
格式字符串。首先定义包含 0-9、A-F 的十六进制字符集hex_chars
。然后将hex
字符串的第一个字符设为 ‘%’,表示 URL 编码的起始标志。接着,将输入字节byte
右移 4 位,得到字节的高 4 位,通过该值作为索引从hex_chars
中获取对应的十六进制字符,赋值给hex
的第二个字符;再将输入字节byte
与 0x0F 进行按位与运算,得到字节的低 4 位,同样通过索引从hex_chars
中获取对应的十六进制字符,赋值给hex
的第三个字符。最后在hex
的第四个位置添加字符串结束符 ‘\0’,完成单个字节到%XX
格式十六进制字符串的转换。
在后续项目优化中,可以基于该函数扩展实现完整的 URL 编码功能,对用户输入的城市名进行 URL 编码处理后,再拼接到 HTTP 请求 URL 中,解决城市名含特殊字符导致的请求失败问题,提升程序的兼容性和稳定性。
五、整体流程总结
天气查询小程序的整体运行流程清晰,各模块之间协同工作,具体流程如下:
-
程序启动与界面展示:程序从
main.c
的main
函数开始执行,main
函数首先调用show.c
中的showUI
函数,在控制台展示天气查询小程序的交互界面,清晰告知用户操作步骤(输入城市名→选择查询类型)。 -
用户输入获取与请求分发:
main
函数调用print.c
中的print
函数,该函数先通过fgets
获取用户输入的城市名,去除换行符后,再通过scanf
获取用户选择的查询类型(1 表示实时天气,2 表示天气预报)。根据用户的查询类型选择,print
函数调用weather.c
中对应的天气查询函数(Real_time_weather
或weather_forecast
),将用户输入的城市名作为参数传递给这些函数。 -
TCP 连接建立:在
Real_time_weather
或weather_forecast
函数中,首先调用weather.c
中的create_tcp_connect
函数,创建 TCP 套接字并与天气 API 服务器(api.k780.com)建立 TCP 连接。若连接成功,返回套接字文件描述符;若连接失败,输出错误信息并终止当前查询流程。 -
HTTP 请求发送:根据查询类型的不同,分别调用
weather.c
中的RLsend_http_request
(实时天气)或FTsend_http_request
(天气预报)函数,构造包含城市名、API 密钥、数据格式等参数的 HTTP GET 请求。将构造好的 HTTP 请求通过之前建立的 TCP 套接字发送到 API 服务器,请求获取对应的天气数据。 -
服务器响应接收与存储:HTTP 请求发送完成后,调用
weather.c
中的recv_http_response
函数,通过 TCP 套接字接收服务器返回的 HTTP 响应数据,并将响应数据写入临时文件./temporaryfile.txt
中,完成响应数据的接收与存储。 -
JSON 数据提取:调用
weather.c
中的filemodify
函数,打开临时文件./temporaryfile.txt
,读取其中的 HTTP 响应数据,通过识别 JSON 数据的起始标志(‘{’)和结束标志(‘}’),提取出纯 JSON 格式的天气数据,并将其写入新文件./file.txt
中,去除 HTTP 响应头信息等无关内容。 -
JSON 数据解析与结果展示:读取
./file.txt
中的纯 JSON 数据,根据查询类型调用对应的 JSON 解析函数:
-
若为实时天气查询,调用
parse_weather_json
函数,解析 JSON 数据,提取城市名、日期、温度、天气状况等实时天气信息,并在控制台打印展示给用户。 -
若为天气预报查询,调用
parse_weather_jsonFT
函数,解析 JSON 数据,将多天的天气预报信息存储到WeatherInfo
结构体数组中,然后遍历该数组,在控制台打印每天的日期、星期、城市名、天气状况、最高温度、最低温度等信息,展示给用户。
- 资源释放与程序结束:在 JSON 数据解析完成后,调用
cJSON_Delete
函数释放 cJSON 对象占用的内存,关闭相关文件,关闭 TCP 套接字,释放程序运行过程中占用的各类资源。若用户需要继续查询天气,可重新执行上述流程;若用户选择退出程序,则程序正常结束。
六、改进方向
虽然当前天气查询小程序已实现核心的天气查询功能,但在功能完整性、健壮性、用户体验等方面仍有较大的改进空间,具体改进方向如下:
1. 补充 URL 编码功能
当前程序在构造 HTTP 请求时,直接将用户输入的城市名拼接到 URL 中,未进行 URL 编码处理。当用户输入的城市名包含空格(如 “New York”)、中文(如 “北京”)、特殊符号(如 “@”“#” 等)时,会导致 HTTP 请求 URL 格式不合法,服务器无法正确解析请求,从而查询失败。
改进方案:基于url_encoder.c
中已有的byte_to_hex
函数,扩展实现完整的 URL 编码功能。在获取用户输入的城市名后,先对城市名字符串进行 URL 编码处理,将特殊字符转换为%XX
格式的编码字符串,再将编码后的城市名拼接到 HTTP 请求 URL 中。具体实现时,需遍历城市名字符串的每个字符,判断字符是否为 ASCII 可打印字符且不属于特殊字符,若属于特殊字符或非 ASCII 字符(如中文),则将其转换为对应的字节(对于中文需考虑编码格式,如 UTF-8),再调用byte_to_hex
函数进行编码,最终生成符合 HTTP 协议规范的 URL 编码城市名。
2. 错误处理优化
当前程序的错误处理机制较为简单,仅在部分关键步骤(如 TCP 连接建立、JSON 解析)中进行了错误提示,缺乏对多种异常情况的全面处理,如网络超时、API 返回错误(如城市不存在、API 密钥过期、请求频率超限等)、文件操作失败(如文件创建失败、文件读写错误等)等,导致程序在遇到这些异常情况时,可能出现崩溃、无响应或输出不明确错误信息等问题,影响用户体验和程序稳定性。
改进方案:
-
网络超时处理:在 TCP 连接建立(
connect
函数)和数据接收(recv
函数)过程中,设置超时时间。通过setsockopt
函数设置套接字的SO_RCVTIMEO
(接收超时)和SO_SNDTIMEO
(发送超时)选项,当连接或数据传输超时后,及时返回错误信息,并提示用户检查网络连接或稍后重试。 -
API 错误处理:在解析 JSON 数据时,除了判断
success
字段外,还需提取 API 返回的错误码(error_code
)和错误信息(error_msg
)字段(具体字段名需参考 API 文档)。当success
字段为 “fail” 或其他表示失败的值时,根据错误码和错误信息输出具体的错误原因,如 “城市不存在,请重新输入”“API 密钥过期,请联系管理员”“请求频率过高,请稍后再试” 等,帮助用户快速定位问题。 -
文件操作错误处理:在文件创建(
open
函数)、文件读写(read
、write
、fread
、fwrite
等函数)过程中,增加返回值判断。若文件操作失败,通过perror
函数输出详细的错误信息(如 “文件创建失败:权限不足”“文件读写错误:设备无空间” 等),并进行相应的错误恢复处理,如释放已分配的资源、提示用户检查文件系统状态等,避免程序因文件操作错误而崩溃。
3. 用户体验提升
当前程序的用户体验较为基础,仅能通过控制台进行简单的交互和结果展示,缺乏一些便捷、人性化的功能,如支持连续查询、天气信息可视化展示、历史查询记录保存等。
改进方案:
-
支持连续查询:在当前查询流程结束后,提示用户是否继续查询其他城市的天气或其他类型的天气信息(实时 / 预报)。若用户选择继续查询,则重新调用
showUI
函数展示界面,并获取用户新的输入;若用户选择退出,则正常结束程序。避免用户每次查询都需要重新启动程序,提升操作便捷性。 -
天气信息可视化展示:丰富天气信息的展示形式,除了文字描述外,可添加简单的天气图标(如用 “☀️” 表示晴天、“🌧️” 表示雨天、“❄️” 表示雪天等),通过控制台输出对应的图标,使天气状况更加直观易懂。同时,可对输出的天气信息进行格式化排版,如使用分隔线、对齐方式等,提升信息的可读性。
-
历史查询记录保存:增加历史查询记录功能,将用户每次查询的城市名、查询类型(实时 / 预报)、查询时间、天气信息等数据保存到本地文件(如
history.txt
)中。提供查看历史记录的功能,用户可通过输入指定命令(如 “3” 表示查看历史记录),程序读取历史记录文件并将历史查询信息展示给用户,方便用户回顾之前的查询结果。 -
添加天气提示功能:根据解析出的天气信息,增加相应的生活提示,如温度较低时提示 “天气寒冷,请注意添衣保暖”;有雨时提示 “今日有雨,建议携带雨具”;紫外线较强时提示 “紫外线强烈,请注意防晒” 等,为用户提供更贴心的服务。
4. 功能扩展
除了当前的实时天气和未来天气预报查询功能外,还可以根据用户需求和 API 提供的能力,扩展更多实用的天气相关功能。
改进方案:
-
添加生活指数查询:许多天气 API 会提供生活指数相关数据,如紫外线指数、空气质量指数(AQI)、体感温度、降水概率、洗车指数、穿衣指数等。可扩展程序功能,增加生活指数查询选项(如用户输入 “3” 表示查询生活指数),在解析 JSON 数据时提取对应的生活指数字段,并展示给用户,为用户的日常生活提供更全面的参考。
-
支持多城市同时查询:允许用户一次性输入多个城市名(如用逗号或空格分隔),程序依次对每个城市进行天气查询,并将每个城市的天气信息分别展示给用户,满足用户同时了解多个城市天气状况的需求。
-
定时查询与提醒:增加定时查询功能,用户可设置查询时间(如每天早上 8 点)和查询城市、查询类型,程序在指定时间自动执行天气查询,并将查询结果以弹窗、邮件或短信(需结合相关服务)等方式提醒用户,实现天气信息的主动推送。
七、项目总结
本天气查询小程序基于 C 语言开发,采用模块化设计思想,将程序划分为界面展示、用户输入处理、网络通信、数据处理、JSON 解析、工具支持等多个核心模块,各模块职责明确、协同高效,共同实现了城市实时天气和未来天气预报的查询功能。
在技术实现上,程序通过 TCP 网络通信与第三方天气 API 服务器建立连接,发送 HTTP GET 请求获取 JSON 格式的天气数据;利用 cJSON 库对 JSON 数据进行解析,提取用户所需的天气信息;通过控制台实现用户交互和结果展示,整体技术路线清晰、可行。
然而,程序在当前阶段仍存在一些不足之处,如缺乏 URL 编码支持导致无法处理特殊字符城市名、错误处理机制不完善、用户体验有待提升以及功能较为单一等。通过后续的改进优化,如补充 URL 编码功能、完善错误处理、提升用户体验以及扩展更多实用功能,可进一步增强程序的稳定性、兼容性和实用性,为用户提供更优质的天气查询服务。
总体而言,本项目成功实现了天气查询的核心功能,验证了基于 C 语言进行网络通信、JSON 解析等开发工作的可行性,为后续相关项目的开发积累了宝贵的经验。