引言
在嵌入式开发或网络编程学习中,通过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;
}
结果展示:

网硕互联帮助中心



评论前必须登录!
注册