ESP8266開發之旅 網絡篇⑪ WebServer——ESP8266WebServer庫的使用

授人以魚不如授人以漁,目的不是爲了教會你具體項目開發,而是學會學習的能力。但願你們分享給你周邊須要的朋友或者同窗,說不定大神成長之路有博哥的奠定石。。。html

QQ技術互動交流羣:ESP8266&32 物聯網開發 羣號622368884,不喜勿噴web

1、你若是想學基於Arduino的ESP8266開發技術

1、基礎篇json

  1. ESP8266開發之旅 基礎篇① 走進ESP8266的世界
  2. ESP8266開發之旅 基礎篇② 如何安裝ESP8266的Arduino開發環境
  3. ESP8266開發之旅 基礎篇③ ESP8266與Arduino的開發說明
  4. ESP8266開發之旅 基礎篇④ ESP8266與EEPROM
  5. ESP8266開發之旅 基礎篇⑤ ESP8266 SPI通訊和I2C通訊
  6. ESP8266開發之旅 基礎篇⑥ Ticker——ESP8266定時庫

2、網絡篇設計模式

  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固件更新

3、應用篇瀏覽器

  1. ESP8266開發之旅 應用篇① 局域網應用 ——炫酷RGB彩燈
  2. ESP8266開發之旅 應用篇② OLED顯示天氣屏
  3. ESP8266開發之旅 應用篇③ 簡易版WiFi小車

4、高級篇cookie

  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的蹤影。app

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

WiFiServer  _server;

WiFiClient  _currentClient;
.............後面省略代碼

2. ESP8266WebServer庫

    若是讀者有下載源碼的話,那麼能夠看到ESP8266WebServer庫 目錄以下:
imagetcp

    老規矩,先上一個博主總結的百度腦圖:
imageide

整體上,根據功能能夠把方法分爲兩大類:

  • 管理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篇都是比較重要的篇章,但願讀者仔細研讀;

相關文章
相關標籤/搜索