本文参考Esp8266学习之旅⑤ 8266原生乐鑫SDK高级使用之封装Post与Get请求云端,拿到“天气预报信息”
当然最后肯定不是拿天气预报这么简单啦
一、预备知识点
有几个关键的知识点需要了解下:
Point 1:
Get
和Post
都是HTTP协议对网页操作的两个方法
Get
:从指定的资源请求数据
Post
:向指定的资源提交要被处理的数据
比如说你访问https://www.baidu.com/index.html
,这是Get
index.html
这个页面,GET
/POST
都是TCP
链接
Point 2:
这里涉及到url
的格式问题
https: | // | www.baidu.com | :80 | / | index.html |
---|---|---|---|---|---|
协议名 | 域名 | 端口号 | 文件名 |
先说一下什么是端口号,端口号是具有网络功能的应用软件的标志符
电脑A打开了可以被电脑B访问的软件,比如共享文件的文件系统软件,还有其他的软件,然后电脑A被电脑B访问,这个时候,电脑A就需要知道这些数据究竟是给哪个软件的,就需要端口号来识别了
访问服务器拿去数据,通过HTTP
协议,服务器就知道这个数据是给用HTTP
协议的软件的
HTTP
协议软件对应的端口号是80
然后看到域名www.baidu.com
,域名对应的就是IP
地址了,我们要把域名转为IP
地址才能知道服务器的所在,涉及到DNS
,域名系统(服务)协议(DNS)是一种分布式网络目录服务,主要用于域名与 IP 地址的相互转换
最后就是文件名了,文件名之前可能会有路径,例如:https://www.baidu.com/search/error.html
的search
二、ESP8266访问url的流程
子流程:解析URL
解析url
和DNS
解析ip
都是为了下面的初始化连接做准备,初始化连接需要对espconn
类型的结构体赋值
espconn
结构体如下:
struct espconn {
/** type of the espconn (TCP, UDP) 选择是TCP、UDP连接*/
enum espconn_type type;
/** current state of the espconn 当前的espconn结构体的状态,还在配置的过程就选择ESPCONN_NONE(具体的类型看ctrl+左键espconn_state)*/
enum espconn_state state;
/**这个看下面的esp_tcp结构体*/
union {
esp_tcp *tcp;
esp_udp *udp;
} proto;
/****************************初始化上面的***********************************/
/** A callback function that is informed about events for this espconn */
espconn_recv_callback recv_callback;
espconn_sent_callback sent_callback;
uint8 link_cnt;
void *reverse;
};
/*这个就是上面的union里面的esp_tcp成员*/
typedef struct _esp_tcp {
int remote_port; // 访问的服务器的端口(80)
int local_port; // 本地的端口espconn_port()获取
uint8 local_ip[4]; // 本地的ip地址
uint8 remote_ip[4]; // 访问的服务器的ip地址,DNS解析获取
/****************************初始化上面的***********************************/
espconn_connect_callback connect_callback;
espconn_reconnect_callback reconnect_callback;
espconn_connect_callback disconnect_callback;
espconn_connect_callback write_finish_fn;
} esp_tcp;
由上面的初始化结构体可以看到需要DNS解析获得远端ip地址,DNS解析函数是
espconn_gethostbyname(struct espconn *pespconn, const char *hostname, ip_addr_t *addr, dns_found_callback found)
需要定义一个espconn
结构体,通过host
去解析,得到ip
地址,成功或者超时后调用回调函数,也就是说需要先得到host
,再往上看一步,解析url
,这里就需要获得host
了,当然这里有错误处理的部分是没有列举出来的,为了方便调试,错误的时候可以打印出信息
这样思路就很清晰了,具体的代码看第四部分
三、部分函数的解析
Function 1:wifi_set_opmode()
这个是设置wifi工作模式的函数
Function 2:wifi_station_set_config()
对wifi
的配置,如名称,密码
可以看一下station_config
结构体
struct station_config {
uint8 ssid[32];
uint8 password[64];
uint8 bssid_set; // Note: If bssid_set is 1, station will just connect to the router
// with both ssid[] and bssid[] matched. Please check about this.
uint8 bssid[6];
};
Function 3:wifi_station_get_connect_status()
检查wifi
连接状态
Function 4:espconn_gethostbyname()
看名字,获取host
通过域名,很明显是获取ip
地址的函数
注意下参数dns_found_callback found
,这是成功找到ip
后的回调函数,比如说我成功通过域名解析到了对方的ip
,这个时候就需要去初始化连接的参数了
Function 5:espconn_regist_connectcb()
这是个注册函数,注册TCB
成功建立连接之后的回调函数,便于进行下一步,成功连接后接收数据
Function 6:espconn_regist_reconcb()
注册连接出错的回调函数,用户可以在注册的函数里面进行重新连接
Function 7:espconn_connect()
建立TCB
连接的api
Function 8:espconn_regist_recvcb()
注册成功接收数据后的回调函数
Function 9:espconn_regist_sentcb()
注册成功发送数据后的回调函数
Function 10:espconn_regist_disconcb()
TCB在接收完数据之后就是正常的断开连接了
###### Function 11:espconn_sent()
TCB
握手成功之后,发送数据请求给服务器,发送成功后转到上面注册的函数,然后wifi接受到服务器返回来的数据会自行跳转到接收成功注册的函数,在接收成功的注册函数里面可以打印或者是解析数据
四、代码的实现
user_main.c
void ICACHE_FLASH_ATTR Check(void)
{
ip_addr_t addr;
struct espconn pespconn;
os_printf("Check!\n");
/*查询wifi的状态*/
if (wifi_station_get_connect_status() == STATION_GOT_IP)
{
os_timer_disarm(&ledtimer);
if (wifi_station_get_connect_status() == STATION_GOT_IP) // 再次确认
{
os_printf("succeed!\n");
os_printf("wifi_name:TP-LINK-8266\n");
startHttpQuestByGET(url);
}
}
else
{
os_printf("fail!\n");
}
}
/*
* @name: user_init[用户程序入口]
*/
void ICACHE_FLASH_ATTR
user_init(void)
{
system_timer_reinit();
uart_init(74880, 74880); //设置串口0和串口1的波特率
os_printf("SDK version:%s\n", system_get_sdk_version());
os_printf("Hello8266!\n");
wifi_set_sleep_type(NONE_SLEEP_T); // 关闭睡眠
// 设置wifi的工作模式
wifi_set_opmode(0x01); // Station模式
os_printf("Config!\n");
/*设置wifi密码名字*/
os_strcpy(wifi_config.ssid, "TP-LINK-8266");
os_strcpy(wifi_config.password, "82668266");
wifi_station_set_config(&wifi_config);
wifi_station_connect();
/*软件定时器*/
os_timer_disarm(&ledtimer); // 关闭定时器
os_timer_setfn(&ledtimer, (os_timer_func_t *)Check, (void*)0); // 设置定时器的回调函数
os_timer_arm_us(&ledtimer, 500000, 1); // 打开定时器,500ms,重复
}
http_parse.c
#include "osapi.h"
#include "http_parse.h"
#include "espconn.h"
#include "c_types.h"
#include "mem.h"
#include "user_interface.h"
#include "driver/uart.h"
uint8 host[32] = {0}; // 放域名
uint8 filename[100] = {0}; // 放文件名
uint16 port = 80; // 端口号,默认是80
uint8 buffer[1024]; // 数据缓存端
struct espconn pespconn;
void ICACHE_FLASH_ATTR my_http_parse(uint8 *url)
{
uint8 *url1;
uint8 *url2;
if (*url == 0)
{
os_printf("url不存在\n");
return ;
}
/*获取host*/
url1 = url;
if (strncmp(url1, "http://", strlen("http://")) == 0)
{
url1 = url1 + strlen("http://"); // 记录的是域名的开始
}
else if (strncmp(url1, "https://", strlen("https://")) == 0)
{
url1 = url1 + strlen("https://"); // 记录的是域名的开始
}
url2 = strchr(url1, (int)'/'); // 需查找的字符不能"/"双引号
if (url2 == NULL)
{
os_printf("host获取失败!!!\n");
return;
}
memcpy(host, url1, strlen(url1) - strlen(url2)); // 从url1复制域名到host
url2 += 1;
host[strlen(url1) - strlen(url2)] = 0; // host的结尾加0
/*获取文件名*/
memcpy(filename, url2, strlen(url2)); // 从url2复制文件名到filename
filename[strlen(url2)] = 0; // filename数组的文件名长度最后面一个元素为0
/*获取端口号*/
url2 = strchr(url1, (int)':');
if (url2 != NULL)
port = atoi(url2 + 1); // 将:后面的数字字符串转为整形,只会转换最近的数字
os_printf("parse succeed!!!\n");
}
void ICACHE_FLASH_ATTR startHttpQuestByGET(uint8 *url)
{
ip_addr_t remote_ip;
memset(buffer, 0, 1024);
os_printf("start parse!!!\n");
my_http_parse(url);
os_sprintf(buffer, GET, filename, host);
os_printf("host:%s\n", host);
wifi_set_sleep_type(NONE_SLEEP_T); // 关闭睡眠
espconn_gethostbyname(&pespconn, host, &remote_ip, DNS_Get_Ip_Fcb);
}
void DNS_Get_Ip_Fcb(const char *name, ip_addr_t *remote_ip, void *callback_arg)
{
if (remote_ip)
os_printf("DSN ip get succeed!!!\n");
else
os_printf("DSN ip get fail!!!\n");
struct ip_info ststion_info;
wifi_get_ip_info(0x00, &ststion_info);
Init_Connect(remote_ip, &ststion_info.ip, port);
}
void ICACHE_FLASH_ATTR Init_Connect(ip_addr_t *remote_ip, ip_addr_t *local_ip, int remote_port)
{
os_printf("init espconn struct!!!\n");
/*配置连接结构体*/
pespconn.type = ESPCONN_TCP;
pespconn.state = ESPCONN_NONE;
pespconn.proto.tcp = (esp_tcp *) os_zalloc(sizeof(esp_tcp)); // 分配内存填充0
os_memcpy(pespconn.proto.tcp->local_ip, local_ip, 4);
os_memcpy(pespconn.proto.tcp->remote_ip, remote_ip, 4);
pespconn.proto.tcp->local_port = espconn_port();
pespconn.proto.tcp->remote_port = remote_port;
/*注册*/
espconn_regist_connectcb(&pespconn, Sent_Data);
espconn_regist_reconcb(&pespconn, Tcp_Disconect);
/*连接服务器*/
os_printf("start connect!!!\n");
espconn_connect(&pespconn);
}
void Sent_Data()
{
os_printf("connect succeed!!!\n");
espconn_regist_sentcb(&pespconn, Sent_Succeed); // 注册发送成功
espconn_regist_recvcb(&pespconn, Receive_Succeed); // 注册接收成功
espconn_regist_disconcb(&pespconn, Release_Succeed); // 注册断开
os_printf("Start request\n");
os_printf("%s\n", buffer);
os_printf("Over request\n");
espconn_send(&pespconn, buffer, strlen(buffer)); // 发送
}
void Tcp_Disconect(void *arg, sint8 err)
{
os_printf("Connect fail!!!\n");
os_printf("Reconnect!!!\n");
espconn_connect(&pespconn);
}
void Sent_Succeed()
{
os_printf("Send succeed!!!\n");
}
void Receive_Succeed(void *arg, char *pdata, unsigned short len)
{
os_printf("Receive succeed!!!\n");
os_printf("Data:%s\n", pdata);
}
void Release_Succeed()
{
os_printf("Release succeed!!!\n");
}
http_parse.c
的函数不单纯是解析的,还包括了连接的,我懒得去新建一个文件,所以直接写在一个文件里面,也不是很多的函数
http_parse.h
#ifndef _HTTP_PARSE_
#define _HTTP_PARSE_
#include "ip_addr.h"
#define GET "GET /%s HTTP/1.1\r\nContent-Type: text/html;charset=utf-8\r\nAccept: */*\r\nHost: %s\r\nConnection: Keep-Alive\r\n\r\n"
void ICACHE_FLASH_ATTR my_http_parse(uint8 *url);
void ICACHE_FLASH_ATTR startHttpQuestByGET(uint8 *url);
void DNS_Get_Ip_Fcb(const char *name, ip_addr_t *ipaddr, void *callback_arg);
void ICACHE_FLASH_ATTR Init_Connect(ip_addr_t *remote_ip, ip_addr_t *local_ip, int remote_port);
void Sent_Data();
void Tcp_Disconect(void *arg, sint8 err);
void Sent_Succeed();
void Receive_Succeed(void *arg, char *pdata, unsigned short len);
void Release_Succeed();
#endif
上面的代码包含了调试内容,忽略即可
五、调试问题
Q1:url解析的问题
字符串比较函数strncmp
不要用strcmp
去代替,后者难实现,指定长度可以减少很多的工作量
Q2:DNS解析问题
先看一下DNS
解析函数的接口,可以看到这里面有4
个参数,参数1
是连接结构体的地址
,参数2
是域名的地址,参数3
是获取的ip
的存放地址
,参数4
是回调函数
err_t espconn_gethostbyname(struct espconn *pespconn, const char *hostname, ip_addr_t *addr, dns_found_callback found);
解析错误极有可能是调用函数传参的问题,很明显,接口是没有定义struct espconn
和ip_addr_t
结构体的,只是定义了结构体指针,那么你在调用的时候就是需要定义一个清晰的结构体,而不是指针,然后再取结构体的指针传参,如下面
ip_addr_t remote_ip;
espconn_gethostbyname(&pespconn, host, &remote_ip, DNS_Get_Ip_Fcb);