The correct way to implement the simplest web server #12219
-
Hi guys, I'm new to ESP32 and micropython, and I'm still learning the basics. I've read several tutorials online about how to set up a web server on the board, and almost all of them use an infinite loop ("while True") to run the server. So my question is - Is that the correct way to do it? So what's the best practice to set up a web server? |
Beta Was this translation helpful? Give feedback.
Replies: 11 comments 10 replies
-
There is no way to do this without an infinite loop in MicroPython. Your server must wait for socket connections from clients. Even if you use ...
async def _serve(self, s, cb):
# Accept incoming connections
while True:
try:
yield core._io_queue.queue_read(s)
except core.CancelledError:
# Shutdown server
s.close()
return
try:
s2, addr = s.accept()
except:
# Ignore a failed accept
continue
s2.setblocking(False)
s2s = Stream(s2, {"peername": addr})
core.create_task(cb(s2s, s2s))
... Without an infinite loop, your server can only accept and serve a client once and then stop. Smart or not, it is what it is. |
Beta Was this translation helpful? Give feedback.
-
I'd use a package like microdot or perhaps tinyweb. Microdot will most probably do all that's needed and more. import uasyncio
import network
async def coro_server( reader, writer ):
data = await reader.read(1024)
print(f"received {data=}")
# Give a response so this can be tested with
# curl http://192.168.xxx.xxx/
writer.write(b'HTTP/1.0 200 OK\n\nHola mundo\n')
await writer.wait_closed()
async def do_connect():
# see standard example of do_connect
async def main():
await do_connect()
server = uasyncio.start_server( coro_server, "0.0.0.0", 80 )
await uasyncio.gather( server, other_tasks() )
main() |
Beta Was this translation helpful? Give feedback.
-
With the ESP32 port and some others, there is a way to register a handler, which is called when some instance connects to a socket. This method is e.g. used by WEBREPL, which is a server. Or ftpd. It is done by a call with option 20 to
The handler runs in scheduled context and can run any Python script. To unregister a handler, call setsockopt() with the value 0f |
Beta Was this translation helpful? Give feedback.
-
Hi Robert, thanks! Yes, we can drop the For those who are building custom firmware and want to disable WEBREPL, you need to enable ```MICROPY_PY_SOCKET_EVENTS`` in "mpconfigport.h".
My earlier statement about the "infinite loop" was wrong. I apologise. With "SOCKET_EVENTS" enabled in the firmware for the ESP32, I can now do the following:
|
Beta Was this translation helpful? Give feedback.
-
@omri-madar, I made wrong claim earlier, sorry. It is possible to use socket event callbacks to write request/response server without looping. The code below seems to work fine. # To test on Linux
# curl --http0.9 http://10.0.1.73:8080
# telnet 10.0.1.73 8080
import socket, select, time, network
network.WLAN(network.AP_IF).active(False)
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("YOUR-WIFI", "YOUR-PWD")
time.sleep(5)
wlan.ifconfig()
def req_handler(cs):
try:
req = cs.read()
if req:
print('req:', req)
rep = b'Hello\r\n'
cs.write(rep)
else:
print('Client close connection')
except Exception as e:
print('Err:', e)
cs.close()
def cln_handler(srv):
cs,ca = srv.accept()
print('Serving:', ca)
cs.setblocking(False)
cs.setsockopt(socket.SOL_SOCKET, 20, req_handler)
port = 8080
addr = socket.getaddrinfo('0.0.0.0', port)[0][-1]
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(addr)
srv.listen(5) # at most 5 clients
srv.setblocking(False)
srv.setsockopt(socket.SOL_SOCKET, 20, cln_handler) Thanks to @robert-hh, I have learned something new today and I am happy. |
Beta Was this translation helpful? Give feedback.
-
Here's the way to implement it simply using microdot. |
Beta Was this translation helpful? Give feedback.
-
Hi Raul, When cln_handler() is called by the srv socket event, a new client socket cs is created. We then attach the req_handler callback to this socket. Now we have two sockets, srv and cs, in our system. Each of these sockets can execute its callback. Say a new client connects, triggering cln_handler(). This handler will create a new socket for the new client. We will then have three active sockets, srv, cs(client-1), cs(client-1). Given that srv.listen(5) can serve five clients at peak traffic, we will have six active sockets, all with callbacks. Note that the example is a simple request/response setup. When a req_handler() is started by a cs socket event, it reads a single request, processes it (here just a print()), processes a response (here just assign 'Hello' to rep and write it to the socket). Close the socket immediately afterwards. Here is a transcript with three active clients:
The first two are from telnet clients and the last one is from a curl client. The curl send request immediately, triggers req_handler() as you can see. We need to write something to the telnet session to make it send something to the server, hence the delay. Below is the actual order in which the requests were made:
In the example we have a server listening only on port 8080. We can have another server listening on port 2323 for example. The server on port 8080 will serve http request/response traffic, while the server on port 2323 will serve plain request/response traffic. The client handlers in this case will be http_cln_handler() and tcp_cln_handler(). A bit of a long explanation, but I hope it answers your question. I have a question for @robert-hh, is there a limit to how many open sockets with callback we can have in ESP32 MicroPython? |
Beta Was this translation helpful? Give feedback.
-
@shariltumin thank you so much for coming back with a better answer, and for wrapping it all up in one script. I used your code as a base, and implemented the most basic web server to handle HTTP requests.
|
Beta Was this translation helpful? Give feedback.
-
@shariltumin: I found
the default and/or the range if you like. From reading other posts it seems that lwip itself has no limit on that. I would be interested in a method to check that value automatically from within a micropython program/session, as it is a parameter that has a certain descriptive value to characterise a port / configuration. |
Beta Was this translation helpful? Give feedback.
-
Hi Raul, it seems that we can have a maximum of 10 active (open) sockets. 1 for the server listening socket and 9 for connected sockets from clients.
Nice of you to find LWIP_MAX_SOCKETS. We can probably do |
Beta Was this translation helpful? Give feedback.
-
For someone learning (like me) how things work here a more elaborate example that includes a # To test on Linux
# curl http://192.168.178.67:8080
# or browser at http://192.168.178.67:8080
import socket, select, time, network
# _SO_REGISTER_HANDLER = const(20)
our_handlers = []
def route(path, methods=['GET']):
def wrapper(handler):
our_handlers.append((path, methods, handler))
return handler
return wrapper
def req_handler(cs):
try:
line = cs.readline()
print('line:', line)
parts = line.decode().split()
if len(parts) < 3:
raise ValueError
r={}
r['method'] = parts[0]
r['path'] = parts[1]
parts = r['path'].split('?', 1)
if len(parts) < 2:
r['query'] = None
else:
r['path'] = parts[0]
r['query'] = parts[1]
r['headers'] = {}
while True:
line = cs.readline()
if not line:
break
line = line.decode()
if line == '\r\n':
break
key, value = line.split(':', 1)
r['headers'][key.lower()] = value.strip()
handled = False
for path, methods, handler in our_handlers:
if r['path'] != path:
continue
if r['method'] not in methods:
continue
handled = True
handler(cs,r)
if not handled:
cs.write(b'HTTP/1.0 404 Not Found\r\n\r\nNot Found')
print('No handler found')
except Exception as e:
print('Err:', e)
cs.close()
def cln_handler(srv):
cs,ca = srv.accept()
print('Serving:', ca)
cs.setblocking(False)
cs.setsockopt(socket.SOL_SOCKET, 20, req_handler) # 20 = _SO_REGISTER_HANDLER
# the above allows for waiting for something that is sent later.
port = 8080
addr = socket.getaddrinfo('0.0.0.0', port)[0][-1]
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(addr)
srv.listen(5) # at most 5 clients
srv.setblocking(False)
srv.setsockopt(socket.SOL_SOCKET, 20, cln_handler) # 20 = _SO_REGISTER_HANDLER
@route('/')
def hello_handler(cs, r): # cs: the socket to read/write from/to, r: dict with additional info
print('r:', r)
cs.write(b'HTTP/1.0 200 OK\r\n')
cs.write(b'Content-Type: text/html; charset=utf-8\r\n')
cs.write(b'\r\n')
cs.write(b'<h1>Hello World!</h1>')
print('our_handlers:', our_handlers) |
Beta Was this translation helpful? Give feedback.
@omri-madar, I made wrong claim earlier, sorry. It is possible to use socket event callbacks to write request/response server without looping. The code below seems to work fine.