Hello Server (host-work)

このEPS32の様なモジュールで何かを作ろうとした時、以外と手間なのがモジュールを操作するためのユーザーインターフェイスです。ボタンを押して測定を開始したり、測定用のパラメータを入力したり等、本体のスケッチよりこちらの方に時間がかかってしまう事も良く有りがちです。しかし、このESP32はWebサーバとなるのでHTMLを用いてボタンや入力欄、グラフやデータの保存等が簡単に実現できそうです。通常のネット使用ではクライアントがほとんどでしたが今回はサーバ側になります。そこでクライアントからの要求に対するサーバのデータの返し方を理解したいと思います。

サンプルスケッチは以下のものを使います。スケッチを実行してホスト名、”esp32.local”をブラウザーに入力すれば、”Hello ESP32″ とブラウザの左上に表示されるものです。


#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char* ssid = "XXXXXX";
const char* password = "YYYYYY";

WebServer server(80);

String index_HD = 
            "<!DOCTYPE html> <html>\n" 
            "<head>\n" 
            "<title>LED Control</title>\n" 
            "</head>\n"
            "<body>\n"
            "<h2>Hello ESP32</h2>\n</body>\n"
            "</html>\n";

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp32")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  server.handleClient();
}

void handleRoot() {
  server.send(200, "text/html", index_HD); 
}

5,6行目には自分のルータの値をいれて下さい。コンパイルして実行して下さい。その後ブラウザを上げて、”esp32.local”と入力すれば、下記が表示されます。

ちなみにこのページのソース

このスケッチは、クライアントから要求に対し、”index_HD”というWebページのデータ(HTLM)を持った文字列を、”server.send()”を使って送っています。ここで、”server.send(code,content_type,content)”の引数をもう一度確認します。

  • code
    • HTTPステータスコード。通信の状態を表すコード。200とか404等が入る。
  • content_type
    • 送信するデータの種類。
    • 例えば、データがHTMLなら、”text/html”。ただの文字列なら、”text/plain”となる。
  • content
    • クライアントに送るデータ本体。

一方で、サーバーからクライアントへの送信フォーマットですが下記の様になります。

”server.send(code,content_type,content)”とこの送信フォーマットを比べると関数の引数とフォーマットの各要素が対応している事が分かります。実際の通信の様子をブラウザの開発モードを使って見て見ます。

例えば、”Fire Fox”ですが、”ツール”ー>”ウエブ開発”ー>”開発ツールを表示”と選んでからスケッチを実行しブラウザに、”esp32.local”と入力して下さい。画面が表示されたら、”ネットワーク”をクリックして下さい。

画面の下にある表に、ステータス:200 メソッド:GET ドメイン:esp32.local:ファイル:/ ……がクライアントが最初にサーバにアクセスした時の送信記録です。

この行一番左端の”200”をクリックして下さい。画面がこの様に変わります。

応答ヘッダー、要求ヘッダーの”生ヘッダーボタン”をクリックして下さい。各ヘッダー部が以下の様に変わります。

この様な形式でヘッダーのやり取りが行われている様です。
クライアントは、”Get http://esp32.local/” とサーバに送ったのですが、実は送った方にもヘッダが有り(上記の要求ヘッダー)サーバに対して要求を出しています。サーバも本体を送信する前に応答ヘッダーを送っています。


HTTP/1.1 200 OK
 Content-Type: text/html
 Content-Length: 109
 Connection: close

スケッチ、”server.send(200, “text/html”, index_HD);”の引数と応答ヘッダーの各行を比べると

  • HTTP/1.1 200 OK
    • 関数の1番目の引数を元に作成された。
  • Content-Type: text/html
    • 関数の2番目の引数を元に作成された。
  • Content-Length: 109
    • 関数の3番目の引数を元に作成された。
  • Connection: close
    • 引数では指定していないので、関数側で自動で制作。

と思われます。要求ヘッダーの内容が良く分からないので何とも言えないのですが、応答ヘッダーはこれで要求の答えになっているのでしょうか。

ソースを探したら

関数send()はWebServer.ccpの内部関数で、ソースを探したら、”ここ” に有りました。多分、send()はこの部分だと思います。ソースの413行目からです。


void WebServer::send(int code, const char* content_type, const String& content) {
    String header;
    // Can we asume the following?
    //if(code == 200 && content.length() == 0 && _contentLength == CONTENT_LENGTH_NOT_SET)
    //  _contentLength = CONTENT_LENGTH_UNKNOWN;
    _prepareHeader(header, code, content_type, content.length());
    _currentClientWrite(header.c_str(), header.length());
    if(content.length())
      sendContent(content);
}
  • _prepareHeader(header, code, content_type, content.length());
    • もらった引数を元にヘッダーを作っている。
  • _currentClientWrite(header.c_str(), header.length())
    • 作成したヘッダーをクライアントに送信
  • sendContent(content);
    • 文字列を送信する関数。content(本体)を送信。

関数の引数を元に応答ヘッダーが作られたいる事は分かったのですが、要求ヘッダーの要求は何処に反映されているのでしょうか。

send() と sendContent()

関数send()は応答ヘッダーを自動で制作してくれる便利な関数ですが、3番目の引数(本体)はWebページ1ページ文のデータ(正確にはクライアントの要求に対する全てのデータ)を持つ必要が有ります。これが非常に長いとメモリーを占有する可能性が有ります。
関数send()で、使用されていた関数sendContent()ですがこれを使って、

  • 応答ヘッダー部を自分で制作し、sendContent()を使用して先ずクライアントに送信
  • その後、送信文字列がある程度の容量に成ったらその都度、sendContent()を使ってクライアントに送信
  • 送信するデータが無くなるまで上記を繰り返す。

この方法なら、送るデータが大きくても一度に読み出さないのでメモリの占有は回避出来ます。検証用のスケッチは以下


#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char* ssid = "XXXXXX";
const char* password = "YYYYYY";

WebServer server(80);

String index_HD = 
            "HTTP/1.1 200 OK\n" 
            "Content-Type: text/html\n" 
            "Content-Length: 109\n"
            "Connection: close\n\n" 
            "<!DOCTYPE html> <html>\n" 
            "<head>\n" 
            "<title>LED Control</title>\n" 
            "</head>\n"
            "<body>\n"
            "<h2>Hello ESP32</h2>\n</body>\n</html>\n";

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp32")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  server.handleClient();
}

void handleRoot() {
  server.sendContent(index_HD); 
}
  • 11から14行
    • 本体に応答ヘッダー部を追加しています。
    • 14行目に改行が2個あるのは、応答ヘッダーと本体のに改行が必要な為です。
  • 56行:server.sendContent(index_HD)
    • 前回のsend()の変わりにこの関数を使う。
    • この関数は引数が1つ。

実行して見て下さい。同じ結果になったと思います。次に、応答ヘッダー部を送ってその他を適当なサイズで数回送る場合です。下記のスケッチを使います。


#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char* ssid = "XXXXXX";
const char* password = "YYYYYY";

WebServer server(80);

String index_HD = 
            "HTTP/1.1 200 OK\n" 
            "Content-Type: text/html\n" 
            "Content-Length: 109\n"
            "Connection: close\n\n"; 

void setup() {
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("esp32")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  
  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  server.handleClient();
}

void handleRoot() {
  server.sendContent(index_HD);
  server.sendContent("<!DOCTYPE html> <html>\n<head>\n"); 
  server.sendContent("<title>LED Control</title>\n</head>\n");
  server.sendContent("<body>\n<h2>Hello ESP32</h2>\n</body>\n</html>\n"); 
}
  • 10から14行
    • 応答ヘッダー部のみに変更。
  • 50から53行
    • 50行でヘッダー部のみ送信。
    • それ以降で残りを3つに分けて送信

問題無く動く事が確認出来たと思います。send()を使わなくともこの様にすればsendcontent()を使ってコマ切れに送ることが可能です。

送信データの終了判断

サーバからのデーターの終了をクライアントはどのように判断しているか。ここまでの話しから応答ヘッダーのContent-Lengthの値を使って終了を判断している事は容易に想像出来ます。Content-Lengthを意図的に変えたらどうなるか実験して見ます。

前回のスケッチ13行目の、”Content-Length: 109\n”を”Content-Length: 80\n”に変更して実行します。画面はこんなになります。

”Hello ESP32”の”Hell”部分のみ表示されています。右下側のサイズをみると転送したデータ数は確かに80バイトになっています。ページのソースは

HTMLが確かにHellで終わっています。話しは別なんですが、この不完全なHTMLでも、”Hell”って表示するんですね。

今回のスケッチで実際に送っている部分は


void handleRoot() {
  server.sendContent(index_HD);
  server.sendContent("<!DOCTYPE html> <html>\n<head>\n"); 
  server.sendContent("<title>LED Control</title>\n</head>\n");
  server.sendContent("<body>\n<h2>Hello ESP32</h2>\n</body>\n</html>\n"); 
}

送信数を、”80”文字として、”Hell”まで表示されているので、4つ目の、”server.sendContent()”の途中で80文字になった様です。しかし、この関数は実行されているので、実際に送信した文字数は前回と同じ109文字。つまり109文字送っても応答ヘッダーで80文字と返せば80文字のみが対象となりそれ以外は無視される様です。

次に送信数を本来より多くしたらどうなるか、送信数を120として実行して見て下さい。

今回はちゃんと、”Hello ESP32″と表示されましたが、サイズは109となっています。応答ヘッダーを確認して見ましょう。ステータスの下の200をクリックしてヘッダーを表示させると

応答ヘッダーは確かに送信文字数120となっています。

後、この処理がさっきの処理より時間がかかっているのですが、気が付きましたか。サーバは120文字と返答して実際には109文字しか送信していません。クライアントは文字数が120と返されたので、109文字後も次のデータの送信を待っていたのだと思います。データが来ないのでタイムアウトとなり(この分反応がさっきと若干遅くなる)、受信したデータのみを表示したと思われます。

最後に、文字数を返さない場合はどうなるか実験します。13行をコメントアウトにして実行して下さい。

先ずは応答ヘッダーにContent-Lengthが無いことから、送信文字数は返していません。次にサイズは109で送信した文字数と同じになっています。最後に反応速度ですが、数を多く送信した場合と同じく若干遅い様です。ここまでをまとめると

  • 送信したデータより数を少なく返す
    • データの読み込みが途中で終わる。反応は早い。
  • 送信したデータより数を多く返す、または返さない。
    • 送ろうとしたデータは全てクライアントに届くが、反応が遅い

となります。

何でこんな話しをしているのか、ちゃんと数返せばいいじゃんと思うでしょうが、実際にスケッチを書くとやって見ないと分からない事が多々有るからです。例えば、ボタンを押すと測定値を返すスケッチの場合、測定値を文字列で表すと桁が変わると送信文字数が変わる。測定しないと送信数が決まらない。この様な場合タイムアウト覚悟で大きな数を返すのでしょうか。

今回の話しですが、送信のタイプが、Content-Type: text/htmlの場合です。他のタイプの場合どのように成るかは分かりません。

付け足し

開発表示の状態リスト部に、”favicon.ico”という物が表示されているのに気づきました。調べたら、タブの左端に表示されるアイコンのことだそうです。

表示したいアイコンを32x32ビットで描き、”ico”という拡張子しでルートに保存すれば良い様です。今回のスケッチは画像読み出しに対応していませんが、前回のスケッチは対応しています。”Pinta”で前回使った電球の点いた方の画像を32x32に縮小し、”data”フォルダーに拡張子”ico”で、保存しました。

スケッチは、void handleWebRequests()で例外処理をしていますが、”ico”拡張子用に、else if(path.endsWith(“.ico”)) { dataType = “image/html”; flg=1; }を追加して下さい。


    path = server.uri();
    if(path.endsWith(".css")) { dataType = "text/css"; flg=1; }
    else if(path.endsWith(".js")) { dataType = "application/javascript"; flg=1; }
    else if(path.endsWith(".png")) { dataType = "image/png"; flg=1; }
    else if(path.endsWith(".gif")) { dataType = "image/gif"; flg=1; }
    else if(path.endsWith(".jpg")) { dataType = "image/jpeg"; flg=1; }
    else if(path.endsWith(".ico")) { dataType = "image/html"; flg=1; }

こんな感じでアイコンが表示されました。