# wb2-http-weather **Repository Path**: jrobot_Q_Q/wb2-http-weather ## Basic Information - **Project Name**: wb2-http-weather - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-09-08 - **Last Updated**: 2024-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## WB2-32S HTTP 获取天气数据教程 ### 功能分析 HTTP请求天气数据首先得要有天气数据来源,其次得有网络请求获取到天气数据,天气api接口大多数都是json数据格式,所以还得需要json数据解析 总结一下大概需要以下几个模块 - HTTP网络请求 - 天气数据接口 - JSON数据解析 ### 天气请求接口 天气接口使用的是易客云的免费api [易客云api](http://yiketianqi.com/index) 主要是对个人免费开放,返回的数据是JSON数据格式 7日天气API接口地址为 其中YOU_APPID和YOU_APP_SECRET的具体值可以根据自己申请的账号进行替换(使用我的也可以,用的多了可能就达到次数限制了) 如果需要可以通过上面易客云的连接注册申请账号,登录后会看到自己的appid和appsecret数据,进行替换就可以了,我自己账号替换后的实际地址是: 为了验证接口地址是否正确可以复制替换后的连接到浏览器地址栏粘贴后点击回车看看是否能够得到正确的天气json数据,不出意外就可以看到天气数据了 **天气接口默认会请求网络ip所在城市的天气,如果要请求其它地方的天气可以在接口天气city参数**,例如请求深圳的地址可以在上面的url后面加上&cityid=101280601具体的接口参数信息可以[点击查看接口文档](http://yiketianqi.com/index/doc?version=v91) **什么是JSON数据** JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,常用于web服务之间的数据传输格式,与XML一样是基于纯文本的数据格式,可以被记事本等文本编辑器打开。JSON数据一般由两种格式 - **名称:值** 键值对集合,类似于一个key对应一个value,value可以是简单的String,Int或者是一个JSON对象 - **值的有序列表** 和大部分语言中的数组类似,存放的同一个类型的集合,可以是String,Int或者是JSON对象 详情可以参考[JSON教程](https://liaoxuefeng.com/blogs/all/2008-08-23-json-intro/index.html) ### HTTP HTTP协议(超文本传输协议Hyper Text Transfer Protocol),是基于TCP协议的应用层协议,简单理解就是客户端和服务端传输数据的一种数据传输规则。计算机中的各种协议就是为了解决不同设备之间的数据传输诞生的。 HTTP是无状态协议,即协议本身并不会对发送过的数据和状态进行持久化处理。这样就可以快速处理大量数据,提高效率 一个HTTP请求主要包含以下几个方面, HTTP URL、HTTP Request和HTTP Response - HTTP URL URL包含了用于查找某个资源的详细信息,其格式如下 ``` http://host[:port]/[path] ``` host可以是域名或者ip格式,port是端口号,http默认是80,https默认是443,默http和https的默认端口可以不写,path代表了请求资源的路径,例如: **** - HTTP Resust请求 ![http_request](image/README/http_request.png) 每个请求都包含三部分 1. **请求行:** 说明请求方法,请求的资源路径,以及所使用的协议版本 2. **请求头:** 由一些系列键值对构成,每行只能有一个键值对,并用\r\n结束 3. **请求体:** 只有在post才会由请求体,get没有 请求方法有 GET - 请求指定的资源。 POST - 提交数据以处理请求。 HEAD - 请求资源的响应头信息。 PUT - 上传文件或者更新资源。 DELETE - 删除指定的资源。 OPTIONS - 请求获取服务器支持的请求方法。 TRACE - 回显服务器收到的请求,主要用于诊断。 CONNECT - 建立一个隧道用于代理服务器的通信,通常用于 HTTPS 一般常用的是GET和POST 常见的请求头包括 Host、User-Agent、Accept、Accept-Encoding、Content-Length、Conten-Type等 详情可参考 [http协议](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overview) 一个完整的HTTP请求头如下图所示 ![1725692671994](image/README/http_request_head.png) - HTTP Response response的报文格式和请求报文格式类似 ![http响应报文](image/README/http_报文结构.png) 可以看到和请求报文格式都是分为开始行,响应头,和响应主,最明显的区别是开始行多了一个状态码,用于标识此次请求的状态。常用的是200 OK,30X表示重定向,40X表示资源找不到,50x表示服务器异常,具体可以参考[HTTP状态码](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status) #### 使用浏览器查看请求头(以edge浏览器为例,其它都差不多) 浏览器打开http链接后点击F12即可打开开发者模式,然后找到网络tab ![网络tab](image/README/edge_debug.png) 然后在左侧选择所要查看的请求,依次选择标头->请求标头-> 勾选原始可以查看原始的格式,如下图所示 ![查看请求](image/README/http_报文.png) 现在对HTTP有了大致的理解了,可以把上面的天气接口粘贴到浏览器地址栏然后回车就可以看到请求信息了,浏览器看到的是经处理过的数据,去掉了响应头等信息,根据Content-Type来显示响应的具体内容,比如是html就显示解析加载后的html页面,如果是json等纯文本格式就不经过任何处理直接显示纯文本,我们用的天气接口就属于是纯文本类型的json格式 ### 编写代码 如果还没有搭建wb2环境的可以参考这两篇教程 【Ai-WB2入门篇】搭建windows+eclipse环境 [https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45149&fromuid=13444](https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45149&fromuid=13444) (出处: 物联网开发者社区-安信可论坛) [Ai-WB2] 有手就行,WSL下开发环境搭建 [https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45163&fromuid=13444](https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45163&fromuid=13444) (出处: 物联网开发者社区-安信可论坛) 建议新建工程通过复制其它demo里面的程序,这里我复制了helloworld项目,具体步骤如下 - application\get-started目录下的helloworld项目并修改目录为weather, - 进入weather目录同样修改helloworld为weather - 如果有删除则build_out目录 - 打开Makefile文件修改PROJECT_NAME的值为weather - 编译烧写重启运行 如果上面能正常运行就可以继续了 ### 新建weather_client文件 再main.c文件同级目录新建分别新建weather_client.h和weather.c文件,weather_client.c包含weather_client.h - 首先要定义请求天气的task ``` void http_weather_task(void* fun); ``` - 定义前面提到的接口地址 ``` #define HTTP_HOST "v1.yiketianqi.com" //需要根据自己的账号来修改appid和appsecret的值,我这个可能会达到限制无法使用 #define HTTP_PATH "api?unescape=1&version=v91&appid=11411413&appsecret=ZD9U8Jqt" #define PORT 80 ``` 最后文件如下 - weather_client.h ``` #ifndef _WEATHER_CLIENT_H #define _WEATHER_CLIENT_H #define HTTP_HOST "v1.yiketianqi.com" //需要根据自己的账号来修改appid和appsecret的值,我这个可能会达到限制无法使用 #define HTTP_PATH "api?unescape=1&version=v91&appid=11411413&appsecret=ZD9U8Jqt" #define PORT 80 typedef void(*callback(char* data)); #ifdef __cplusplus extern "C" { #endif /** * http 请求天气数据任务 * @param 回调函数 */ void http_weather_task(void* fun); #ifdef __cplusplus } #endif #endif ``` - weather_client.c ``` #include "weather_client.h" #include "FreeRTOS.h" #include "task.h" void http_weather_task(void* argv) { while(1) { vTaskDelay(1000); } } ``` 这里参考其它demo使用了FreeRTOS代码,所以要先添加了freeRTOS.h的头文件和task.h头文件,freeRTOS相关教程可以参考[FreeRTOS官方教程](https://www.freertos.org/zh-cn-cmn-s/Documentation/01-FreeRTOS-quick-start/01-Beginners-guide/01-RTOS-fundamentals) ### 添加wifi相关依赖 PS: 这里主要是针对新手如果了解怎么去添加依赖可以跳过 sdk目录里面的application都是存放的各种demo有iot、peripherals、protocols、security、storage、system、wifi,都是根据内容进行分类了,见名知意很容易就知道里面大致是什么内容了,需要其它方面学习的可以首先参考到application目录下面查找看看有没有需要的 components下面是各个功能模块的组件,如network、os、platform等 我们首先要添加的就是wifi相关的功能,打开application\protocols\socket\tcp_client\main.c 看名字可以知道这个项目是tcp客户端的项目,而我们的教程HTTP协议也是基于tcp的,所以有很大程度的重合 - 找到include头文件 ``` #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tcp_example.h" ``` 和wifi相关的肯定是需要的,先把wifi相关的头文件复制到我们项目的main.c文件里面 lwip是一个开源的轻量化的TCP/IP协议栈,在嵌入式网络模块经常看到他的身影,wb2也不意外,同样是移植了lwip的网络模块,lwip相关的头文件一样要复制到weather_client.c中 打开tcp_client项目中的Makefile文件,可以看到INCLUDE_COMPONENTS设置了很多依赖的库,特别是wifi、lwip和rtos有关的字段,肯定是我们所需要的,同样要按照格式复制到自己项目中的Makefile中 ``` COMPONENTS_NETWORK := sntp dns_server lwip lwip_dhcpd lwip_netif //后面太多了省略了 INCLUDE_COMPONENTS += freertos_riscv_ram //.....省略...... INCLUDE_COMPONENTS += $(COMPONENTS_NETWORK) ``` 可以看到依赖的network模块有sntp和dns_server,复制到我们的Makefile中 COMPONENTS_NETWORK只是定义了变量,依赖还需要设置 INCLUDE_COMPONENTS += $(COMPONENTS_NETWORK)这样才算完整 +=是Makefile中的赋值操作意思是添加赋值把后面的值添加到原值后类似于文件操作中的append 赋值的结果如下: ``` COMPONENTS_BLSYS := bltime blfdt blmtd bloop loopadc looprt loopset COMPONENTS_VFS := romfs COMPONENTS_NETWORK := sntp dns_server COMPONENTS_WIFI := wifi wifi_manager wifi_hosal INCLUDE_COMPONENTS += freertos_riscv_ram bl602 bl602_std newlibc hosal mbedtls_lts lwip vfs yloop utils cli blog blog_testc coredump INCLUDE_COMPONENTS += $(COMPONENTS_NETWORK) INCLUDE_COMPONENTS += $(COMPONENTS_BLSYS) INCLUDE_COMPONENTS += $(COMPONENTS_VFS) INCLUDE_COMPONENTS += $(PROJECT_NAME) INCLUDE_COMPONENTS += ${COMPONENTS_WIFI} ``` 为了方便区分,这里定义了COMPONENTS_WIFI变量,这样的更加清晰明了 然后开始编译项目,我这里是用的make命令编译,看自己使用的是那种方式... 不出意外的话要出意外了, 错误如下 ``` D:/vscodeProject/aithinker/Ai-Thinker-WB2/components/network/wifi/include/bl60x_fw_api.h:6:10: fatal error: bl_os_private.h: No such file or directory 6 | #include "bl_os_private.h" | ^~~~~~~~~~~~~~~~~ compilation terminated. make[1]: *** [/d/vscodeProject/aithinker/Ai-Thinker-WB2/make_scripts_riscv/component_wrapper.mk:313:mai n.o] 错误 1 make: *** [D:\vscodeProject\aithinker\Ai-Thinker-WB2/make_scripts_riscv/project.mk:579:component-weathe r-build] 错误 2 make: *** 正在等待未完成的任务.... ``` 讲下我的大致思路,然后根据自己的情况自己去尝试解决错误,无论是学什么,特别是学计算机相关的一定不能害怕错误,错误才是能学到经验的最好过程,还能收获解决问题本身的能力,遇到的多了自然就知道是哪里的问题,凭直觉就可以大致定位到问题然后就是消灭问题 PS: 找错误一定要从上往下找,因为编译顺序问题,后面的错误有可能是前面错误引起的,解决了前面的问题后面可能就没有错误了 上面错误信息是**fatal error: bl_os_private.h: No such file or directory**很明显是找不到这个文件了,我们直接去项目中搜索,我这里用的vscode,直接在搜索栏搜索这个文件 ![1725766606987](image/README/bl_os_private.png) 可以看到是有这个文件的,其位置是commponents\bl_os_adapter\include\bl_os_adapter.h 根据这个结构很明显看出来这个文件是属于bl_os_adapter库的,所以我们要在Makefile中加入这个库,我们就加在WIIF变量后面 ``` COMPONENTS_WIFI := wifi wifi_manager wifi_hosal bl_os_adapter ``` 加完后继续make后出现了同样的错误 ``` D:/vscodeProject/aithinker/Ai-Thinker-WB2/components/network/wifi_manager/bl60x_wifi_driver/bl_rx.c:25:10: fatal error: bl_wpa.h: No such file or directory 25 | #include ``` 依然是找不到文件,是不是轻车熟路了! 按照上面方法找到wpa_supplicant并添加依赖后继续make 是不是还发现了找不到文件?没错还是一样的方法找到后添加依赖,重复上面依赖后终于make成功了 #### 连接wifi 查看tcp_client项目中的main.c文件发现里面除了tcp相关的代码就是连接wifi的代码 - main函数 main函数中初始化了tcpip_init()函数并且创建了proc_main_entry任务 - proc_main_entry函数 注册了wifi事件的回调函数,并其开始了hal_wifi_start_firmware_task任务,然后就删除自身任务了 - hal_wifi_start_firmware_task 此任务是lib库里面的,没有源码,看名字也知道是和wifi硬件有关的 - event_cb_wifi_event 这个函数是注册wifi事件回调的,里面有个重要的参数event,就是通过event->code值来确认wifi回调的具体事件,常见的有CODE_WIFI_ON_MGMR_DONE wifi_mgmr库初始化完成,然后就可以利用该库提供的函数进行操作,还有CODE_WIFI_ON_GOT_IP用来通知活取到网络ip了,此时就可以进行联网操作了,我们也是在收到此通知后开始请求天气数据的,还有其它的ap事件,扫描事件等等,根据需求在收到对应事件后进行处理其它操作 最后就是把ROUTER_SSID和ROUTER_PWD替换成你自己的wifi热点了,替换完main.c文件如下 ``` /* * @Author: xuhongv@yeah.net xuhongv@yeah.net * @Date: 2022-10-03 15:02:19 * @LastEditors: xuhongv@yeah.net xuhongv@yeah.net * @LastEditTime: 2022-10-08 14:55:16 * @FilePath: \bl_iot_sdk_for_aithinker\applications\get-started\helloworld\helloworld\main.c * @Description: Hello world */ #include #include #include #include #include #include "bl_sys.h" #include #include #include #include #include #define ROUTER_SSID "emm" #define ROUTER_PWD "1234567890" static wifi_conf_t conf = { .country_code = "CN", }; /** * @brief wifi_sta_connect * wifi station mode connect start * @param ssid * @param password */ static void wifi_sta_connect(char* ssid, char* password) { wifi_interface_t wifi_interface; wifi_interface = wifi_mgmr_sta_enable(); wifi_mgmr_sta_connect(wifi_interface, ssid, password, NULL, NULL, 0, 0); } /** * @brief event_cb_wifi_event * wifi connet ap event Callback function * @param event * @param private_data */ static void event_cb_wifi_event(input_event_t* event, void* private_data) { static char* ssid; static char* password; switch (event->code) { case CODE_WIFI_ON_INIT_DONE: { printf("[APP] [EVT] INIT DONE %lld\r\n", aos_now_ms()); wifi_mgmr_start_background(&conf); } break; case CODE_WIFI_ON_MGMR_DONE: { printf("[APP] [EVT] MGMR DONE %lld\r\n", aos_now_ms()); //_connect_wifi(); wifi_sta_connect(ROUTER_SSID, ROUTER_PWD); } break; case CODE_WIFI_ON_SCAN_DONE: { printf("[APP] [EVT] SCAN Done %lld\r\n", aos_now_ms()); // wifi_mgmr_cli_scanlist(); } break; case CODE_WIFI_ON_DISCONNECT: { printf("[APP] [EVT] disconnect %lld\r\n", aos_now_ms()); } break; case CODE_WIFI_ON_CONNECTING: { printf("[APP] [EVT] Connecting %lld\r\n", aos_now_ms()); } break; case CODE_WIFI_CMD_RECONNECT: { printf("[APP] [EVT] Reconnect %lld\r\n", aos_now_ms()); } break; case CODE_WIFI_ON_CONNECTED: { printf("[APP] [EVT] connected %lld\r\n", aos_now_ms()); } break; case CODE_WIFI_ON_PRE_GOT_IP: { printf("[APP] [EVT] connected %lld\r\n", aos_now_ms()); } break; case CODE_WIFI_ON_GOT_IP: { printf("[APP] [EVT] GOT IP %lld\r\n", aos_now_ms()); printf("[SYS] Memory left is %d Bytes\r\n", xPortGetFreeHeapSize()); //WiFi connection succeeded, create TCP client task xTaskCreate(tcp_client_task, (char*)"tcp_client_task", 1024 * 2, NULL, 16, NULL); } break; case CODE_WIFI_ON_PROV_SSID: { printf("[APP] [EVT] [PROV] [SSID] %lld: %s\r\n", aos_now_ms(), event->value ? (const char*)event->value : "UNKNOWN"); if (ssid) { vPortFree(ssid); ssid = NULL; } ssid = (char*)event->value; } break; case CODE_WIFI_ON_PROV_BSSID: { printf("[APP] [EVT] [PROV] [BSSID] %lld: %s\r\n", aos_now_ms(), event->value ? (const char*)event->value : "UNKNOWN"); if (event->value) { vPortFree((void*)event->value); } } break; case CODE_WIFI_ON_PROV_PASSWD: { printf("[APP] [EVT] [PROV] [PASSWD] %lld: %s\r\n", aos_now_ms(), event->value ? (const char*)event->value : "UNKNOWN"); if (password) { vPortFree(password); password = NULL; } password = (char*)event->value; } break; case CODE_WIFI_ON_PROV_CONNECT: { printf("[APP] [EVT] [PROV] [CONNECT] %lld\r\n", aos_now_ms()); printf("connecting to %s:%s...\r\n", ssid, password); wifi_sta_connect(ssid, password); } break; case CODE_WIFI_ON_PROV_DISCONNECT: { printf("[APP] [EVT] [PROV] [DISCONNECT] %lld\r\n", aos_now_ms()); } break; default: { printf("[APP] [EVT] Unknown code %u, %lld\r\n", event->code, aos_now_ms()); /*nothing*/ } } } static void proc_main_entry(void* pvParameters) { aos_register_event_filter(EV_WIFI, event_cb_wifi_event, NULL); hal_wifi_start_firmware_task(); aos_post_event(EV_WIFI, CODE_WIFI_ON_INIT_DONE, 0); vTaskDelete(NULL); } void main(void) { puts("[OS] Starting weather Stack...\r\n"); tcpip_init(NULL, NULL); puts("[OS] proc_main_entry task...\r\n"); xTaskCreate(proc_main_entry, (char*)"main_entry", 1024, NULL, 15, NULL); } ``` 然后执行编译命令你会出现另一个错误 ``` D:/vscodeProject/aithinker/Ai-Thinker-WB2/applications/get-started/weather-demo/weather/main.c:49:33: error: unknown type name 'input_event_t' 49 | static void event_cb_wifi_event(input_event_t* event, void* private_data) ``` 这次是找不到类型定义了,还是在vscode中搜索结构体,看看能不能找到,点击shift+ctrl+f搜索后如下所示 ![input_event_t](image/README/input_event_t.png) 可以看到是在yloop.h中定义的,找到tcp_client\main.c可以发现它有引入了yloop.h头文件,那我们也同样引入 kernel.h ``` #include #include ``` 最后在include上我们的weather_client.h头文件,并在CODE_WIFI_ON_GOT_IP的回调事件中把tcp_client_task 替换成我们的http_weather_task,这样在获取到ip的时候就能启动我们的http请求任务了,最终Makefile的依赖如下图 ``` COMPONENTS_BLSYS := bltime blfdt blmtd bloop loopadc looprt loopset COMPONENTS_VFS := romfs COMPONENTS_NETWORK := sntp dns_server lwip lwip_dhcpd lwip_netif COMPONENTS_WIFI := wifi wifi_manager wifi_hosal bl_os_adapter wpa_supplicant blcrypto_suite INCLUDE_COMPONENTS += freertos_riscv_ram bl602 bl602_std hosal newlibc mbedtls_lts lwip vfs yloop utils cli blog blog_testc coredump INCLUDE_COMPONENTS += rfparam_adapter_tmp INCLUDE_COMPONENTS += $(COMPONENTS_BLSYS) INCLUDE_COMPONENTS += $(COMPONENTS_VFS) INCLUDE_COMPONENTS += $(COMPONENTS_NETWORK) INCLUDE_COMPONENTS += ${COMPONENTS_WIFI} INCLUDE_COMPONENTS += $(PROJECT_NAME) ``` 最后编译烧录重启后就可以看到连上wifi的log了 ``` IP:192.168.134.211 MASK: 255.255.255.0 Gateway: 192.168.134.210 [lwip] netif status callback IP: 192.168.134.211 MK: 255.255.255.0 GW: 192.168.134.210 ``` ### http请求 前面了解过http是基于tcp协议的,lwip中提供了socket库,通过socket可以创建tcp连接,然后对socket按照http报文格式进行读写就可以实现http请求了 sockest.h文件位置为components\network\lwip\src\include\lwip\sockets.h lwip\apps其实也提供了了http_client.h,大致试过里面的函数,没找到怎么用的,就索性就通过socket自己写了 socket常用的函数有下面几个 ``` //根据指定网络类型协议创建socket,返回socket描述符 int lwip_socket(int domain, int type, int protocol); //sockfd需要连接的socketfd,name是远程socket的地址信息, int lwip_connect(int sockfd, const struct sockaddr *name, socklen_t namelen); //从指定socketfd中读取len字节数据到mem缓存中,返回值是实际读取的字节数 ssize_t lwip_read(int s, void *mem, size_t len); //从dataptr中读取size个数据到socketfd中,返回读取成功的数据数 ssize_t lwip_write(int s, const void *dataptr, size_t size); //获取连接到该socke的client信息 int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen); //把ip和端口信息绑定到指定的socket上 int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen); //监听socket,如果有客户端连接上了该socke就可以通过accpet获取到客户端信息 int lwip_listen(int s, int backlog); //关闭socket int lwip_close(int s); ``` 我们只需要创建socke并连接到天气服务器提供http服务就可以进行读写操作了,由于socket的接口只支持ip形式的地址,所以在使用socket之前需要把域名转换成IP,需要用到lwip\api.h下面的netconn_gethostbyname函数 最终创建socket代码如下 ``` ip_addr_t dns_ip; struct sockaddr_in client_addr; //根据服务器域名获取ip ret = netconn_gethostbyname(HTTP_HOST, &dns_ip); if (ERR_OK != ret) { printf("get host ip error\n"); } //把int32类型的ip转换成十进制点分ip host_ip = ip_ntoa(&dns_ip); printf("host name: %s, ip: %s\n", HTTP_HOST, host_ip); while (1) { if (try_count > 3) { goto end; } //创建socket sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { printf("socket cerate error: %d\n", sock); vTaskDelay(10); try_count++; continue; } //初始化client memset(&client_addr, 0, sizeof(struct sockaddr_in)); //设置socket为ip4协议 client_addr.sin_family = AF_INET; //把"127.0.0.1"点分十进制ip转换成32位的int整数格式 client_addr.sin_addr.s_addr = dns_ip.addr; //设置端口号 client_addr.sin_port = htons(PORT); //连接socket if (connect(sock, (struct sockaddr*)&client_addr, sizeof(client_addr)) != 0) { printf("connect failed!\n"); closesocket(sock); vTaskDelay(10); try_count++; continue; } } ``` connect成功后我们的client客户端就和服务端建立了tcp连接,接下来就该向socket读写http报文格式来实现http请求了 #### 向socket中写如http数据报文 通过前面的了解大概知道了http报文格式,开始行包含内容有描述了请求方法,资源路径和协议版本,协议版本最好使用HTTP/1.0,1.1版本会返回chunked格式,方便解析使用1.0版本就绝对没有chunked格式了 ![报文字符串](image/README/报文字符串.png) 然后通过snprintf函数把域名地址端口等信息进行格式化替换 最终替换的结果如下 ``` "GET http://v1.yiketianqi.com:80/api?unescape=1&version=v91&appid=11411413&appsecret=ZD9U8Jqt HTTP/1.0\r\n" "User-Agent: lwip client\r\n" "Accept: */*\r\n" "Host: v1.yiketianqi.com\r\n" "Connection: Close\r\n" "\r\n" ``` 向socket中写入请求数据 ``` static int create_request_string(const char* host, int port, const char* uri, char* buf, size_t buf_size) { return snprintf(buf, buf_size, HTTPC_REQ_11_PROXY_PORT, host, port, uri, "lwip client", host); } //创建请求数据 len = create_request_string(HTTP_HOST, PORT, HTTP_PATH, send_buf, SEND_BUF_SIZE); //向socket写入请求数据 write(sock, send_buf, len); ``` 最后写入成功后开始从socket中读取数据 ``` do { /** * 每次读取32个字节放入tmp中,不一定够32字节 * 所以用返回值表示读取的字节数 * 如果返回0说明读完了,如果返回-1说明出现异常(读完了继续读也是 返回-1) */ len = recv(sock, tmp, 32, 0); //计算一共读了多少字节 sum_len += len; if (len < 0) { printf("recv error: %d\n", len); break; } if (0 == len) { printf("receive successful!\n"); break; } /** * 从接收的tmp数组中复制len个字节到recv_buf中 * 起始地址+已经读的字节就是需要拼接的地方 * 最后可能只读了1个字节,其余的字节都是之前读的旧数据 * 所以读了多少字节就复制多少字节,不能全部复制 */ strncat(recv_buf, tmp, len); } while (1); ``` 调用strnact把读取tmp中的数据append到recv_buf的参数中数据的长度一定要是实际数据的长度,如果写固定大小很可能造成最后一次复制的数据出现异常 最后可以在创建task的时候传入一个函数指针当作回调函数,当获取到数据的时候就回调此函数 ``` void weather_callback(char *response) { //TODO } //传入回调函数 xTaskCreate(http_weather_task, (char*)"tcp_client_task", 1024 * 2, weather_request_callback, 16, NULL); //处理回调函数 if (NULL != argv) { callback* fun = (callback*)argv; fun(recv_buf); } ``` #### 解析返回数据 在此之前我们需要在Makefile中添加cjson库的依赖,cjson库位置在components\stage\cjson ``` INCLUDE_COMPONENTS += cjson ``` 在我们的项目main.c文件中添加cSJON.h头文件 ``` #include ``` 通过http 请求读取到response后就可以解析response的数据了通过前面个报文格式可以知道response数据和response head中间隔了\r\n\r\n 所以可以通过字符串查找函数strstr()可以获取到response body,也就是我们需要的天气的json字符串 ``` char* body = strstr(response, "\r\n\r\n"); //把response body数据解析成json对象 cJSON* root = cJSON_Parse(body + 4); //如果不是json对象说明返回的内容不对,跳到退出的地方 if (root->type != cJSON_Object) { goto end; } ``` 可以把http返回的数据放到在线json工具进行格式化,方便查看json里面的数据内容 www.json.cn ![json数据](image/README/json数据.png) 可以看到里面data是一个数组,每个数组存放的是一天的天气信息 可以通过root根节点拿到city城市信息,以及data天气数据的数组 ``` //获取city数据 cJSON* city = cJSON_GetObjectItem(root, "city"); //获取到data数组,里面是7天的天气数据数据 cJSON* day_json = cJSON_GetObjectItem(root, "data"); //获取array大小 int days = cJSON_GetArraySize(day_json); printf("days: %d,city: %s\n\n", days, city->valuestring); ``` 最后循环遍历每个数组里面的对象,获取每天的天气信息,结束的时候不能要记得释放内存(前面的response是通过读tcp连接malloc动态申请的内存),完整的处理代码如下 ``` void weather_request_callback(char* response) { /** * 响应头和响应体是通过\r\n分割开的,每一行结尾也是一个\r\n * 用两个\r\n就可以把字符串分开 */ char* body = strstr(response, "\r\n\r\n"); cJSON* root = cJSON_Parse(body + 4); if (root->type != cJSON_Object) { goto end; } //获取city数据 cJSON* city = cJSON_GetObjectItem(root, "city"); //获取到data数组,里面是7天的天气数据数据 cJSON* day_json = cJSON_GetObjectItem(root, "data"); //获取array大小 int days = cJSON_GetArraySize(day_json); printf("days: %d,city: %s\n\n", days, city->valuestring); /* * 遍历获取到date、week、wea、tem数据 * 具体里面有什么数据可以看weather.json里面的内容 */ for (int i = 0;i < days;i++) { //拿到数字里面的对象 cJSON* item = cJSON_GetArrayItem(day_json, i); cJSON* date = cJSON_GetObjectItem(item, "date"); cJSON* week = cJSON_GetObjectItem(item, "week"); cJSON* wea = cJSON_GetObjectItem(item, "wea"); cJSON* tem = cJSON_GetObjectItem(item, "tem"); //天气信息打印串口 printf("date: %s\nweek: %s\nwea: %s\ntem: %s℃\n\n", date->valuestring, week->valuestring, wea->valuestring, tem->valuestring); } end: //释放内存 vPortFree(response); //释放cJSON对象 cJSON_Delete(root); } ``` 运行效果如下 ![运行效果](image/README/运行效果.png) 到这里简单的http请求天气数据就算是完成了,个人水平有限难免有理解不到位或者写错的地方请多多包含,最后完整代码放到gitee上https://gitee.com/jrobot_Q_Q/wb2-http-weather/