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定时器中断示例解析