ESP8266開發之旅 網絡篇⑦ TCP Server & TCP Client

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

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

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

1、基礎篇git

  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、網絡篇github

  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、應用篇web

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

4、高級篇算法

  1. ESP8266開發之旅 進階篇① 代碼優化 —— ESP8266內存管理
  2. ESP8266開發之旅 進階篇② 閒聊Arduino IDE For ESP8266配置
  3. ESP8266開發之旅 進階篇③ 閒聊 ESP8266 Flash
  4. ESP8266開發之旅 進階篇④ 常見問題 —— 解決困擾
  5. ESP8266開發之旅 進階篇⑤ 代碼規範 —— 像寫文章同樣優美
  6. ESP8266開發之旅 進階篇⑥ ESP-specific APIs說明

1. 前言

    一般,爲了讓手機連上一個WiFi熱點,基本上都是打開手機設置裏面的WiFi設置功能,而後會看到裏面有個WiFi熱點列表,而後選擇你要的鏈接上去。
    基本上你只要打開手機鏈接WiFi功能,都會發現附近有超級多的各類來路不明的WiFi熱點(鏈接有風險需謹慎),那麼手機是怎麼知道附近的WiFi的呢?
    一般,無線網絡提供的WiFi熱點,大部分都開放了SSID廣播(記得以前樓主講過WiFi熱點也能夠隱藏的),Scan WiFi的功能就是掃描出全部附近的WiFi熱點的SSID信息,這樣一來,客戶端就能夠根據須要選擇不一樣的SSID連入對應的無線網絡中。
    在前面章節裏面,博主講解了ESP8266WiFi庫裏面的一些重要內容。這裏回顧一下博主講了哪些重要內容:json

  1. ESP8266WiFiSTA庫 ------ STA模式專用庫
  2. ESP8266WiFiAP庫 ------ soft-AP模式專用庫
  3. ESP8266WiFiScan庫 ------ WiFi掃描功能庫
  4. ESP8266WiFiGeneric庫 ------ WiFi基礎功能庫(WiFi事件、WiFi模式)
  5. WiFi模塊的工做模式:STA模式、soft-AP模式和STA兼soft-AP模式

注意點:後端

  • 這些功能的引入都是一句簡單的代碼
#include <ESP8266WiFi.h>

    固然,ESP8266WiFi庫裏面還有其餘重要內容,好比跟http相關的 WiFiClientWiFiServer,跟https相關的 WiFiClientSecureWiFiServerSecure
    終於,到這篇,能夠看到跟網絡請求有關的東西了。
    那確定就會有不少人會問:到底何時用到哪一個呢?
    在這裏,博主給你們歸納瞭如下幾點,但願深刻理解核心:api

  1. WiFi工做模式設置跟網絡請求無關,決定於ESP8266模塊想以什麼角色接入網絡中。
  • 若是ESP8266只是想靜靜地作個美男子,不想別人鏈接你,只是想一味地獲取,那麼你就果斷設置成STA模式;
  • 若是ESP8266想作箇中央空調服務大衆收集大衆的需求,那麼你就果斷設置成soft-AP模式;
  • WiFi工做模式,博主理解爲「物理結構」模式;
  1. 至因而client仍是Server,取決於ESP8266開發需求;
  • 若是業務要求是獲取其餘server提供的數據(發送請求,好比請求天氣信息),那麼你就可使用Client模式;
  • 若是業務要求是別人請求你獲取某些數據(web請求),那麼你可使用Server模式;
  • client or server,取決於你的業務需求;

    這一章節,咱們講講解兩大模塊:數組

  • TCP client,對應 WiFiClient
  • TCP Server,對應 WiFiServer 庫。

    至於什麼是TCP傳輸協議,你們執行查資料吧。

  • TCP是底層通信協議,定義的是數據傳輸和鏈接方式的規範;
  • HTTP是應用層協議,定義的是傳輸數據的內容的規範;
  • HTTP協議中的數據是利用TCP協議傳輸的,因此支持HTTP也就必定支持TCP;

2. TCP client

概念圖:
image
    client,又名客戶端,也就是須要經過獲取server提供的服務數據來展現本身。Tcp client,只是架構在tcp協議之上的客戶端。上圖中,ESP8266做爲client端,經過路由,訪問局域網內的Pc server或者廣域網下的網絡服務器信息,server收到請求後會處理請求而且把響應數據返回以供ESP8266使用。

3. WiFiClient庫

    博主總結了 WiFiClient 百度腦圖:
image
    總體上來講,方法能夠分爲4類:

  • 第一類方法,鏈接操做;
  • 第二類方法,發送請求操做;
  • 第三類方法,響應操做;
  • 第四類方法,普通設置;

3.1 鏈接操做

3.1.1 connect - 啓動tcp鏈接

函數說明:

/**
 * 創建一個tcp鏈接
 * @param ip    IPAddress of tcpserver
 * @param port  port of tcpserver
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(IPAddress ip, uint16_t port);

/**
 * 創建一個tcp鏈接
 * @param host    host of tcpserver (192.xx.xx.xx)
 * @param port  port of tcpserver
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(const char *host, uint16_t port)

/**
 * 創建一個tcp鏈接
 * @param host    host of tcpserver (192.xx.xx.xx)
 * @param port  port of tcpserver
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(const String host, uint16_t port);

3.1.2 connected - 判斷client是否還在鏈接

函數說明:

/**
 * 判斷tcp鏈接是否創建起來(ESTABLISHED)
 * @return  result of tcp connect
 *           1 --- success
 *           0 --- fail
 */
uint8_t connected();

3.1.3 stop - 中止tcp鏈接

函數說明:

/**
 * 關閉tcp鏈接
 */
void stop();

3.1.4 status - 鏈接狀態

函數說明:

/**
 * 獲取tcp鏈接狀態
 * @return  result of tcp connect
 *          CLOSED      = 0,
 *          LISTEN      = 1,
 *          SYN_SENT    = 2,
 *          SYN_RCVD    = 3,
 *          ESTABLISHED = 4,
 *          FIN_WAIT_1  = 5,
 *          FIN_WAIT_2  = 6,
 *          CLOSE_WAIT  = 7,
 *          CLOSING     = 8,
 *          LAST_ACK    = 9,
 *          TIME_WAIT   = 10
 */
uint8_t status();

3.2 發送數據操做

發送操做的源碼能夠查閱 Print.cpp

3.2.1 write - 發送數據到client鏈接的server

函數說明:

/**
 * 發送數據
 * @param str 須要單個字節
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t write(uint8_t);

/**
 * 發送數據
 * @param str 須要發送字符串或者字符數組
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t write(const char *str);

/**
 * 發送數據
 * @param buffer 須要發送字符串或者字符數組
 * @param size 數據字節數
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t write(const char *buffer, size_t size)

/**
 * 發送數據
 * @param stream 數據流,好比文件流
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t write(Stream& stream);

注意點:

  • write(uint8_t)函數是發送數據的底層方法,也就是說print、println底層也是調用write;
  • write(const char str) 函數底層是調用 write(const char buffer, size_t size),經過strlen計算長度;
size_t write(const char *str) {
    if(str == NULL)
        return 0;
    return write((const uint8_t *) str, strlen(str));
}

函數說明:

/**
 * 發送數據
 * @param FlashStringHelper 須要發送的字符串,字符串存在flash中(PROGMEM)
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t print(const __FlashStringHelper *);

/**
 * 發送數據
 * @param String 須要發送的字符串,字符串存在內存中
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t print(const String &);

/**
 * 發送數據
 * @param String 須要發送的字符數組,字符數組存在內存中
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t print(const char[]);

/**
 * 發送數據
 * @param String 須要發送的字符
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t print(char);


/**
 * 發送數據
 * @param String 須要發送的數據,可能是數字,轉成對應的進制,通常都是傳輸數字型數據
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t print(unsigned char, int = DEC);
size_t print(int, int = DEC);
size_t print(unsigned int, int = DEC);
size_t print(long, int = DEC);
size_t print(unsigned long, int = DEC);
size_t print(double, int = 2);

注意點:

  • 讀者須要特別關注 print(const __FlashStringHelper *) 這個函數,之後代碼內存優化需用用到;
    常見用法:
//實例代碼 非完整代碼 不可直接使用 理解便可
WiFiClient client;
client.print( F("This is an flash string")); //字符串「This is an flash string」存在於flash

3.2.3 println - 發送數據到client鏈接的server

函數說明:

/**
 * 發送數據,而且加上換行符 "\r\n"
 * @param FlashStringHelper 須要發送的字符串,字符串存在flash中(PROGMEM)
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(const __FlashStringHelper *);

/**
 * 發送數據,而且加上換行符 "\r\n"
 * @param String 須要發送的字符串,字符串存在內存中
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(const String &s);

/**
 * 發送數據,而且加上換行符 "\r\n"
 * @param String 須要發送的字符數組,字符數組存在內存中
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(const char[]);

/**
 * 發送數據,而且加上換行符 "\r\n"
 * @param String 須要發送的字符
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(char);

/**
 * 發送數據,而且加上換行符 "\r\n"
 * @param String 須要發送的數據,可能是數字,轉成對應的進制,通常都是傳輸數字型數據
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(unsigned char, int = DEC);
size_t println(int, int = DEC);
size_t println(unsigned int, int = DEC);
size_t println(long, int = DEC);
size_t println(unsigned long, int = DEC);
size_t println(double, int = 2);

/**
 * 發送換行符 "\r\n"
 * @return size_t 成功寫入發送緩衝區的字節數
 */
size_t println(void);

注意點:

  • println系列其實就是在print系列的基礎上加上了換行符 "\r\n";

3.3 響應操做

3.3.1 available() - 返回接收緩存區可讀取字節數

函數說明:

/**
 * 返回接收緩存區可讀取字節數
 * @return int 接收緩衝區可讀取字節數
 */
int available();

注意點:

  • 經過此方法,咱們能夠判斷髮送出去的請求是否有響應信息;

3.3.2 availableForWrite() - 返回發送緩衝區剩餘可寫字節數

函數說明:

/**
 * 返回發送緩衝區剩餘可寫字節數
 * @return int 發送緩衝區剩餘可寫字節數
 */
size_t availableForWrite();

注意點:

  • 通常來講,調用發送數據操做以後,並不會馬上發送出去,而是把數據放入發送緩衝區,經過機制不斷讀取發送緩衝區的數據不斷髮送出去;
  • 能夠經過此函數判斷請求是否發送完畢;

3.3.3 read() - 讀取接收緩衝區一個字節

函數說明:

/**
 * 讀取接收緩衝區一個字節
 * @return int 一字節數據
 */
int read();

注意點:

  • 此函數讀取完數據後,會把該數據從緩衝區清掉;

3.3.4 read(buf,size) - 讀取接收緩衝區size大小的字節數據

函數說明:

/**
 * 讀取接收緩衝區size大小的字節數據
 * @param buf 數據存儲到該buf
 * @param size 讀取大小
 * @return int 成功讀取的大小
 */
int read(uint8_t *buf, size_t size);

注意點:

  • 此函數讀取完數據後,會把該數據從緩衝區清掉;

3.3.5 peek() - 讀取接收緩衝區一個字節

函數說明:

/**
 * 讀取接收緩衝區一個字節
 * @return int 一字節數據
 */
int peek();

注意點:

  • 此函數讀取完數據後,不會把該數據從緩衝區清掉,因此須要特別關注這一點;

3.3.6 peekBytes(buf,size) - 讀取接收緩衝區size大小的字節數據

函數說明:

/**
 * 讀取接收緩衝區length大小的字節數據
 * @param buffer 數據存儲到該 buffer
 * @param length 讀取大小
 * @return size_t 成功讀取的大小
 */
size_t peekBytes(uint8_t *buffer, size_t length);
size_t peekBytes(char *buffer, size_t length);

注意點:

  • 此函數讀取完數據後,不會把該數據從緩衝區清掉,因此須要特別關注這一點;

3.3.7 readStringUntil - 讀取響應數據直到某個字符串爲止

函數說明:

/**
 * 讀取響應數據直到某個字符串爲止
 * @param end 結束字符
 * @return String 讀取成功的字符串
 */
String readStringUntil(char end);

3.3.8 find - 查找某個字符串

函數說明:

/**
 * 判斷是否存在某個目標字符串
 * @param buffer 目標字符串
 * @return bool 存在返回true
 */
bool find(char *buffer);

注意點:

  • 此函數會把數據從緩衝區清掉;

3.3.9 flush - 清除接收緩衝區

函數說明:

/**
 * 清除緩衝區
 */
void flush(void);

注意點:

  • 新版本flush功能是等待緩衝區中的全部傳出字符都已發送。因此作不了清除緩衝區的做用;
    能夠有如下代替:
while(client.read()>0);

方法要點

  • 博主建議你們儘可能用批量處理的方法,好比 readStringUntil、read(buf,size)、peekBytes(buf,length),性能方面會好不少;
  • 博主經過查看源碼,發現client的發送緩衝區的大小是256Bytes;

3.4 普通設置

3.4.1 setNoDelay - 是否禁用 Nagle 算法。

函數說明:

/**
 * 是否禁用 Nagle 算法。
 * @param nodelay true表示禁用 Nagle 算法
 */
void setNoDelay(bool nodelay);

底層源碼:

void setNoDelay(bool nodelay)
{
        if(!_pcb) {
            return;
        }
        if(nodelay) {
            tcp_nagle_disable(_pcb);
        } else {
            tcp_nagle_enable(_pcb);
        }
}

注意點:

  • Nagle 算法的目的是經過合併一些小的發送消息,而後一次性發送全部的消息來減小經過網絡發送的小數據包的tcp/ip流量。這種方法的缺點是延遲了單個消息的發送,直到一個足夠大的包被組裝。

4. 實例操做

    前面講了這麼多理論內容,接下來用幾個例子來講明一下。

4.1 演示 WiFiClient 與 TCP server 之間的通訊功能

例子介紹:
    本實驗演示 WiFiClient 與 TCP server 之間的通訊功能,須要使用到TCP調試助手,請在TCP調試助手上創建一個Tcp server,ip地址是192.168.1.102,端口號是8234。

源碼:

/**
 * Demo:
 *    STA模式下,演示WiFiClient與TCP server之間的通訊功能
 *    本實驗須要跟TCP調試助手一塊兒使用。
 * @author 單片機菜鳥
 * @date 2019/1/25
 */
#include <ESP8266WiFi.h>
 
//如下三個定義爲調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
#define AP_SSID "TP-LINK_5344" //這裏改爲你的wifi名字
#define AP_PSW  "xxxxxxx"//這裏改爲你的wifi密碼
 
const uint16_t port = 8234;
const char * host = "192.168.1.102"; // ip or dns
WiFiClient client;//建立一個tcp client鏈接
 
void setup() {
  //設置串口波特率,以便打印信息
  DebugBegin(115200);
  //延時5s 爲了演示效果
  delay(5000);
  // 我不想別人鏈接我,只想作個站點
  WiFi.mode(WIFI_STA);
  WiFi.begin(AP_SSID,AP_PSW);
 
  DebugPrint("Wait for WiFi... ");
  //等待wifi鏈接成功
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
 
  DebugPrintln("");
  DebugPrintln("WiFi connected");
  DebugPrint("IP address: ");
  DebugPrintln(WiFi.localIP());
 
  delay(500);
}
 
void loop() {
  
  DebugPrint("connecting to ");
  DebugPrintln(host);
 
  if (!client.connect(host, port)) {
    DebugPrintln("connection failed");
    DebugPrintln("wait 5 sec...");
    delay(5000);
    return;
  }
 
  // 發送數據到Tcp server
  DebugPrintln("Send this data to server");
  client.println(String("Send this data to server"));
 
  //讀取從server返回到響應數據
  String line = client.readStringUntil('\r');
  DebugPrintln(line);
 
  DebugPrintln("closing connection");
  client.stop();
 
  DebugPrintln("wait 5 sec...");
  delay(5000);
}

測試結果:
image

4.2 演示 Http請求天氣接口信息

例子介紹:
    經過TCP client包裝Http請求協議去調用天氣接口獲取天氣信息
源碼:

/**
 * Demo:
 *    演示Http請求天氣接口信息
 * @author 單片機菜鳥
 * @date 2019/09/04
 */
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
 
//如下三個定義爲調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* ssid     = "TP-LINK_5344";         // XXXXXX -- 使用時請修改成當前你的 wifi ssid
const char* password = "6206908you11011010";         // XXXXXX -- 使用時請修改成當前你的 wifi 密碼
const char* host = "api.seniverse.com";
const char* APIKEY = "wcmquevztdy1jpca";        //API KEY
const char* city = "guangzhou";
const char* language = "zh-Hans";//zh-Hans 簡體中文  會顯示亂碼
  
const unsigned long BAUD_RATE = 115200;                   // serial connection speed
const unsigned long HTTP_TIMEOUT = 5000;               // max respone time from server
const size_t MAX_CONTENT_SIZE = 1000;                   // max size of the HTTP response
 
// 咱們要今後網頁中提取的數據的類型
struct WeatherData {
  char city[16];//城市名稱
  char weather[32];//天氣介紹(多雲...)
  char temp[16];//溫度
  char udate[32];//更新時間
};
  
WiFiClient client;
char response[MAX_CONTENT_SIZE];
char endOfHeaders[] = "\r\n\r\n";
 
void setup() {
  // put your setup code here, to run once:
  WiFi.mode(WIFI_STA);     //設置esp8266 工做模式
  DebugBegin(BAUD_RATE);
  DebugPrint("Connecting to ");//寫幾句提示,哈哈
  DebugPrintln(ssid);
  WiFi.begin(ssid, password);   //鏈接wifi
  WiFi.setAutoConnect(true);
  while (WiFi.status() != WL_CONNECTED) {
    //這個函數是wifi鏈接狀態,返回wifi連接狀態
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrintln("WiFi connected");
  delay(500);
  DebugPrintln("IP address: ");
  DebugPrintln(WiFi.localIP());//WiFi.localIP()返回8266得到的ip地址
  client.setTimeout(HTTP_TIMEOUT);
}
 
void loop() {
  // put your main code here, to run repeatedly:
  //判斷tcp client是否處於鏈接狀態,不是就創建鏈接
  while (!client.connected()){
     if (!client.connect(host, 80)){
         DebugPrintln("connection....");
         delay(500);
     }
  }
  //發送http請求 而且跳過響應頭 直接獲取響應body
  if (sendRequest(host, city, APIKEY) && skipResponseHeaders()) {
    //清除緩衝
    clrEsp8266ResponseBuffer();
    //讀取響應數據
    readReponseContent(response, sizeof(response));
    WeatherData weatherData;
    if (parseUserData(response, &weatherData)) {
      printUserData(&weatherData);
    }
  }
  delay(5000);//每5s調用一次
}
 
/**
* @發送http請求指令
*/
bool sendRequest(const char* host, const char* cityid, const char* apiKey) {
  // We now create a URI for the request
  //心知天氣  發送http請求
  String GetUrl = "/v3/weather/now.json?key=";
  GetUrl += apiKey;
  GetUrl += "&location=";
  GetUrl += city;
  GetUrl += "&language=";
  GetUrl += language;
  // This will send the request to the server
  client.print(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n\r\n");
  DebugPrintln("create a request:");
  DebugPrintln(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n");
  delay(1000);
  return true;
}
  
/**
* @Desc 跳過 HTTP 頭,使咱們在響應正文的開頭
*/
bool skipResponseHeaders() {
  // HTTP headers end with an empty line
  bool ok = client.find(endOfHeaders);
  if (!ok) {
    DebugPrintln("No response or invalid response!");
  }
  return ok;
}
  
/**
* @Desc 從HTTP服務器響應中讀取正文
*/
void readReponseContent(char* content, size_t maxSize) {
  size_t length = client.peekBytes(content, maxSize);
  delay(100);
  DebugPrintln("Get the data from Internet!");
  content[length] = 0;
  DebugPrintln(content);
  DebugPrintln("Read data Over!");
  client.flush();//清除一下緩衝
}
  
/**
 * @Desc 解析數據 Json解析
 * 數據格式以下:
 * {
 *    "results": [
 *        {
 *            "location": {
 *                "id": "WX4FBXXFKE4F",
 *                "name": "北京",
 *                "country": "CN",
 *                "path": "北京,北京,中國",
 *                "timezone": "Asia/Shanghai",
 *                "timezone_offset": "+08:00"
 *            },
 *            "now": {
 *                "text": "多雲",
 *                "code": "4",
 *                "temperature": "23"
 *            },
 *            "last_update": "2017-09-13T09:51:00+08:00"
 *        }
 *    ]
 *}
 */
bool parseUserData(char* content, struct WeatherData* weatherData) {
//    -- 根據咱們須要解析的數據來計算JSON緩衝區最佳大小
//   若是你使用StaticJsonBuffer時才須要
//    const size_t BUFFER_SIZE = 1024;
//   在堆棧上分配一個臨時內存池
//    StaticJsonBuffer<BUFFER_SIZE> jsonBuffer;
//    -- 若是堆棧的內存池太大,使用 DynamicJsonBuffer jsonBuffer 代替
  DynamicJsonBuffer jsonBuffer;
   
  JsonObject& root = jsonBuffer.parseObject(content);
   
  if (!root.success()) {
    DebugPrintln("JSON parsing failed!");
    return false;
  }
    
  //複製咱們感興趣的字符串
  strcpy(weatherData->city, root["results"][0]["location"]["name"]);
  strcpy(weatherData->weather, root["results"][0]["now"]["text"]);
  strcpy(weatherData->temp, root["results"][0]["now"]["temperature"]);
  strcpy(weatherData->udate, root["results"][0]["last_update"]);
  //  -- 這不是強制複製,你可使用指針,由於他們是指向「內容」緩衝區內,因此你須要確保
  //   當你讀取字符串時它仍在內存中
  return true;
}
   
// 打印從JSON中提取的數據
void printUserData(const struct WeatherData* weatherData) {
  DebugPrintln("Print parsed data :");
  DebugPrint("City : ");
  DebugPrint(weatherData->city);
  DebugPrint(", \t");
  DebugPrint("Weather : ");
  DebugPrint(weatherData->weather);
  DebugPrint(",\t");
  DebugPrint("Temp : ");
  DebugPrint(weatherData->temp);
  DebugPrint(" C");
  DebugPrint(",\t");
  DebugPrint("Last Updata : ");
  DebugPrint(weatherData->udate);
  DebugPrintln("\r\n");
}
   
// 關閉與HTTP服務器鏈接
void stopConnect() {
  DebugPrintln("Disconnect");
  client.stop();
}
  
void clrEsp8266ResponseBuffer(void){
    memset(response, 0, MAX_CONTENT_SIZE);      //清空
}

注意點:

  • 這裏用到了ArduinoJson庫,你們能夠經過 ArduinoJson,後面博主也計劃專門出一篇講解它;儘可能使用ArduinoJson 5.x版本,6.x版本改變很大,可能不少方法對不上;

測試結果:
image

注意點:

  • Http協議,最好仍是要了解的;
  • 可能不少人以爲這樣拼裝請求很麻煩,因此請關注HttpClient篇章,簡化請求;

    Tcpclient就介紹到這裏,博主只是帶領你們作簡單學習,深刻的理解還請自行查閱源碼;

5. TCP Server

    接下來,博主開始介紹TCP Client的重要夥伴 —— Tcp Server。
    如今,手機上網已是人們天天必不可少的事情。好比刷微博,刷朋友圈,刷新聞等等; 那麼這些朋友圈、微博、新聞內容都是從哪裏來的呢?作個App開發的同窗都應該知道,手機App屬於client端,屬於UI端,展現UI內容,而顯示什麼UI內容基本上都是發送一些http請求到後端服務(server),服務器根據具體的請求內容返回對應的響應內容。
    所謂server,能夠簡單理解爲提供服務,提供數據的一個地方。
    先來理解一下概念圖:
image
    mobile phone做爲client端,經過路由熱點,向Server端的ESP8266請求數據,8266獲取到請求後解析請求而後返回響應數據。
    可是,請開發者注意:ESP8266上創建一個server是比較簡單的,不過是屬於局域網內的server,由於真正意義上的server並非這樣的,大夥瞭解一個這樣的概念就好

6. WiFiServer庫

    在ESP8266上創建TCP Server須要用到WiFiServer庫,WiFiServer庫也是屬於ESP8266WiFi庫裏面的一部分,主要是負責跟server有關的操做。
    先來了解一下總體函數結構,博主總結了一波百度腦圖:
image
    方法整體上能夠分爲三部分:

  • 管理server方法;
  • WiFiClient接入方法;
  • 響應WiFiClient的請求(這部分方法請看上面講解);

6.1 管理server

6.1.1 WiFiServer server(port) —— 建立TCP server

函數說明:

/**
 * 函數功能:建立TCP server
 * @param addr server的ip地址
 * @param port server的端口
 */
WiFiServer(IPAddress addr, uint16_t port);

/**
 * 函數功能:建立TCP server
 * @param port server的端口
 */
WiFiServer(uint16_t port);

6.1.2 begin() —— 啓動TCP server

函數說明:

/**
 * 函數功能:啓動TCP server
 */
void begin();
/**
 * 函數功能:啓動TCP server
 * @param port server端口號
 */
void begin(uint16_t port);

注意點:

  • begin()和 WiFiServer(addr, port)或者WiFiServer(port)一塊兒使用;

6.1.3 setNoDelay() —— 關閉延時發送功能

函數說明:

/**
 * 是否禁用 Nagle 算法。
 * @param nodelay true表示禁用 Nagle 算法
 */
void setNoDelay(bool nodelay);

注意點:

  • Nagle 算法的目的是經過合併一些小的發送消息,而後一次性發送全部的消息來減小經過網絡發送的小數據包的tcp/ip流量。這種方法的缺點是延遲了單個消息的發送,直到一個足夠大的包被組裝。

6.1.4 close() —— 關閉TCP server

函數說明:

/**
 * 關閉TCP server
 */
void close();

6.1.5 stop() —— 中止TCP server

函數說明:

/**
 * 中止TCP server
 */
void stop();

注意點:

  • stop()和 close()是一樣的功能,因此調用哪個都沒有問題;
void WiFiServer::stop() {
    close();
}

6.1.1 status() ——返回TCP server狀態

函數說明:

/**
 * 返回TCP server狀態
 * @return wl_tcp_state tcp狀態
 */
uint8_t status();

wl_tcp_state 包括:

//博主暫時沒理解具體每個怎麼用
enum wl_tcp_state {
  CLOSED      = 0,// 關閉
  LISTEN      = 1,// 監聽中
  SYN_SENT    = 2,
  SYN_RCVD    = 3,
  ESTABLISHED = 4,// 創建鏈接
  FIN_WAIT_1  = 5,
  FIN_WAIT_2  = 6,
  CLOSE_WAIT  = 7,
  CLOSING     = 8,
  LAST_ACK    = 9,
  TIME_WAIT   = 10
};

6.2 WiFiClient接入

6.2.1 available —— 獲取有效的wificlient鏈接

函數說明:

/**
 * 獲取有效的wificlient鏈接
 * @return 若是存在有效的wificlient鏈接,就返回WiFilient對象,若是沒有那就返回一個無效的wificlient(connected等於false,開發者能夠經過判斷connected()
 */
WiFiClient available(uint8_t* status = NULL);

函數源碼:

WiFiClient WiFiServer::available(byte* status) {
    (void) status;
    //判斷是否有非空的鏈接對象
    if (_unclaimed) {
        WiFiClient result(_unclaimed);
        _unclaimed = _unclaimed->next();
        result.setNoDelay(_noDelay);
        DEBUGV("WS:av\r\n");
        return result;
    }

    optimistic_yield(1000);
    //沒有鏈接對象就返回無用的wificlient對象
    return WiFiClient();
}

6.2.2 hasClient —— 判斷是否有client鏈接

函數說明:

/**
 * 判斷是否有client鏈接
 * @return bool 若是有client鏈接就返回true
 */
bool hasClient();

注意點:

  • 開發者能夠經過判斷這個函數來判斷是否有client鏈接,而後調用available() 方法來獲取鏈接,這樣拿到wificlient以後就能夠調用wificlient的方法;

7. 實例操做

    前面講了這麼多理論內容,接下來用幾個例子來講明一下。

7.1 演示WiFiServer功能

例子介紹:
    8266做爲WiFiServer端,打開TCP調試助手,模擬TCP Client的請求。
例子源碼:

/**
 * Demo:
 *    演示WiFiServer功能
 *    打開TCP調試助手 模擬TCP client請求
 * @author 單片機菜鳥
 * @date 2019/09/04
 */
#include <ESP8266WiFi.h>
 
//定義最多多少個client能夠鏈接本server(通常不要超過4個)
#define MAX_SRV_CLIENTS 1
//如下三個定義爲調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* ssid = "TP-LINK_5344";
const char* password = "6206908you11011010";
 
//建立server 端口號是23
WiFiServer server(23);
//管理clients
WiFiClient serverClients[MAX_SRV_CLIENTS];
 
void setup() {
  DebugBegin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  DebugPrint("\nConnecting to "); 
  DebugPrintln(ssid);
  uint8_t i = 0;
  while (WiFi.status() != WL_CONNECTED && i++ < 20) {
    delay(500);
  }
  if (i == 21) {
    DebugPrint("Could not connect to"); 
    DebugPrintln(ssid);
    while (1) {
      delay(500);
    }
  }
  //啓動server
  server.begin();
  //關閉小包合併包功能,不會延時發送數據
  server.setNoDelay(true);
 
  DebugPrint("Ready! Use 'telnet ");
  DebugPrint(WiFi.localIP());
  DebugPrintln(" 23' to connect");
}
 
void loop() {
  uint8_t i;
  //檢測是否有新的client請求進來
  if (server.hasClient()) {
    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
      //釋放舊無效或者斷開的client
      if (!serverClients[i] || !serverClients[i].connected()) {
        if (serverClients[i]) {
          serverClients[i].stop();
        }
        //分配最新的client
        serverClients[i] = server.available();
        DebugPrint("New client: "); 
        DebugPrint(i);
        break;
      }
    }
    //當達到最大鏈接數 沒法釋放無效的client,須要拒絕鏈接
    if (i == MAX_SRV_CLIENTS) {
      WiFiClient serverClient = server.available();
      serverClient.stop();
      DebugPrintln("Connection rejected ");
    }
  }
  //檢測client發過來的數據
  for (i = 0; i < MAX_SRV_CLIENTS; i++) {
    if (serverClients[i] && serverClients[i].connected()) {
      if (serverClients[i].available()) {
        //get data from the telnet client and push it to the UART
        while (serverClients[i].available()) {
          //發送到串口調試器
          Serial.write(serverClients[i].read());
        }
      }
    }
  }
 
  if (Serial.available()) {
    //把串口調試器發過來的數據 發送給client
    size_t len = Serial.available();
    uint8_t sbuf[len];
    Serial.readBytes(sbuf, len);
    //push UART data to all connected telnet clients
    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
      if (serverClients[i] && serverClients[i].connected()) {
        serverClients[i].write(sbuf, len);
        delay(1);
      }
    }
  }
}

測試結果:
image

7.2 演示web Server功能

例子介紹:
    8266做爲web server端,打開PC瀏覽器輸入IP地址,請求web server。
例子源碼:

/**
 * Demo:
 *    演示web Server功能
 *    打開PC瀏覽器 輸入IP地址。請求web server
 * @author 單片機菜鳥
 * @date 2019/09/05
 */
#include <ESP8266WiFi.h>
 
const char* ssid = "TP-LINK_5344";//wifi帳號 這裏須要修改
const char* password = "xxxx";//wifi密碼 這裏須要修改
 
//建立 tcp server 端口號是80
WiFiServer server(80);
 
void setup(){
  Serial.begin(115200);
  Serial.println();
 
  Serial.printf("Connecting to %s ", ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
  }
  Serial.println(" connected");
  //啓動TCP 鏈接
  server.begin();
  //打印TCP server IP地址
  Serial.printf("Web server started, open %s in a web browser\n", WiFi.localIP().toString().c_str());
}
 
/**
 * 模擬web server 返回http web響應內容
 * 這裏是手動拼接HTTP響應內容
 * 後面樓主會繼續講解另外兩個專用於http請求的庫
 */
String prepareHtmlPage(){
  String htmlPage =
     String("HTTP/1.1 200 OK\r\n") +
            "Content-Type: text/html\r\n" +
            "Connection: close\r\n" +  // the connection will be closed after completion of the response
            "Refresh: 5\r\n" +  // refresh the page automatically every 5 sec
            "\r\n" +
            "<!DOCTYPE HTML>" +
            "<html>" +
            "Analog input:  " + String(analogRead(A0)) +
            "</html>" +
            "\r\n";
  return htmlPage;
}
 
 
void loop(){
  WiFiClient client = server.available();
  // wait for a client (web browser) to connect
  if (client){
    Serial.println("\n[Client connected]");
    while (client.connected()){
      // 不斷讀取請求內容
      if (client.available()){
        String line = client.readStringUntil('\r');
        Serial.print(line);
        // wait for end of client's request, that is marked with an empty line
        if (line.length() == 1 && line[0] == '\n'){
          //返回響應內容
          client.println(prepareHtmlPage());
          break;
        }
      }
      //因爲咱們設置了 Connection: close  當咱們響應數據以後就會自動斷開鏈接
    }
    delay(100); // give the web browser time to receive the data
 
    // close the connection:
    client.stop();
    Serial.println("[Client disonnected]");
  }
}

測試結果:
image

7.3 演示簡單web Server功能,webserver會根據請求來作不一樣的操做

例子介紹:
    8266做爲WiFiServer端,演示簡單web Server功能,webserver會根據請求來作不一樣的操做。
例子源碼:

/*
* Demo:
*    演示簡單web Server功能
*    web server會根據請求來作不一樣的操做
*    http://server_ip/gpio/0 打印 /gpio0
*    http://server_ip/gpio/1 打印 /gpio1
*    server_ip就是ESP8266的Ip地址
* @author 單片機菜鳥
* @date 2019/09/05
*/
 
#include <ESP8266WiFi.h>
 
//如下三個定義爲調試定義
#define DebugBegin(baud_rate)    Serial.begin(baud_rate)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
 
const char* ssid = "TP-LINK_5344";//wifi帳號 這裏須要修改
const char* password = "xxxx";//wifi密碼 這裏須要修改
 
// 建立tcp server
WiFiServer server(80);
 
void setup() {
  DebugBegin(115200);
  delay(10);
 
  // Connect to WiFi network
  DebugPrintln("");
  DebugPrintln(String("Connecting to ") + ssid);
  //我只想作個安靜的美男子 STA
  WiFi.mode(WIFI_STA);
  //我想鏈接路由wifi
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(".");
  }
  DebugPrintln("");
  DebugPrintln("WiFi connected");
 
  // 啓動server
  server.begin();
  DebugPrintln("Server started");
 
  // 打印IP地址
  DebugPrintln(WiFi.localIP().toString());
}
 
void loop() {
  // 等待有效的tcp鏈接
  WiFiClient client = server.available();
  if (!client) {
    return;
  }
 
  DebugPrintln("new client");
  //等待client數據過來
  while (!client.available()) {
    delay(1);
  }
 
  // 讀取請求的第一行 會包括一個url,這裏只處理url
  String req = client.readStringUntil('\r');
  DebugPrintln(req);
  //清掉緩衝區數據 聽說這個方法沒什麼用 能夠換種實現方式
  client.flush();
 
  // 開始匹配
  int val;
  if (req.indexOf("/gpio/0") != -1) {
    DebugPrintln("/gpio0");
    val = 0;
  } else if (req.indexOf("/gpio/1") != -1) {
    DebugPrintln("/gpio1");
    val = 1;
  } else {
    DebugPrintln("invalid request");
    //關閉這個client請求
    client.stop();
    return;
  }
  //清掉緩衝區數據
  client.flush();
 
  // 準備響應數據
  String s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nGPIO is now ";
  s += (val) ? "high" : "low";
  s += "</html>\n";
 
  // 發送響應數據給client
  client.print(s);
  delay(1);
  DebugPrintln("Client disonnected");
 
  // The client will actually be disconnected
  // when the function returns and 'client' object is detroyed
}

測試結果:
image

8. 總結

    這一篇章,博主主要講了TCP通訊的兩大角色——client和server。你們須要區分tcp http。而且也要區分工做模式和client server不是一個概念,二者沒有必然的聯繫。這篇算是入門http請求的重點內容,但願讀者能夠仔細研讀,並結合源碼去理解。

相關文章
相關標籤/搜索