MicroPython 和 ESP8266 http 服务器请求不完整

Posted

技术标签:

【中文标题】MicroPython 和 ESP8266 http 服务器请求不完整【英文标题】:MicroPython and ESP8266 http server incomplete request 【发布时间】:2021-11-23 11:10:32 【问题描述】:

我正在尝试基于 MicroPython 的基本 socket 模块实现一个简单的 http 服务器,该模块提供静态 html,可以接收和处理简单的 http GET 和 POST 请求以在 ESP 上保存一些数据。

我按照本教程https://randomnerdtutorials.com/esp32-esp8266-micropython-web-server/ 更改了一些部分。

webserver.py

import logging
from socket import socket, getaddrinfo, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR

from request import Request
from request.errors import PayloadError

log = logging.getLogger(__name__)

def __read_static_html(path: str) -> bytes:
  with open(path, "rb") as f:
    static_html = f.read()

  return static_html

def __create_socket(address: str = "0.0.0.0", port: int = 8080) -> socket:
  log.info("creating socket...")
  s = socket(AF_INET, SOCK_STREAM)
  s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

  addr = getaddrinfo(address, port)[0][-1]
  s.bind(addr)

  log.info("socket bound on   ".format(addr))

  return s

def __read_from_connection(conn: socket) -> Request:
  log.info("read from connection...")
  raw_request = conn.recv(4096)

  return Request(raw_request.decode("utf-8"))

def listen_and_serve(webfile: str, address: str, port: int):
  server_socket = __create_socket(address, port)

  log.info("listen on server socket")
  server_socket.listen(5)

  while True:
    # accept connections
    client_server_connection, client_address = server_socket.accept()
    log.info("connection from ".format(client_address))

    req = __read_from_connection(client_server_connection)

    log.info("got request: ".format(req.get_method()))

    path = req.get_path()
    if path != '/':
      log.info("invalid path: ".format(path))
      client_server_connection.send(b"HTTP/1.1 404 Not Found\n")
      client_server_connection.send(b"Connection: close\n\n")
      client_server_connection.close()
      continue

    if req.get_method() == "POST":
      log.info("handle post request")
      try:
        pl = req.get_payload()
        log.debug(pl)
      except PayloadError as e:
        log.warning("error: ".format(e))
        client_server_connection.send(b"HTTP/1.1 400 Bad Request\n")
        client_server_connection.send(b"Connection: close\n\n")
        client_server_connection.close()
        continue

    log.info("read static html...")
    static_html = __read_static_html(webfile)

    log.info("send header...")
    client_server_connection.send(b"HTTP/1.1 200 OK\n")
    client_server_connection.send(b"Connection: close\n\n")

    log.info("send html...")
    client_server_connection.sendall(static_html)

    log.info("closing client server connection")
    client_server_connection.close()

请求模块是我自己编写的 http 请求解析器,对我需要的支持最少。

提供的 html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP Basic Configuration</title>
  </head>
  <body>
    <h1>ESP Basic Configuration</h1>
    <form action="/" method="post" enctype="application/x-www-form-urlencoded">
      <h3>Network</h3>
      <div>
        <label for="network.ssid">SSID</label>
        <input id="network.ssid" name="network.ssid" type="text" />
      </div>
      <div>
        <label for="network.password">Password</label>
        <input id="network.password" name="network.password" type="password" />
      </div>
      <button type="submit">Save</button>
    </form>
  </body>
</html>

当我使用正常的 Python3.9 在我的系统上运行代码时,一切似乎都正常。 在我的 ESP8266 上运行代码,raw_request 的长度被截断为 536 字节。所以有些请求不完整,payload无法读取。

我已经阅读了默认情况下套接字是非阻塞的,并且可能会发生短读。我尝试过使用带有超时的阻塞套接字。但我总是有超时,我认为不应该有。

我尝试过使用类似文件的套接字对象,如下所示:https://docs.micropython.org/en/latest/esp8266/tutorial/network_tcp.html#simple-http-server 但是由于\r\n 的if 条件,请求读取在标头之后停止。 删除此条件并仅检查 if not line 会在下一行读取循环。

我目前不知道如何才能获取带有有效负载的完整请求。

编辑:添加 MRE 这是我可以重现该问题的最小示例:

main.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR

def main():
  server_socket = socket(AF_INET, SOCK_STREAM)
  server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

  server_socket.bind(("0.0.0.0", 8080))

  server_socket.listen(5)

  while True:
    # accept connections
    client_server_connection, client_address = server_socket.accept()

    raw_request = client_server_connection.recv(4096)

    print("raw request length: ".format(len(raw_request)))
    print(raw_request)

    client_server_connection.send(b"HTTP/1.1 200 OK\r\n")
    client_server_connection.send(b"Connection: close\r\n\r\n")

    client_server_connection.sendall(b"""<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP Basic Configuration</title>
  </head>
  <body>
    <h1>ESP Basic Configuration</h1>
    <form action="/" method="post" enctype="application/x-www-form-urlencoded">
      <h3>Network</h3>
      <div>
        <label for="network.ssid">SSID</label>
        <input id="network.ssid" name="network.ssid" type="text" />
      </div>
      <div>
        <label for="network.password">Password</label>
        <input id="network.password" name="network.password" type="password" />
      </div>
      <button type="submit">Save</button>
    </form>
  </body>
</html>
""")

    client_server_connection.close()


if __name__ == "__main__":
  main()

当使用打印语句输出原始请求时,我得到以下信息:

raw request length: 536
b'POST / HTTP/1.1\r\nHost: 192.168.0.113:8080\r\nConnection: keep-alive\r\nContent-Length: 39\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nOrigin: http://192.168.0.113:8080\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nReferer: http://192.168.0.113:8080/\r\nA'

请求仍被截断为 536 字节,请求突然结束。

【问题讨论】:

你能 MRE 这个吗?删除邮政编码并减少到最简单的错误。如果没有发布任何内容,我将在接下来的几天内尝试。不过,您可能只是达到了 esp8266 的限制:使用 uPy 的 esp32 体验要好得多 @2e0byo MRE?对不起,我在这里很新。这是什么意思? ***.com/help/minimal-reproducible-example @2e0byo 我要将 MRE 添加到我原来的问题中。 【参考方案1】:
def __read_from_connection(conn: socket) -> Request:
  log.info("read from connection...")
  raw_request = conn.recv(4096)

您假设recv 将返回您需要的所有数据。这个假设是错误的。 TCP 没有消息的概念,而是一个字节流,所以你需要一次又一次地recv,直到你得到你需要的所有数据。 所有数据的含义由 HTTP 消息格式定义 - 请参阅HTTP standard in RFC 7230。

为什么recv 返回的数据量在 ESP8266 和 PC 之间不同:可能是因为 ESP8266 的套接字缓冲区较小,并在 TCP 中将其声明为窗口。这使得对端以更小的数据包发送数据。

除此之外,以响应结尾的行必须是 \r\n 而不是 \n

但是由于\r\n 的if 条件,请求读取在标头之后停止。 删除此条件并仅检查 if not line 保持下一行读取的循环。

空行 (\r\n) 标志着 HTTP 标头的结束。要找出正文的大小,您需要解析 HTTP 标头并查找长度信息,即 Content-length 标头用于固定长度或 Transfer-Encoding: chunked 当正文以小块发送且不知道完整长度时前面。详见标准。

【讨论】:

我将尝试先实现读取和解析标题,并在我期望正文时决定读取过去的\r\n。我试过了会回来的。 我终于找到了一些时间来测试一下。 1. 重读:我想我会在一个循环中用更小的块大小重新读取套接字,直到一次读取返回的块大小小于这个块大小。这导致一次读取 512 字节和 24 字节。导致以前的 536 字节限制。 2.阅读过去:然后我切换到client_server_connection.makefile("rb")readline,解析每一行并预期content-length标题并允许读取标题,如果数据已发送。这会导致在标头之后readline 上的连接挂起,直到连接被客户端取消。 @JoshuaIrm:当前问题已得到解答,即解释了您在当前代码中看到的问题。如果你的新代码有不同的问题,那么请提出一个新问题并在其中包含所有必要的细节,即代码、调试信息、期望、发生的事情……。不要让你的问题成为一个移动的目标。

以上是关于MicroPython 和 ESP8266 http 服务器请求不完整的主要内容,如果未能解决你的问题,请参考以下文章

esp32 cam+esp8266用micropython实现人脸识别开门

MicroPython ESP32/8266定时器中断示例解析

MicroPython esp8266固件烧写教程

MicroPython+ESP8266:UART串口通信

MicroPython ESP8266配网示例以及network模块功能介绍

Micropython esp32/8266网页点灯控制示例