使用Arduino IDE在esp32上运行WebServer

#目的
WebServer是非常常用的一个功能，在设备上使用该功能用户就可以不依赖app直接通过浏览器访问和操作设备。另外即使是用app的，对于app开发来说直接访问webapi也比处理tcp/udp要方便些。
#使用详解
##基本使用
WebServer简单点理解就是网页服务器，主要干的活就是用户访问链接的时候执行相应的动作，对于开发来说主要处理的就是注册链接并编写用户访问该链接时需要执行的操作。
使用步骤如下：
1、引入相应库`#include <WebServer.h>`;
2、声明`webserver`并设置端口号，一般WebServer端口号使用`80`;
3、使用`on()`方法注册链接与回调函数；
4、使用`begin()`方法启动服务器进行请求监听；
5、使用`handleClient()`处理来自客户端的请求；
可以使用下面代码进行测试(替换掉wifi名和密码)：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

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

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  String message = "客户端信息：";
  message += "\nClient IP: ";
  IPAddress addr = server.client().remoteIP(); //客户端ip
  message += String(addr[0]) + "." + String(addr[1]) + "." + String(addr[2]) + "." + String(addr[3]);
  message += "\nURI: ";
  message += server.uri(); //打印当前url
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST"; //判断http请求方法
  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(200, "text/plain", message);
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/msg", HTTP_GET, handleRoot); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
写esp32后在浏览器中输入串口监视器的服务器ip地址即可访问，输入`ip/p1`显示页面一，输入`ip/p2`显示页面二。注意浏览器的设备用的网络一定要和esp32连接的wifi一致才能访问。
![Alt Text](https://img-blog.csdnimg.cn/2019041515221669.gif) 
##使用路径参数
如果你有很多相似的链接比如`/user/1`、`/user/2`、`/user/3`、`/user/4`……，使用上面的方法时就需要每个链接都需要进行声明注册，比较不方便，这里可以使用路径参数来处理这些相似的或是动态的链接，可以用下面的代码进行测试(替换掉wifi名和密码)：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

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

WebServer server(80); //声明WebServer对象

void handleArg1() //回调函数
{
  String arg = server.pathArg(0);
  server.send(200, "text/plain", "这是链接/{}，参数是： " + arg);
}

void handleArg2() //回调函数
{
  String arg0 = server.pathArg(0);
  String arg1 = server.pathArg(1);
  server.send(200, "text/plain", "这是链接/p/{}/d/{}，参数是： " + arg0 + " 、 " + arg1);
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/{}", handleArg1);        //注册链接与回调函数
  server.on("/p/{}/d/{}", handleArg2); //注册链接与回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
![Alt Text](https://img-blog.csdnimg.cn/201904161751067.gif)
##设置未注册页面响应
如果用户访问了未注册的的链接时我们最好能给个提示，比如我们在上网时经常能见到的“网页不存在”、“404 Not Found”等。在这里我们可以用`onNotFound()`方法来给出这样的提示，用户在访问不存在的链接时会跳转到该方法所绑定的回调函数上，可以用下面代码进行测试(替换掉wifi名和密码)：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

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

WebServer server(80); //声明WebServer对象

void handleNotFound() //未注册链接回调函数
{
  server.send(404, "text/plain", "访问的页面不存在哦");
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.onNotFound(handleNotFound); //未注册链接回调函数注册

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
![Alt Text](https://img-blog.csdnimg.cn/20190415155512327.gif)
用手机自带浏览器访问时直接提示页面不存在。但实际上测试成功了。
##用户认证
用户认证可以提供一定的安全性，这里提供了`BASIC_AUTH`和`DIGEST_AUTH`两种方式，一般来说`DIGEST_AUTH`方式安全性稍高些，下面代码进行了基本的测试：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

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

WebServer server(80); //声明WebServer对象

const char *username = "user";     //用户名
const char *userpassword = "1234"; //用户密码

void handleRoot() //回调函数
{
  if (!server.authenticate(username, userpassword)) //校验用户是否登录
  {
    return server.requestAuthentication(); //请求进行用户登录认证
  }
  server.send(200, "text/plain", "登录成功!"); //登录成功则显示真正的内容
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
![Alt Text](https://img-blog.csdnimg.cn/20190423090922998.gif)
##请求方信息
客户端请求链接，我们也能够知道客户端请求的一些信息，可以用下面代码进行测试：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

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

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  String message = "客户端信息：";
  message += "\nClient IP: ";
  IPAddress addr = server.client().remoteIP(); //客户端ip
  message += String(addr[0]) + "." + String(addr[1]) + "." + String(addr[2]) + "." + String(addr[3]);
  message += "\nURI: ";
  message += server.uri(); //打印当前url
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST"; //判断http请求方法
  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(200, "text/plain", message);
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/msg", HTTP_GET, handleRoot); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
![Alt Text](https://img-blog.csdnimg.cn/20190424182505685.gif)
#网页与后台数据交互
网页与后台数据交互需要用到网页和html和ajax知识，可以参考：[《从零开始的ESP8266探索（06）-使用Server功能搭建Web Server》中《通过网页收发数据》章节](https://blog.csdn.net/Naisu_kun/article/details/80398667#_116)
我们首先准备一个带ajax脚本的网页，但是网页代码测试有错误，点击后无法显示随机数：
```
<html>
<head>
    <title>ESP32 WebServer Test</title>
    <script>
        function getData() {
            var xmlhttp;
            if (window.XMLHttpRequest) {
                xmlhttp = new XMLHttpRequest();
            }
            else {
                xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
            }
            xmlhttp.onreadystatechange = function () {
                if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                    //将获取到的内容写到txtRandomData中
                    document.getElementById("txtRandomData").innerHTML = xmlhttp.responseText;
                }
            },
                xmlhttp.open("GET", "getRandomData", true); //以GET方法打开getRandomData
            xmlhttp.send();
        }
    </script>
</head>
<body>
    <div id="txtRandomData">Unkwon</div>
    <input type="button" value="random" onclick="getData()">
</body>
</html>
```
然后通过工具将它转换成字符串嵌入到代码中，工具可以参考：
http://tools.jb51.net/transcoding/html2js
也可以先压缩网页然后再转换成字符串，这样可以减小网页大小，提高效率，可以参考：
https://tool.lu/html/
下面就是带有 网页与后台数据交互功能 的完整代码：

```
#include <WiFi.h>
#include <WebServer.h> //引入相应库

//网页
String myhtmlPage =
    String("") +
    "<html>" +
    "<head>" +
    "    <title>ESP32 WebServer Test</title>" +
    "    <script>" +
    "        function getData() {" +
    "            var xmlhttp;" +
    "            if (window.XMLHttpRequest) {" +
    "                xmlhttp = new XMLHttpRequest();" +
    "            }" +
    "            else {" +
    "                xmlhttp = new ActiveXObject(\"Microsoft.XMLHTTP\");" +
    "            }" +
    "            xmlhttp.onreadystatechange = function () {" +
    "                if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {" +
    "                    document.getElementById(\"txtRandomData\").innerHTML = xmlhttp.responseText;" +
    "                }" +
    "            }," +
    "                xmlhttp.open(\"GET\", \"getRandomData\", true); " +
    "            xmlhttp.send();" +
    "        }" +
    "    </script>" +
    "</head>" +
    "<body>" +
    "    <div id=\"txtRandomData\">Unkwon</div>" +
    "    <input type=\"button\" value=\"random\" οnclick=\"getData()\">" +
    "</body>" +
    "</html>";

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

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/html", myhtmlPage); //！！！注意返回网页需要用"text/html" ！！！
}

void handleAjax() //回调函数
{
  String message = "随机数据：";
  message += String(random(10000)); //取得随机数
  server.send(200, "text/plain", message); //将消息发送回页面
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);                        //注册链接和回调函数
  server.on("/getRandomData", HTTP_GET, handleAjax); //注册网页中ajax发送的get方法的请求和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
}
```
因为网页代码测试失败，所以以下的gif需要更正网页代码错误后才能实现。
![Alt Text](https://img-blog.csdnimg.cn/20190425123611689.gif)
可以看到当点击网页上random按钮时触发了`getData()`方法，该方法向服务器请求`getRandomData`链接，服务器在收到该请求后进行了响应，把数据返回给客户页面。上面只是简单演示，你也可以使用上面的方法来控制设备（比如点亮个灯、开合继电器等）。
#常用方法
`WebServer(int port = 80)`
构造方法；
`void begin()`
`void begin(uint16_t port)`
服务器启动监听；
`void handleClient();`
处理来自客户端的请求；
`void close()`
`void stop()`
停止当前监听；
`bool authenticate(const char * username, const char * password)`
校验用户是否登录；
`void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String(""))`
请求进行用户登录认证（浏览器端会打开登录窗口）；
`void on(const String &uri, THandlerFunction handler)`
`void on(const String &uri, HTTPMethod method, THandlerFunction fn)`
`void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn)`
注册链接与回调函数；
`void onNotFound(THandlerFunction fn)`
注册未注册链接回调函数
`void onFileUpload(THandlerFunction fn)`
注册文件上传回调函数；
`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 sendHeader(const String& name, const String& value, bool first = false)`
发送响应头；
`void sendContent(const String& content)`
发送响应正文内容；
`String uri()`
返回客户端请求的url；
`HTTPMethod method()`
返回客户端请求方法
`String pathArg(unsigned int i)`
通过序号获取路径参数；
`template<typename T>`
`size_t streamFile(T &file, const String& contentType)`
发送文件，返回发送的字节数；
#其它说明
##HTTP状态码说明
上面的`200`、`404`等是HTTP状态码。用户在请求访问某个地址的时候，WebServer需要进行响应，发送响应头，响应头中第一行一般像是这样的`HTTP/1.1 200 OK`，其中`200`就是状态码。常见的状态码如下：

`200`服务器成功返回网页；
`404`请求的网页不存在；
`301`本网页被永久性转移到另一个URL；
`503`服务器目前不可用；
`401`请求未经授权，需要登录认证；
`……`
##Content-Type说明
>上面的`text/plain`、`text/html`这些是Content-Type，具体说明如下：
Content-Type（MediaType），即是Internet Media Type，互联网媒体类型，也叫做MIME类型。在互联网中有成百上千中不同的数据类型，HTTP在传输数据对象时会为他们打上称为MIME的数据格式标签，用于区分数据类型。最初MIME是用于电子邮件系统的，后来HTTP也采用了这一方案。
在HTTP协议消息头中，使用Content-Type来表示请求和响应中的媒体类型信息。它用来告诉服务端如何处理请求的数据，以及告诉客户端（一般是浏览器）如何解析响应的数据，比如显示图片，解析并展示html等等。
Content-Type举例：

*  `text/plain`纯文本文件；
*  `text/htmlhtml`文件；
*  `application/jsonjson`形式数据文件；
*  `application/xmlxml`形式数据文件；
*  `image/pngpng`格式图片；
*  `……`
#总结
WebServer的使用主要就是上面这些了，其它的一些相功能（DNS服务器、将网页数据存储在SD卡、通过网页更新设备固件等）会在后面单独写文章进行介绍。
更多内容可以参考下面：

https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer

https://www.cnblogs.com/testyao/p/6548261.html