ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用

授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。

QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷
image

一、你如果想学基于Arduino的ESP8266开发技术

一、基础篇

  1. ESP8266开发之旅 基础篇① 走进ESP8266的世界
  2. ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境
  3. ESP8266开发之旅 基础篇③ ESP8266与Arduino的开发说明
  4. ESP8266开发之旅 基础篇④ ESP8266与EEPROM
  5. ESP8266开发之旅 基础篇⑤ ESP8266 SPI通信和I2C通信
  6. ESP8266开发之旅 基础篇⑥ Ticker——ESP8266定时库

二、网络篇

  1. ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
  2. ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
  3. ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
  4. ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
  5. ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
  6. ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
  7. ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
  8. ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
  9. ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
  10. ESP8266开发之旅 网络篇⑩ UDP服务
  11. ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
  12. ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
  13. ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
  14. ESP8266开发之旅 网络篇⑭ web配网
  15. ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
  16. ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新

三、应用篇

  1. ESP8266开发之旅 应用篇① 局域网应用 ——炫酷RGB彩灯
  2. ESP8266开发之旅 应用篇② OLED显示天气屏
  3. ESP8266开发之旅 应用篇③ 简易版WiFi小车

四、高级篇

  1. ESP8266开发之旅 进阶篇① 代码优化 —— ESP8266内存管理
  2. ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266配置
  3. ESP8266开发之旅 进阶篇③ 闲聊 ESP8266 Flash
  4. ESP8266开发之旅 进阶篇④ 常见问题 —— 解决困扰
  5. ESP8266开发之旅 进阶篇⑤ 代码规范 —— 像写文章一样优美
  6. ESP8266开发之旅 进阶篇⑥ ESP-specific APIs说明

1. 前言

    在前面章节的博客中,博主介绍了ESP8266WiFi库 Tcp server的用法,并模拟了Http webserver的功能。但是,可以看出通过Tcp server 处理http请求,我们需要自己解析请求协议以及判断各种数据,稍微不小心就很容易出现错误。
    那么有没有针对Http webserver操作的库呢?答案肯定是有的,这就是博主本篇需要跟大家讲述的知识——ESP8266WebServer库。
    请注意,ESP8266WebServer库不属于ESP8266WiFi库的一部分,所以需要引入

#include <ESP8266WebServer.h>

    博主说过,Http是基于Tcp协议之上的,所以你在ESP8266WebServer源码中会看到WiFiServer和WiFiClient的踪迹。

............ 前面省略代码
struct RequestArgument {
    String key;
    String value;
  };

WiFiServer  _server;

WiFiClient  _currentClient;
.............后面省略代码

2. ESP8266WebServer库

    如果读者有下载源码的话,那么可以看到ESP8266WebServer库 目录如下:
image

    老规矩,先上一个博主总结的百度脑图:
image

总体上,根据功能可以把方法分为两大类:

  • 管理webserver方法;
  • 处理client请求方法;
  • 响应client请求方法;

注意点:

  • 博主希望读者可以先理解 ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用(该篇讲述了一些http协议的知识点),然后再来仔细查看本篇内容,我相信会事半功倍;

2.1 webserver管理方法

    http请求方法又可以有更好的细分。

2.1.1 ESP8266WebServer() —— 创建web server

函数说明:

/**
 * 创建webserver
 * @param  addr  IPAddress (IP地址)
 * @param  port  int  (端口号,默认是80)
 */
ESP8266WebServer(IPAddress addr, int port = 80);
 
/**
 * 创建webserver(使用默认的IP地址)
 * @param  port  int  (端口号,默认是80)
 */
ESP8266WebServer(int port = 80);

2.1.2 begin() —— 启动web server

函数说明:

/**
 * 启动webserver
 */  
void begin();
 
/**
 * 启动webserver
 * @param port uint16_t 端口号
 */
void begin(uint16_t port);

注意点:

  • 尽量在配置好各个请求处理之后再调用begin方法;

2.1.3 close() —— 关闭webserver

函数说明:

/**
 * 关闭webserver,关闭TCP连接
 */
void close();

2.1.4 stop() —— 关闭webserver

函数说明:

/**
 * 关闭webserver
 * 底层就是调用close();
 */
void stop();

2.2 处理client请求方法

2.2.1 on() —— 官方请求响应回调

函数1说明:

/**
 * 配置uri对应的handler,handler也就是处理方法
 * @param  uri  const String (uri路径)
 * @param  handler  THandlerFunction  (对应uri处理函数)
 */
void on(const String &uri, THandlerFunction handler);

注意点:

  • 注意点:这里对应的Http_Method 是Http_ANY,也就是不区分GET、POST等

函数2说明:

/**
 * 配置uri对应的handler,handler也就是处理方法
 * @param  uri  const String (uri路径)
 * @param  method  HTTPMethod(Http请求方法)
 *         可选参数:HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, 
                     HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS
 * @param  fn  THandlerFunction  (对应uri处理函数)
 */
void on(const String &uri, HTTPMethod method, THandlerFunction fn);

函数3说明:

/**
 * 配置uri对应的handler,handler也就是处理方法
 * @param  uri  const String (uri路径)
 * @param  method  HTTPMethod(Http请求方法)
 *         可选参数:HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, 
 *                   HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS
 * @param  fn  THandlerFunction  (对应uri处理函数)
 * @param  ufn THandlerFunction  (文件上传处理函数)
 */
void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn);

注意点:

  • 函数1和函数2两个on方法最终都会调用到这个方法
  • 请求处理函数THandlerFunction定义为:
typedef std::function<void(void)> THandlerFunction;
也就是说我们的请求处理函数定义应该是:
void methodName(void);
一般我们习惯写成:
void handleXXXX(){
    //以下写上处理代码
}
  • 最终底层代码会把fn ufn uri method封装成一个RequestHandler(FunctionRequestHandler)。

在这里,博主先给大家看看On方法的源码:

/**
 * 绑定uri对应的请求回调方法
 * @param  uri  const String (uri路径)
 * @param  method  HTTPMethod(Http请求方法)
 *         可选参数:HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, 
 *                   HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS
 * @param  fn  THandlerFunction  (对应uri处理函数)
 * @param  ufn THandlerFunction  (文件上传处理函数 
 */
void ESP8266WebServer::on(const String &uri, HTTPMethod method, ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction ufn) {
  _addRequestHandler(new FunctionRequestHandler(fn, ufn, uri, method));
}

/**
 * 组成请求处理链表
 * @param RequestHandler* 请求处理者
 */
void ESP8266WebServer::_addRequestHandler(RequestHandler* handler) {
    if (!_lastHandler) {
      _firstHandler = handler;
      _lastHandler = handler;
    }
    else {
      _lastHandler->next(handler);
      _lastHandler = handler;
    }
}

注意点:

  • 这里用到了一种思路叫做“责任链设计模式”,各个请求组成一个顺序的链表(责任链设计模式,请自行百度了解);
  • 既然是链表,那么是不是意味着排在前面的优先得到处理呢?(读者可以考虑哪些请求概率比较高然后优先放在链表的前面)。
  • on()函数用到了FunctionRequestHandler来包装请求处理;

2.2.2 addHandler() —— 自定义请求响应回调

函数说明:

/**
 * 添加一个自定义的RequestHandler(请求处理)
 * @param handler RequestHandler (自主实现的RequestHandler)
 */
void addHandler(RequestHandler* handler);

看看源码:

/**
 * 添加一个自定义的RequestHandler(请求处理)
 * @param handler RequestHandler (自主实现的RequestHandler)
 */
void ESP8266WebServer::addHandler(RequestHandler* handler) {
    _addRequestHandler(handler);
}

到这里,我们需要来了解一下 RequestHandler 是什么神奇的类,我们需要怎么样做处理呢?

先来看看 RequestHandler 的类结构:

/**
 * RequestHandler 的类结构
 */
class RequestHandler {
public:
    virtual ~RequestHandler() { }
    //判断请求处理者是否可以处理该uri,并且匹配method
    virtual bool canHandle(HTTPMethod method, String uri) { (void) method; (void) uri; return false; }
    //判断请求处理者是否可以处理文件上传,一般用于http文件上传
    virtual bool canUpload(String uri) { (void) uri; return false; }
    //调用处理方法 - 普通请求
    virtual bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) { (void) server; (void) requestMethod; (void) requestUri; return false; }
    //调用处理方法 - 文件上传
    virtual void upload(ESP8266WebServer& server, String requestUri, HTTPUpload& upload) { (void) server; (void) requestUri; (void) upload; }
    //获取当前处理者的下一个处理者
    RequestHandler* next() { return _next; }
    //设置当前处理者的下一个处理者,这里就是责任链的关键
    void next(RequestHandler* r) { _next = r; }

private:
    RequestHandler* _next = nullptr;
};

    可以看出,RequestHandler 主要包装了WebServer可以处理的http请求。当有请求来的时候,就会调用对应的 RequestHandler;
    接下来我们来看一个用得最多的一个 RequestHandler 子类——FunctionRequestHandler类(博主上面说到了这个注意点):

class FunctionRequestHandler : public RequestHandler {
public:
    FunctionRequestHandler(ESP8266WebServer::THandlerFunction fn, ESP8266WebServer::THandlerFunction ufn, const String &uri, HTTPMethod method)
    : _fn(fn)
    , _ufn(ufn)
    , _uri(uri)
    , _method(method)
    {
    }

    bool canHandle(HTTPMethod requestMethod, String requestUri) override  {
        //以下判断这个handler是否可以处理这个 requestUri
        //判断requestMethod是否匹配
        if (_method != HTTP_ANY && _method != requestMethod)
            return false;

        //判断requestUri是否匹配
        if (requestUri != _uri)
            return false;

        return true;
    }

    bool canUpload(String requestUri) override  {
        //以下判断这个handler是否可以处理这个 文件上传请求
        //判断文件上传函数是否实现 或者 开发者定义了文件上传函数,但是method不是 HTTP_POST 或者 requestUri没对上
        if (!_ufn || !canHandle(HTTP_POST, requestUri))
            return false;

        return true;
    }

    bool handle(ESP8266WebServer& server, HTTPMethod requestMethod, String requestUri) override {
        (void) server;
        if (!canHandle(requestMethod, requestUri))
            return false;
        //调用请求处理函数
        _fn();
        return true;
    }

    void upload(ESP8266WebServer& server, String requestUri, HTTPUpload& upload) override {
        (void) server;
        (void) upload;
        if (canUpload(requestUri))
        //调用处理文件上传函数
            _ufn();
    }

protected:
    //通用请求处理函数
    ESP8266WebServer::THandlerFunction _fn;
    //文件上传请求处理函数
    ESP8266WebServer::THandlerFunction _ufn;
    //匹配的uri
    String _uri;
    //匹配的HTTPMethod
    HTTPMethod _method;
};

    通过上面的代码分析,博主相信大家应该对请求处理类有一个初步的认识,用起来得心应手。

2.2.3 onNotFound() —— 配置无效uri的handler

函数说明:

/**
 * 配置无效uri的handler
 * @param  fn  THandlerFunction  (对应uri处理函数)
 */
void onNotFound(THandlerFunction fn);  //called when handler is not assigned

注意点:

  • 当找不到可以处理某一个http请求的时候就会调用该函数配置的fn;
  • 当然,如果你没有配置这个方法也可以,因为核心库底层有了默认实现:
......
if (!handled) {
    using namespace mime;
    //发送默认的404错误
    send(404, String(FPSTR(mimeTable[html].mimeType)), String(F("Not found: ")) + _currentUri);
    handled = true;
}
......

2.2.4 onFileUpload() —— 配置处理文件上传的handler

函数说明:

/**
 * 配置处理文件上传的handler
 * @param  fn  THandlerFunction  (对应uri处理函数)
 */
void onFileUpload(THandlerFunction fn); //handle file uploads

2.3 处理client请求方法

2.3.1 uri() —— 获取请求的uri

函数说明:

/**
 * 获取请求的uri
 */  
String uri();

2.3.2 method() —— 获取请求方法

函数说明:

/**
 * 获取请求的uri
 */  
HTTPMethod method()

其中,HTTPMethod取值范围为:

enum HTTPMethod { HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS };

2.3.2 arg(name) —— 获取请求参数的值

函数说明:

/**
 * 根据请求key获取请求参数的值
 * @param name String(请求key)
 */
String arg(String name);// get request argument value by name

2.3.3 arg(index) —— 获取请求参数的值

函数说明:

/**
 * 获取第几个请求参数的值
 * @param i int(请求index)
 */
String arg(int i);// get request argument value by number

2.3.4 argName(index) —— 获取请求参数的名称

函数说明:

/**
 * 获取第几个请求参数的名字
 * @param i int(请求index)
 */
String argName(int i);// get request argument name by number

2.3.5 args() —— 获取参数个数

函数说明:

/**
 * 获取参数个数
 */
int args(); // get arguments count

2.3.6 hasArg() —— 是否存在某个参数

函数说明:

/**
 * 是否存在某个参数
 */
bool hasArg(String name);// check if argument exists

2.3.7 collectHeaders() —— 设置需要收集的请求头

函数说明:

/**
 * 设置需要收集的请求头(1-n个)
 * @param headerKeys[] const char *   请求头的名字
 * @param headerKeysCount const size_t 请求头的个数
 */
void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); // set the request headers to collect

2.3.8 collectHeaders() —— 设置需要收集的请求头

函数说明:

/**
 * 设置需要收集的请求头(1-n个)
 * @param headerKeys[] const char *   请求头的名字
 * @param headerKeysCount const size_t 请求头的个数
 */
void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); // set the request headers to collect

2.3.9 header(name) —— 获取请求头参数值

函数说明:

/**
 * 获取请求头参数值
 * @param name   const char *   请求头的名字
 * @return value of headerkey(name)
 */
String header(String name);// get request header value by name

2.3.10 header(index) —— 获取第index个请求头参数值

函数说明:

/**
 * 获取第i个请求头参数值
 * @param i   size_t   请求头索引值
 * @return value of header index
 */
String header(int i);// get request header value by number

2.3.11 headerName(index) —— 获取第index个请求头名字

函数说明:

/**
 * 获取第i个请求头名字
 * @param i   size_t   请求头索引值
 * @return name of header index
 */
String headerName(int i);// get request header name by number

2.3.12 headers() —— 获取收集请求头个数

函数说明:

/**
 * 获取收集请求头个数
 * @return count int
 */
int headers();// get header count

2.3.13 hasHeader(name) —— 判断是否存在某一个请求头

函数说明:

/**
 * 判断是否存在某一个请求头
 * @param name   const char*   请求头名字
 * @return bool
 */
bool hasHeader(String name); // check if header exists

2.3.14 hostHeader() —— 获取请求头Host的值

函数说明:

/**
 * 获取请求头Host的值
 */
String hostHeader();// get request host header if available or empty String if not

2.3.15 authenticate() —— 认证校验

函数说明:

/**
 * 认证校验(Authorization)
 * @param  fn  THandlerFunction  (对应uri处理函数) 
 * @param  username const char * 用户账号
 * @param  password const char * 用户密码
 */  
bool authenticate(const char * username, const char * password);

看看authenticate底层源码:

bool ESP8266WebServer::authenticate(const char * username, const char * password){
  //判断是否存在 Authorization 请求头
  if(hasHeader(FPSTR(AUTHORIZATION_HEADER))) {
    String authReq = header(FPSTR(AUTHORIZATION_HEADER));
    //判断 Authorization的值是不是base64编码
    if(authReq.startsWith(F("Basic"))){
      authReq = authReq.substring(6);
      authReq.trim();
      char toencodeLen = strlen(username)+strlen(password)+1;
      char *toencode = new char[toencodeLen + 1];
      if(toencode == NULL){
        authReq = "";
        return false;
      }
      char *encoded = new char[base64_encode_expected_len(toencodeLen)+1];
      if(encoded == NULL){
        authReq = "";
        delete[] toencode;
        return false;
      }
      sprintf(toencode, "%s:%s", username, password);
      //判断通过username:password生成的base64编码是否和请求头的Authorization的值一样,一样表示通过验证
      if(base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) {
        authReq = "";
        delete[] toencode;
        delete[] encoded;
        return true;
      }
      delete[] toencode;
      delete[] encoded;
    } else if(authReq.startsWith(F("Digest"))) {
      // HTTP Authorization 之 Digest Auth 用到MD5加密
      authReq = authReq.substring(7);
      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.println(authReq);
      #endif
      String _username = _extractParam(authReq,F("username=\""));
      if(!_username.length() || _username != String(username)) {
        authReq = "";
        return false;
      }
      // extracting required parameters for RFC 2069 simpler Digest
      String _realm    = _extractParam(authReq, F("realm=\""));
      String _nonce    = _extractParam(authReq, F("nonce=\""));
      String _uri      = _extractParam(authReq, F("uri=\""));
      String _response = _extractParam(authReq, F("response=\""));
      String _opaque   = _extractParam(authReq, F("opaque=\""));

      if((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_response.length()) || (!_opaque.length())) {
        authReq = "";
        return false;
      }
      if((_opaque != _sopaque) || (_nonce != _snonce) || (_realm != _srealm)) {
        authReq = "";
        return false;
      }
      // parameters for the RFC 2617 newer Digest
      String _nc,_cnonce;
      if(authReq.indexOf(FPSTR(qop_auth)) != -1) {
        _nc = _extractParam(authReq, F("nc="), ',');
        _cnonce = _extractParam(authReq, F("cnonce=\""));
      }
      MD5Builder md5;
      md5.begin();
      md5.add(String(username) + ':' + _realm + ':' + String(password));  // md5 of the user:realm:user
      md5.calculate();
      String _H1 = md5.toString();
      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.println("Hash of user:realm:pass=" + _H1);
      #endif
      md5.begin();
      if(_currentMethod == HTTP_GET){
        md5.add(String(F("GET:")) + _uri);
      }else if(_currentMethod == HTTP_POST){
        md5.add(String(F("POST:")) + _uri);
      }else if(_currentMethod == HTTP_PUT){
        md5.add(String(F("PUT:")) + _uri);
      }else if(_currentMethod == HTTP_DELETE){
        md5.add(String(F("DELETE:")) + _uri);
      }else{
        md5.add(String(F("GET:")) + _uri);
      }
      md5.calculate();
      String _H2 = md5.toString();
      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.println("Hash of GET:uri=" + _H2);
      #endif
      md5.begin();
      if(authReq.indexOf(FPSTR(qop_auth)) != -1) {
        md5.add(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
      } else {
        md5.add(_H1 + ':' + _nonce + ':' + _H2);
      }
      md5.calculate();
      String _responsecheck = md5.toString();
      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.println("The Proper response=" +_responsecheck);
      #endif
      if(_response == _responsecheck){
        authReq = "";
        return true;
      }
    }
    authReq = "";
  }
  return false;
}

注意点:

  • 这里涉及到了HTTP Authorization的两种验证方式:HTTP Basic Auth 和 HTTP Digest Auth(感兴趣的读者请自行查阅资料);
  • 该方法会对http请求进行验证用户信息,如果不通过,理论上需要用户重新输入正确用户名称和密码以便再次请求;

2.3.16 handleClient() —— 处理http请求

这是一个非常重要的方法,所以请认真阅读博主的讲解。
函数说明:

/**
 * 等待请求进来并处理
 */
void handleClient();

接下来,博主将分析源码,看看webserver是怎么样解析http请求然后调用具体的请求处理函数:

void ESP8266WebServer::handleClient() {
  //判断当前状态是不是空闲状态
  if (_currentStatus == HC_NONE) {
    //有http请求进来
    WiFiClient client = _server.available();
    if (!client) {
      return;
    }

#ifdef DEBUG_ESP_HTTP_SERVER
    DEBUG_OUTPUT.println("New client");
#endif
    //设置当前的http client请求
    _currentClient = client;
    //更改当前状态为等待读取数据状态
    _currentStatus = HC_WAIT_READ;
    _statusChange = millis();
  }

  bool keepCurrentClient = false;
  bool callYield = false;

  if (_currentClient.connected()) {
    switch (_currentStatus) {
    case HC_NONE:
      // No-op to avoid C++ compiler warning
      break;
    case HC_WAIT_READ:
      // Wait for data from client to become available
      //判断是否有请求数据
      if (_currentClient.available()) {
        //开始解析http请求
        if (_parseRequest(_currentClient)) {
          _currentClient.setTimeout(HTTP_MAX_SEND_WAIT);
          _contentLength = CONTENT_LENGTH_NOT_SET;
          //处理请求
          _handleRequest();

          if (_currentClient.connected()) {
            _currentStatus = HC_WAIT_CLOSE;
            _statusChange = millis();
            keepCurrentClient = true;
          }
        }
      } else { // !_currentClient.available()
        //等待请求数据到来,会设置一个超时时间
        if (millis() - _statusChange <= HTTP_MAX_DATA_WAIT) {
          keepCurrentClient = true;
        }
        callYield = true;
      }
      break;
    case HC_WAIT_CLOSE:
      // Wait for client to close the connection
      if (millis() - _statusChange <= HTTP_MAX_CLOSE_WAIT) {
        keepCurrentClient = true;
        callYield = true;
      }
    }
  }

  if (!keepCurrentClient) {
     //断开tcp连接
    _currentClient = WiFiClient();
    _currentStatus = HC_NONE;
    _currentUpload.reset();
  }

  if (callYield) {
    yield();
  }
}

注意点:

  • _parseRequest 方法负责解析http请求:
bool ESP8266WebServer::_parseRequest(WiFiClient& client) {
  // 读取http请求的第一行
  String req = client.readStringUntil('\r');
  client.readStringUntil('\n');
  //重置请求头
  for (int i = 0; i < _headerKeysCount; ++i) {
    _currentHeaders[i].value =String();
   }

  // First line of HTTP request looks like "GET /path HTTP/1.1"
  // Retrieve the "/path" part by finding the spaces
  int addr_start = req.indexOf(' ');
  int addr_end = req.indexOf(' ', addr_start + 1);
  if (addr_start == -1 || addr_end == -1) {
#ifdef DEBUG_ESP_HTTP_SERVER
    DEBUG_OUTPUT.print("Invalid request: ");
    DEBUG_OUTPUT.println(req);
#endif
    return false;
  }
  //获取Http requestMethod
  String methodStr = req.substring(0, addr_start);
  //获取Http requestUri
  String url = req.substring(addr_start + 1, addr_end);
  String versionEnd = req.substring(addr_end + 8);
  //获取Http 请求版本
  _currentVersion = atoi(versionEnd.c_str());
  String searchStr = "";
  //判断 requestUri里面是否有queryParam,例如:/path?xxx=xxx
  int hasSearch = url.indexOf('?');
  if (hasSearch != -1){
    //把url和query param拆开来
    searchStr = url.substring(hasSearch + 1);
    url = url.substring(0, hasSearch);
  }
  _currentUri = url;
  _chunked = false;

  //判断Method
  HTTPMethod method = HTTP_GET;
  if (methodStr == F("POST")) {
    method = HTTP_POST;
  } else if (methodStr == F("DELETE")) {
    method = HTTP_DELETE;
  } else if (methodStr == F("OPTIONS")) {
    method = HTTP_OPTIONS;
  } else if (methodStr == F("PUT")) {
    method = HTTP_PUT;
  } else if (methodStr == F("PATCH")) {
    method = HTTP_PATCH;
  }
  _currentMethod = method;

#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("method: ");
  DEBUG_OUTPUT.print(methodStr);
  DEBUG_OUTPUT.print(" url: ");
  DEBUG_OUTPUT.print(url);
  DEBUG_OUTPUT.print(" search: ");
  DEBUG_OUTPUT.println(searchStr);
#endif

  //attach handler
  RequestHandler* handler;
  //查找可以处理该请求的requestHandler
  for (handler = _firstHandler; handler; handler = handler->next()) {
    if (handler->canHandle(_currentMethod, _currentUri))
      break;
  }
  _currentHandler = handler;

  String formData;
  // POST请求
  if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE){
    String boundaryStr;
    String headerName;
    String headerValue;
    bool isForm = false;
    bool isEncoded = false;
    uint32_t contentLength = 0;
    //解析请求头
    while(1){
      req = client.readStringUntil('\r');
      client.readStringUntil('\n');
      if (req == "") break;//no moar headers
      int headerDiv = req.indexOf(':');
      if (headerDiv == -1){
        break;
      }
      headerName = req.substring(0, headerDiv);
      headerValue = req.substring(headerDiv + 1);
      headerValue.trim();
      //收集请求头信息
       _collectHeader(headerName.c_str(),headerValue.c_str());

      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.print("headerName: ");
      DEBUG_OUTPUT.println(headerName);
      DEBUG_OUTPUT.print("headerValue: ");
      DEBUG_OUTPUT.println(headerValue);
      #endif
      //判断 Content_Type
      if (headerName.equalsIgnoreCase(FPSTR(Content_Type))){
        using namespace mime;
        // Content_Type = "text/plain"
        if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))){
          isForm = false;
        } else if (headerValue.startsWith(F("application/x-www-form-urlencoded"))){
          isForm = false;
          //有加了编码
          isEncoded = true;
        } else if (headerValue.startsWith(F("multipart/"))){
          //获取 boundary,用于分割不同的字段
          boundaryStr = headerValue.substring(headerValue.indexOf('=') + 1);
          boundaryStr.replace("\"","");
          isForm = true;
        }
      } else if (headerName.equalsIgnoreCase(F("Content-Length"))){
        //判断 Content-Length 数值
        contentLength = headerValue.toInt();
      } else if (headerName.equalsIgnoreCase(F("Host"))){
        _hostHeader = headerValue;
      }
    }

    //不是 multipart/form-data 
    if (!isForm){
      size_t plainLength;
      //读取请求内容 最大超时 HTTP_MAX_POST_WAIT
      char* plainBuf = readBytesWithTimeout(client, contentLength, plainLength, HTTP_MAX_POST_WAIT);
      if (plainLength < contentLength) {
        free(plainBuf);
        return false;
      }
      if (contentLength > 0) {
        // 属于 application/x-www-form-urlencoded
        if(isEncoded){
          //url encoded form 处理表单数据
          if (searchStr != "") searchStr += '&';
          searchStr += plainBuf;
        }
        //开始解析 queryParam
        _parseArguments(searchStr);
        if(!isEncoded){
          //plain post json or other data
          RequestArgument& arg = _currentArgs[_currentArgCount++];
          arg.key = F("plain");
          arg.value = String(plainBuf);
        }

  #ifdef DEBUG_ESP_HTTP_SERVER
        DEBUG_OUTPUT.print("Plain: ");
        DEBUG_OUTPUT.println(plainBuf);
  #endif
        free(plainBuf);
      } else {
        // No content - but we can still have arguments in the URL.
        _parseArguments(searchStr);
      }
    }
    // multipart/form-data 
    if (isForm){
      //解析query param
      _parseArguments(searchStr);
      //读取表单请求数据
      if (!_parseForm(client, boundaryStr, contentLength)) {
        return false;
      }
    }
  } else {
    //以下是GET请求解析
    String headerName;
    String headerValue;
    //parse headers
    while(1){
      req = client.readStringUntil('\r');
      client.readStringUntil('\n');
      if (req == "") break;//no moar headers
      int headerDiv = req.indexOf(':');
      if (headerDiv == -1){
        break;
      }
      headerName = req.substring(0, headerDiv);
      headerValue = req.substring(headerDiv + 2);
      _collectHeader(headerName.c_str(),headerValue.c_str());

      #ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.print("headerName: ");
      DEBUG_OUTPUT.println(headerName);
      DEBUG_OUTPUT.print("headerValue: ");
      DEBUG_OUTPUT.println(headerValue);
      #endif

      if (headerName.equalsIgnoreCase("Host")){
        _hostHeader = headerValue;
      }
    }
    _parseArguments(searchStr);
  }
  client.flush();

#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("Request: ");
  DEBUG_OUTPUT.println(url);
  DEBUG_OUTPUT.print(" Arguments: ");
  DEBUG_OUTPUT.println(searchStr);
#endif

  return true;
}

/**
 * 解析参数
 */
void ESP8266WebServer::_parseArguments(String data) {
#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("args: ");
  DEBUG_OUTPUT.println(data);
#endif
  if (_currentArgs)
    delete[] _currentArgs;
  _currentArgs = 0;
  if (data.length() == 0) {
    _currentArgCount = 0;
    _currentArgs = new RequestArgument[1];
    return;
  }
  _currentArgCount = 1;

  for (int i = 0; i < (int)data.length(); ) {
    i = data.indexOf('&', i);
    if (i == -1)
      break;
    ++i;
    ++_currentArgCount;
  }
#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("args count: ");
  DEBUG_OUTPUT.println(_currentArgCount);
#endif

  _currentArgs = new RequestArgument[_currentArgCount+1];
  int pos = 0;
  int iarg;
  for (iarg = 0; iarg < _currentArgCount;) {
    int equal_sign_index = data.indexOf('=', pos);
    int next_arg_index = data.indexOf('&', pos);
#ifdef DEBUG_ESP_HTTP_SERVER
    DEBUG_OUTPUT.print("pos ");
    DEBUG_OUTPUT.print(pos);
    DEBUG_OUTPUT.print("=@ ");
    DEBUG_OUTPUT.print(equal_sign_index);
    DEBUG_OUTPUT.print(" &@ ");
    DEBUG_OUTPUT.println(next_arg_index);
#endif
    if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) {
#ifdef DEBUG_ESP_HTTP_SERVER
      DEBUG_OUTPUT.print("arg missing value: ");
      DEBUG_OUTPUT.println(iarg);
#endif
      if (next_arg_index == -1)
        break;
      pos = next_arg_index + 1;
      continue;
    }
    RequestArgument& arg = _currentArgs[iarg];
    arg.key = urlDecode(data.substring(pos, equal_sign_index));
    arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index));
#ifdef DEBUG_ESP_HTTP_SERVER
    DEBUG_OUTPUT.print("arg ");
    DEBUG_OUTPUT.print(iarg);
    DEBUG_OUTPUT.print(" key: ");
    DEBUG_OUTPUT.print(arg.key);
    DEBUG_OUTPUT.print(" value: ");
    DEBUG_OUTPUT.println(arg.value);
#endif
    ++iarg;
    if (next_arg_index == -1)
      break;
    pos = next_arg_index + 1;
  }
  _currentArgCount = iarg;
#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("args count: ");
  DEBUG_OUTPUT.println(_currentArgCount);
#endif

}

/**
 * 解析 multipart/form-data 
 */
bool ESP8266WebServer::_parseForm(WiFiClient& client, String boundary, uint32_t len){
  (void) len;
#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("Parse Form: Boundary: ");
  DEBUG_OUTPUT.print(boundary);
  DEBUG_OUTPUT.print(" Length: ");
  DEBUG_OUTPUT.println(len);
#endif
  String line;
  int retry = 0;
  do {
    line = client.readStringUntil('\r');
    ++retry;
  } while (line.length() == 0 && retry < 3);

  client.readStringUntil('\n');
  //开始读取表单
  if (line == ("--"+boundary)){
    RequestArgument* postArgs = new RequestArgument[32];
    int postArgsLen = 0;
    while(1){
      String argName;
      String argValue;
      String argType;
      String argFilename;
      bool argIsFile = false;

      line = client.readStringUntil('\r');
      client.readStringUntil('\n');
      if (line.length() > 19 && line.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))){
        int nameStart = line.indexOf('=');
        if (nameStart != -1){
          argName = line.substring(nameStart+2);
          nameStart = argName.indexOf('=');
          if (nameStart == -1){
            //文本内容
            argName = argName.substring(0, argName.length() - 1);
          } else {
            //文件内容
            argFilename = argName.substring(nameStart+2, argName.length() - 1);
            argName = argName.substring(0, argName.indexOf('"'));
            argIsFile = true;
#ifdef DEBUG_ESP_HTTP_SERVER
            DEBUG_OUTPUT.print("PostArg FileName: ");
            DEBUG_OUTPUT.println(argFilename);
#endif
            //use GET to set the filename if uploading using blob
            if (argFilename == F("blob") && hasArg(FPSTR(filename))) 
              argFilename = arg(FPSTR(filename));
          }
#ifdef DEBUG_ESP_HTTP_SERVER
          DEBUG_OUTPUT.print("PostArg Name: ");
          DEBUG_OUTPUT.println(argName);
#endif
          using namespace mime;
          argType = FPSTR(mimeTable[txt].mimeType);
          line = client.readStringUntil('\r');
          client.readStringUntil('\n');
          if (line.length() > 12 && line.substring(0, 12).equalsIgnoreCase(FPSTR(Content_Type))){
            argType = line.substring(line.indexOf(':')+2);
            //skip next line
            client.readStringUntil('\r');
            client.readStringUntil('\n');
          }
#ifdef DEBUG_ESP_HTTP_SERVER
          DEBUG_OUTPUT.print("PostArg Type: ");
          DEBUG_OUTPUT.println(argType);
#endif
          if (!argIsFile){
            //文本内容处理方式
            while(1){
              line = client.readStringUntil('\r');
              client.readStringUntil('\n');
              if (line.startsWith("--"+boundary)) break;
              if (argValue.length() > 0) argValue += "\n";
              argValue += line;
            }
#ifdef DEBUG_ESP_HTTP_SERVER
            DEBUG_OUTPUT.print("PostArg Value: ");
            DEBUG_OUTPUT.println(argValue);
            DEBUG_OUTPUT.println();
#endif

            RequestArgument& arg = postArgs[postArgsLen++];
            arg.key = argName;
            arg.value = argValue;

            if (line == ("--"+boundary+"--")){
            //判断读取结束
#ifdef DEBUG_ESP_HTTP_SERVER
              DEBUG_OUTPUT.println("Done Parsing POST");
#endif
              break;
            }
          } else {
            //文件内容处理方式,开始处理文件上传
            _currentUpload.reset(new HTTPUpload());
            _currentUpload->status = UPLOAD_FILE_START;
            _currentUpload->name = argName;
            _currentUpload->filename = argFilename;
            _currentUpload->type = argType;
            _currentUpload->totalSize = 0;
            _currentUpload->currentSize = 0;
#ifdef DEBUG_ESP_HTTP_SERVER
            DEBUG_OUTPUT.print("Start File: ");
            DEBUG_OUTPUT.print(_currentUpload->filename);
            DEBUG_OUTPUT.print(" Type: ");
            DEBUG_OUTPUT.println(_currentUpload->type);
#endif
            //处理文件上传
            if(_currentHandler && _currentHandler->canUpload(_currentUri))
              _currentHandler->upload(*this, _currentUri, *_currentUpload);
             //表示文件正在执行写操作,我们可以在handler里面存文件内容下来,用到FS
            _currentUpload->status = UPLOAD_FILE_WRITE;
            uint8_t argByte = _uploadReadByte(client);
readfile:
            while(argByte != 0x0D){
              if (!client.connected()) return _parseFormUploadAborted();
              _uploadWriteByte(argByte);
              argByte = _uploadReadByte(client);
            }

            argByte = _uploadReadByte(client);
            if (!client.connected()) return _parseFormUploadAborted();
            if (argByte == 0x0A){
              argByte = _uploadReadByte(client);
              if (!client.connected()) return _parseFormUploadAborted();
              if ((char)argByte != '-'){
                //continue reading the file
                _uploadWriteByte(0x0D);
                _uploadWriteByte(0x0A);
                goto readfile;
              } else {
                argByte = _uploadReadByte(client);
                if (!client.connected()) return _parseFormUploadAborted();
                if ((char)argByte != '-'){
                  //continue reading the file
                  _uploadWriteByte(0x0D);
                  _uploadWriteByte(0x0A);
                  _uploadWriteByte((uint8_t)('-'));
                  goto readfile;
                }
              }

              uint8_t endBuf[boundary.length()];
              client.readBytes(endBuf, boundary.length());

              if (strstr((const char*)endBuf, boundary.c_str()) != NULL){
                if(_currentHandler && _currentHandler->canUpload(_currentUri))
                  _currentHandler->upload(*this, _currentUri, *_currentUpload);
                _currentUpload->totalSize += _currentUpload->currentSize;
                _currentUpload->status = UPLOAD_FILE_END;
                //上传文件结束
                if(_currentHandler && _currentHandler->canUpload(_currentUri))
                  _currentHandler->upload(*this, _currentUri, *_currentUpload);
#ifdef DEBUG_ESP_HTTP_SERVER
                DEBUG_OUTPUT.print("End File: ");
                DEBUG_OUTPUT.print(_currentUpload->filename);
                DEBUG_OUTPUT.print(" Type: ");
                DEBUG_OUTPUT.print(_currentUpload->type);
                DEBUG_OUTPUT.print(" Size: ");
                DEBUG_OUTPUT.println(_currentUpload->totalSize);
#endif
                line = client.readStringUntil(0x0D);
                client.readStringUntil(0x0A);
                if (line == "--"){
#ifdef DEBUG_ESP_HTTP_SERVER
                  DEBUG_OUTPUT.println("Done Parsing POST");
#endif
                  break;
                }
                continue;
              } else {
                _uploadWriteByte(0x0D);
                _uploadWriteByte(0x0A);
                _uploadWriteByte((uint8_t)('-'));
                _uploadWriteByte((uint8_t)('-'));
                uint32_t i = 0;
                while(i < boundary.length()){
                  _uploadWriteByte(endBuf[i++]);
                }
                argByte = _uploadReadByte(client);
                goto readfile;
              }
            } else {
              _uploadWriteByte(0x0D);
              goto readfile;
            }
            break;
          }
        }
      }
    }

    int iarg;
    int totalArgs = ((32 - postArgsLen) < _currentArgCount)?(32 - postArgsLen):_currentArgCount;
    for (iarg = 0; iarg < totalArgs; iarg++){
      RequestArgument& arg = postArgs[postArgsLen++];
      arg.key = _currentArgs[iarg].key;
      arg.value = _currentArgs[iarg].value;
    }
    if (_currentArgs) delete[] _currentArgs;
    _currentArgs = new RequestArgument[postArgsLen];
    for (iarg = 0; iarg < postArgsLen; iarg++){
      RequestArgument& arg = _currentArgs[iarg];
      arg.key = postArgs[iarg].key;
      arg.value = postArgs[iarg].value;
    }
    _currentArgCount = iarg;
    if (postArgs) 
      delete[] postArgs;
    return true;
  }
#ifdef DEBUG_ESP_HTTP_SERVER
  DEBUG_OUTPUT.print("Error: line: ");
  DEBUG_OUTPUT.println(line);
#endif
  return false;
}

content-type取值可以参考 四种常见的 POST 提交数据方式对应的content-type取值

  • _handleRequest 方法负责处理请求:
void ESP8266WebServer::_handleRequest() {
  bool handled = false;
  if (!_currentHandler){
#ifdef DEBUG_ESP_HTTP_SERVER
    DEBUG_OUTPUT.println("request handler not found");
#endif
  }
  else {
    //调用对应的请求处理函数
    handled = _currentHandler->handle(*this, _currentMethod, _currentUri);
#ifdef DEBUG_ESP_HTTP_SERVER
    if (!handled) {
      DEBUG_OUTPUT.println("request handler failed to handle request");
    }
#endif
  }
  if (!handled && _notFoundHandler) {
    //没有任何匹配的请求处理handler,默认提示404
    _notFoundHandler();
    handled = true;
  }
  if (!handled) {
    using namespace mime;
    send(404, String(FPSTR(mimeTable[html].mimeType)), String(F("Not found: ")) + _currentUri);
    handled = true;
  }
  if (handled) {
    _finalizeResponse();
  }
  _currentUri = "";
}

注意点:

  • 处理文件上传,使用到了 HTTPUpload,当不断把文件内容写入buf时,也会不断触发我们对应的url 请求处理回调函数,这就意味着我们可以在请求处理回调函数里面把文件内容保存在本地文件系统(FS,后面博主会讲解这一块);
  • 后面读者会发现,我们会在代码中用到这个类,这里先初略知道有这么一个关于Http上传的封装类;

在这里,博主重新梳理了WebServer处理Http请求的逻辑:

  • 首先,获取有效的Http请求:**_currentClient.available()**;
  • 然后,开始解析Http请求:**_parseRequest(_currentClient)**;
  1. 解析 HTTP requestUri、Http requestMethod、HttpVersion
  2. 寻找可以处理该请求的 requestHandler
  3. 对于GET请求,解析请求头、请求参数(requestArguments)、请求主机名;
  4. 对于POST、PUT等非GET请求,也会解析请求头、请求参数、请求主机名;,然后根据Content_Type的类型去匹配不同的读取数据方法。如果 Content_Type 是 multipart/form-data,那么会处理表单数据,需用到boundaryStr(特别地,如果涉及到文件上传功能,这会在文件上传处理过程中回调我们注册进去的文件上传处理回调函数,请读者自行往上翻阅);如果 Content_Type 属于其他的,则直接读取处理;
  • 最后,匹配可以处理该请求的方法:**_handleRequest();在该方法中会回调我们在第2步找到的 requestHandler,requestHandler会回调我们注册进去的对应请求的回调函数**;
  • 至此,整体的Http请求解析完成;

2.4 响应client请求方法

    当我们经过handleClient()解析完http请求之后,我们就可以在requestHandler设置的请求处理回调函数里面获得http请求的具体信息,然后根据具体信息给到对应的响应信息。那么,我们可以在回调函数里面做什么呢?

2.4.1 upload() —— 处理文件上传

函数说明:

/**
 * 获取文件上传处理对象
 */
HTTPUpload& upload();

我们来看看HTTPUpload的定义:

typedef struct {
  HTTPUploadStatus status;//上传文件的状态
  String  filename;//文件名字
  String  name;
  String  type;//文件类型
  size_t  totalSize;    // 文件大小
  size_t  currentSize;  // size of data currently in buf
  uint8_t buf[HTTP_UPLOAD_BUFLEN];//缓冲区,这里就是我们需要处理的重点
} HTTPUpload;

实例使用(在文件上传处理函数里面):

//实例说明 非完整代码,无法直接运行,理解即可
/**
 * 处理文件上传 HandlerFunction
 * 此方法会在文件上传过程中多次回调,我们可以判断上传状态
 */
void handleFileUpload() {
  //判断http requestUri
  if (server.uri() != "/edit") {
    return;
  }
  //获得 Http上传文件处理对象
  HTTPUpload& upload = server.upload();
  //文件开始上传
  if (upload.status == UPLOAD_FILE_START) {
    String filename = upload.filename;
    if (!filename.startsWith("/")) {
      filename = "/" + filename;
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename);
    //本地文件系统创建一个文件用来保存内容
    fsUploadFile = SPIFFS.open(filename, "w");
    filename = String();
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    //文件开始写入文件
    //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize);
    if (fsUploadFile) {
      //写入文件
      fsUploadFile.write(upload.buf, upload.currentSize);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    //文件上传结束
    if (fsUploadFile) {
      fsUploadFile.close();
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize);
  }
}

//注册文件上传处理回调
server.on("/edit", HTTP_POST, []() {
    server.send(200, "text/plain", "");
  }, handleFileUpload);

2.4.2 sendHeader() —— 设置响应头

函数说明:

/**
 * 设置响应头
 * @param name 响应头key
 * @param value 响应头value
 * @param first 是否需要放在第一行
 */
void sendHeader(const String& name, const String& value, bool first = false);

2.4.3 setContentLength() —— 设置响应体长度

函数说明:

/**
 * 设置响应内容长度
 * @param contentLength 长度
 * 重大注意点:对于不知道长度调用server.setContentLength(CONTENT_LENGTH_UNKNOWN);
 *             然后调用若干个server.sendContent(),最后需要关闭与client的短连接(close)以表示内容结束。
 */
void setContentLength(const size_t contentLength);

2.4.4 sendContent()/sendContent_P() —— 设置响应内容

函数说明:

/**
 * 发送响应内容
 * @param content 响应内容
 */
void sendContent(const String& content);
void sendContent_P(PGM_P content);
void sendContent_P(PGM_P content, size_t size);

2.4.5 requestAuthentication() —— 请求client认证

函数说明:

/**
 * 请求client认证(Authorization)
 * @param  mode  HTTPAuthMethod  (验证方式,默认BASIC_AUTH)
 * @param  realm const char*
 * @param  authFailMsg const String
 */
void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String("") );

2.4.6 streamFile() —— 发送响应文件流

函数说明:

/**
 * 发送响应文件流
 * @param  file  具体文件
 * @param  contentType 响应类型
 * @param  authFailMsg const String
 */
size_t streamFile(T &file, const String& contentType);

注意点:

  • 该函数需要结合FS来讲解,本篇章略;

2.4.7 send() —— 发送响应数据

这个是我们发送响应数据的核心,需要仔细了解;
函数说明:

/**
 * 发送响应数据
 * @param code 响应状态码
 * @param content_type 响应内容类型
 * @param content 具体响应内容
 */
void send(int code, const char* content_type = NULL, const String& content = String(""));
void send(int code, char* content_type, const String& content);
void send(int code, const String& content_type, const String& content);
void send_P(int code, PGM_P content_type, PGM_P content);
void send_P(int code, PGM_P content_type, PGM_P content, size_t contentLength);

我们这里分析send的源码:

void ESP8266WebServer::send(int code, const char* content_type, const String& content) {
    String header;
    //拼装响应头
    _prepareHeader(header, code, content_type, content.length());
    //发送响应头
    _currentClientWrite(header.c_str(), header.length());
    if(content.length())
    //发送响应内容
      sendContent(content);
}

//发送响应头
void ESP8266WebServer::_prepareHeader(String& response, int code, const char* content_type, size_t contentLength) {
    // http协议版本
    response = String(F("HTTP/1.")) + String(_currentVersion) + ' ';
    //响应码
    response += String(code);
    response += ' ';
    response += _responseCodeToString(code);
    response += "\r\n";

    using namespace mime;
    //响应类型
    if (!content_type)
        content_type = mimeTable[html].mimeType;

    sendHeader(String(F("Content-Type")), String(FPSTR(content_type)), true);
    //响应内容长度
    if (_contentLength == CONTENT_LENGTH_NOT_SET) {
        sendHeader(String(FPSTR(Content_Length)), String(contentLength));
    } else if (_contentLength != CONTENT_LENGTH_UNKNOWN) {
        sendHeader(String(FPSTR(Content_Length)), String(_contentLength));
    } else if(_contentLength == CONTENT_LENGTH_UNKNOWN && _currentVersion){ //HTTP/1.1 or above client
      //let's do chunked
      _chunked = true;
      sendHeader(String(F("Accept-Ranges")),String(F("none")));
      sendHeader(String(F("Transfer-Encoding")),String(F("chunked")));
    }
    sendHeader(String(F("Connection")), String(F("close")));

    response += _responseHeaders;
    response += "\r\n";
    _responseHeaders = "";
}

/**
 * 发送响应内容
 */
void ESP8266WebServer::sendContent(const String& content) {
  const char * footer = "\r\n";
  size_t len = content.length();
  if(_chunked) {
    char * chunkSize = (char *)malloc(11);
    if(chunkSize){
      sprintf(chunkSize, "%x%s", len, footer);
      _currentClientWrite(chunkSize, strlen(chunkSize));
      free(chunkSize);
    }
  }
  _currentClientWrite(content.c_str(), len);
  if(_chunked){
    _currentClient.write(footer, 2);
    if (len == 0) {
      _chunked = false;
    }
  }
}

3. 实例操作

讲了那么多的理论知识,该开始实际操作了,请看以下几个例子。

3.1 演示webserver的基础功能

实验说明:

  • 演示webserver的基础功能,wifi模块连接上热点之后,在pc浏览器输入serverip+uri来访问

源码:

/**
 * Demo:
 *    演示webserver基础功能
 *    (当wifi模块连接上ap之后,在pc浏览器中输入ip+uri来访问)
 * @author 单片机菜鸟
 * @date 2019/09/10
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
 
//以下三个定义为调试定义
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* AP_SSID     = "TP-LINK_5344";         // XXXXXX -- 使用时请修改为当前你的 wifi ssid
const char* AP_PSK = "6206908you11011010";         // XXXXXX -- 使用时请修改为当前你的 wifi 密码
const unsigned long BAUD_RATE = 115200;                   // serial connection speed
 
//声明一下函数
void initBasic(void);
void initWifi(void);
void initWebServer(void);
 
ESP8266WebServer server(80);//创建一个webserver
 
/**
 * 处理根目录uri请求
 * uri:http://server_ip/
 */
void handleRoot() {
  server.send(200, "text/plain", "hello from esp8266!");
}
 
/**
 * 处理无效uri
 * uri:http://server_ip/xxxx
 */
void handleNotFound() {
  //打印无效uri的信息 包括请求方式 请求参数
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
 
void setup(void) {
  initBasic();
  initWifi();
  initWebServer();
}
 
/**
 * 初始化基础功能:波特率
 */
void initBasic(){
  DebugBegin(BAUD_RATE);
}
 
/**
 * 初始化wifi模块:工作模式 连接网络
 */
void initWifi(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(AP_SSID, AP_PSK);
  DebugPrintln("");
 
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrint("Connected to ");
  DebugPrintln(AP_SSID);
  DebugPrint("IP address: ");
  DebugPrintln(WiFi.localIP());
}
 
/**
 * 初始化webserver
 */
void initWebServer(){
  //以下配置uri对应的handler
  server.on("/", handleRoot);
 
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });
 
  server.onNotFound(handleNotFound);
  //启动webserver
  server.begin();
  DebugPrintln("HTTP server started");
}
 
void loop(void) {
  server.handleClient();
}

实验结果:

image

image

image

3.2 演示webserver返回html功能

实验说明:

  • 演示webserver返回html功能,wifi模块连接上热点之后,在pc浏览器输入serverip+uri来访问

源码:

/**
 * Demo:
 *    演示webserver html功能
 *    (当wifi模块连接上ap之后,在pc浏览器中输入ip+uri来访问)
 * @author 单片机菜鸟
 * @date 2019/09/10
 */
 
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
 
//以下三个定义为调试定义
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* AP_SSID     = "TP-LINK_5344";         // XXXXXX -- 使用时请修改为当前你的 wifi ssid
const char* AP_PSK = "6206908you11011010";         // XXXXXX -- 使用时请修改为当前你的 wifi 密码
const unsigned long BAUD_RATE = 115200;                   // serial connection speed
 
//声明一下函数
void initBasic(void);
void initWifi(void);
void initWebServer(void);
 
ESP8266WebServer server(80);
 
/**
 * 处理根目录uri请求
 * uri:http://server_ip/
 */
void handleRoot() {
  char temp[400];
  int sec = millis() / 1000;
  int min = sec / 60;
  int hr = min / 60;
 
  snprintf(temp, 400,
 
           "<html>\
  <head>\
    <meta http-equiv='refresh' content='5'/>\
    <title>ESP8266 Demo</title>\
    <style>\
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>\
  </head>\
  <body>\
    <h1>Hello from ESP8266!</h1>\
    <p>Uptime: %02d:%02d:%02d</p>\
    <img src=\"/test.svg\" />\
  </body>\
</html>",
 
           hr, min % 60, sec % 60
          );
  server.send(200, "text/html", temp);
}
 
/**
 * 处理无效uri
 * uri:http://server_ip/xxxx
 */
void handleNotFound() {
  //打印无效uri的信息 包括请求方式 请求参数
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
 
void setup(void) {
  initBasic();
  initWifi();
  initWebServer();
}
 
void loop(void) {
  server.handleClient();
}
 
/**
 * 初始化基础功能:波特率
 */
void initBasic(){
  DebugBegin(BAUD_RATE);
}
 
/**
 * 初始化wifi模块:工作模式 连接网络
 */
void initWifi(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(AP_SSID, AP_PSK);
  DebugPrintln("");
 
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrint("Connected to ");
  DebugPrintln(AP_SSID);
  DebugPrint("IP address: ");
  DebugPrintln(WiFi.localIP());
}
 
/**
 * 初始化webserver
 */
void initWebServer(){
  //以下配置uri对应的handler
  server.on("/", handleRoot);
 
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });
  server.on("/test.svg", drawGraph);
  server.onNotFound(handleNotFound);
  //启动webserver
  server.begin();
  DebugPrintln("HTTP server started");
}
 
/**
 * 画图
 */
void drawGraph() {
  String out = "";
  char temp[100];
  out += "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"400\" height=\"150\">\n";
  out += "<rect width=\"400\" height=\"150\" fill=\"rgb(250, 230, 210)\" stroke-width=\"1\" stroke=\"rgb(0, 0, 0)\" />\n";
  out += "<g stroke=\"black\">\n";
  int y = rand() % 130;
  for (int x = 10; x < 390; x += 10) {
    int y2 = rand() % 130;
    sprintf(temp, "<line x1=\"%d\" y1=\"%d\" x2=\"%d\" y2=\"%d\" stroke-width=\"1\" />\n", x, 140 - y, x + 10, 140 - y2);
    out += temp;
    y = y2;
  }
  out += "</g>\n</svg>\n";
 
  server.send(200, "image/svg+xml", out);
}

实验结果:

image

image

3.3 演示webserver校验

实验说明:

  • 演示webserver校验帐号密码功能,Authenticate请求头

源码:

/**
 * Demo:
 *    演示webserver auth校验功能
 *    (当wifi模块连接上ap之后,在pc浏览器中输入ip+uri来访问,不过需要带校验请求头)
 * @author 单片机菜鸟
 * @date 2019/09/10
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
 
//以下三个定义为调试定义
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* AP_SSID     = "TP-LINK_5344";         // XXXXXX -- 使用时请修改为当前你的 wifi ssid
const char* AP_PSK = "xxxx";         // XXXXXX -- 使用时请修改为当前你的 wifi 密码
const unsigned long BAUD_RATE = 115200;                   // serial connection speed
const char* www_username = "admin";
const char* www_password = "esp8266";
 
//声明一下函数
void initBasic(void);
void initWifi(void);
void initWebServer(void);
 
ESP8266WebServer server(80);//创建webserver
 
void setup() {
  initBasic();
  initWifi();
  initWebServer();
}
 
void loop() {
  server.handleClient();
}
 
/**
 * 初始化基础功能:波特率
 */
void initBasic(){
  DebugBegin(BAUD_RATE);
}
 
/**
 * 初始化wifi模块:工作模式 连接网络
 */
void initWifi(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(AP_SSID, AP_PSK);
  DebugPrintln("");
 
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrint("Connected to ");
  DebugPrintln(AP_SSID);
  DebugPrint("IP address: ");
  DebugPrintln(WiFi.localIP());
}
 
/**
 * 初始化webserver
 */
void initWebServer(){
  //以下配置uri对应的handler
  server.on("/", []() {
    //校验帐号和密码
    if (!server.authenticate(www_username, www_password)) {
      return server.requestAuthentication();
    }
    server.send(200, "text/plain", "Login OK");
  });
  server.begin();
 
  DebugPrint("Open http://");
  DebugPrint(WiFi.localIP());
  DebugPrintln("/ in your browser to see it working");
}

实验结果:

image

image

image

3.4 演示webserver登陆功能

实验说明:

  • 演示webserver登陆功能,html登陆页面

源码:

/**
 * Demo:
 *    演示webserver auth校验功能
 *    (当wifi模块连接上ap之后,在pc浏览器中输入ip+uri来访问,不过需要带校验请求头)
 * @author 单片机菜鸟
 * @date 2019/09/10
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
 
//以下三个定义为调试定义
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* AP_SSID     = "TP-LINK_5344";         // XXXXXX -- 使用时请修改为当前你的 wifi ssid
const char* AP_PSK = "6206908you11011010";         // XXXXXX -- 使用时请修改为当前你的 wifi 密码
const unsigned long BAUD_RATE = 115200;                   // serial connection speed
 
//声明一下函数
void initBasic(void);
void initWifi(void);
void initWebServer(void);
 
ESP8266WebServer server(80);
 
/**
 * 校验是否存在cookie头并且cookie头的值是正确的
 */
bool is_authentified() {
  DebugPrintln("Enter is_authentified");
  //是否存在cookie头
  if (server.hasHeader("Cookie")) {
    DebugPrint("Found cookie: ");
    //获取cookie头的信息
    String cookie = server.header("Cookie");
    DebugPrintln(cookie);
    if (cookie.indexOf("ESPSESSIONID=1") != -1) {
      DebugPrintln("Authentification Successful");
      return true;
    }
  }
  DebugPrintln("Authentification Failed");
  return false;
}
 
/**
 * 处理登陆uri
 */
void handleLogin() {
  String msg;
  //判断是否存在cookie头
  if (server.hasHeader("Cookie")) {
    DebugPrint("Found cookie: ");
    String cookie = server.header("Cookie");
    DebugPrint(cookie);
  }
  //判断是否存在DISCONNECT参数
  if (server.hasArg("DISCONNECT")) {
    DebugPrintln("Disconnection");
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.sendHeader("Set-Cookie", "ESPSESSIONID=0");
    server.send(301);
    return;
  }
  //判断是否存在USERNAME和PASSWORD参数
  if (server.hasArg("USERNAME") && server.hasArg("PASSWORD")) {
    if (server.arg("USERNAME") == "admin" &&  server.arg("PASSWORD") == "admin") {
      server.sendHeader("Location", "/");
      server.sendHeader("Cache-Control", "no-cache");
      server.sendHeader("Set-Cookie", "ESPSESSIONID=1");
      server.send(301);
      DebugPrintln("Log in Successful");
      return;
    }
    msg = "Wrong username/password! try again.";
    DebugPrintln("Log in Failed");
  }
  //返回html 填写账号密码页面
  String content = "<html><body><form action='/login' method='POST'>To log in, please use : admin/admin<br>";
  content += "User:<input type='text' name='USERNAME' placeholder='user name'><br>";
  content += "Password:<input type='password' name='PASSWORD' placeholder='password'><br>";
  content += "<input type='submit' name='SUBMIT' value='Submit'></form>" + msg + "<br>";
  content += "You also can go <a href='/inline'>here</a></body></html>";
  server.send(200, "text/html", content);
}
 
/**
 * 根目录处理器
 */
//root page can be accessed only if authentification is ok
void handleRoot() {
  DebugPrintln("Enter handleRoot");
  String header;
  if (!is_authentified()) {
    //校验不通过
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(301);
    return;
  }
  String content = "<html><body><H2>hello, you successfully connected to esp8266!</H2><br>";
  if (server.hasHeader("User-Agent")) {
    content += "the user agent used is : " + server.header("User-Agent") + "<br><br>";
  }
  content += "You can access this page until you <a href=\"/login?DISCONNECT=YES\">disconnect</a></body></html>";
  server.send(200, "text/html", content);
}
 
/**
 * 无效uri处理器
 */
void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}
 
void setup(void) {
  initBasic();
  initWifi();
  initWebServer();
}
 
void loop(void) {
  server.handleClient();
}
 
/**
 * 初始化基础功能:波特率
 */
void initBasic(){
  DebugBegin(BAUD_RATE);
}
 
/**
 * 初始化wifi模块:工作模式 连接网络
 */
void initWifi(){
  WiFi.mode(WIFI_STA);
  WiFi.begin(AP_SSID, AP_PSK);
  DebugPrintln("");
 
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrint("Connected to ");
  DebugPrintln(AP_SSID);
  DebugPrint("IP address: ");
  DebugPrintln(WiFi.localIP());
}
 
/**
 * 初始化webserver
 */
void initWebServer(){
  //以下配置uri对应的handler
  
  server.on("/", handleRoot);
  server.on("/login", handleLogin);
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works without need of authentification");
  });
 
  server.onNotFound(handleNotFound);
  //设置需要收集的请求头
  const char * headerkeys[] = {"User-Agent", "Cookie"} ;
  size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);
  //收集头信息
  server.collectHeaders(headerkeys, headerkeyssize);
  server.begin();
  DebugPrintln("HTTP server started");
}

4. 总结

这一篇章,主要讲解了Http协议的webserver。本篇和Httpclient篇都是比较重要的篇章,希望读者仔细研读;

posted @ 2019-06-18 22:41  单片机菜鸟  阅读(...)  评论(... 编辑 收藏