As my code is similar to the WebREPL code [webrepl.py] and [webrepl_cli.py], I tried the following: I connected to the ESP using WebREPL. Then I opened a new browser tab and reloaded http://192.168.4.1:8266/ all the time. On WebREPL, you will see something like:
Code: Select all
Concurrent WebREPL connection from ('192.168.178.21', 51268) rejected
- WebREPL disconnects (when free OS memory drops to certain value? websockets could still be working), or
- ESP does a soft reboot (blue LED blinks), or
- ESP does not react any more
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hi again,
I am trying to implement a web server to control the ESP8266 and/or monitor some information received from it. Multiple clients (let's say at most two or three) are able to connect to the server using websockets (at the same time). They should receive live updates from the server (e. g. the value of a temperature sensor should be displayed without reloading the page all the time). I already got this working.
The problem: Of course, there must be a limit for the number of clients, otherwise the ESP8266 will get out of OS memory (see esp.freemem()) some time. So when the maximum number of clients (in my example two) is reached and another client tries to connect to the server, it should be blocked. The problem is that when I simply press the reload button on my browser all the time (or even just pressing reload button and then canceling the reload), the server will crash (and I can't figure out at which point it crashes).
First, I sent a 503 error when no more connections were allowed. As this gave me OSErrors when reloading to fast, I now simply do not even accept() new connections when the limit is exceeded.
Here is the (messy) code [webserver.py]:
Code: Select all
import os
import time
import socket
from websocket import websocket
import websocket_helper
import esp
class ClientClosedError(Exception):
pass
class WebSocketClient:
def __init__(self): # setup method has to be called after init!
self._close_required = False # client should be closed
self._check_required = False # should check if still connected
self.address = None # address
self._s = None # socket
self.ws = None # websocket (based on the socket)
self._close_clb = None # close callback (function that is called after closing the socket)
def setup(self, sock, addr, close_callback):
self.address = addr
self._s = sock
self.ws = websocket(self._s, True)
self._close_clb = close_callback
sock.setblocking(False)
sock.setsockopt(socket.SOL_SOCKET, 0x14, self.notify)
def connected(self) -> bool:
return self.ws is not None
def notify(self, self_s): # (self_s == self.s)
self._check_required = True # only check state when reading
def read(self) -> bytes: # raises a ClientClosedError if connection was closed
if self._check_required:
self._check_required = False
state = int(str(self._s).split(' ')[1].split('=')[1])
if state == 3: # connection pending (probably stuck)
self._close_required = True
read = None
try:
read = self.ws.read()
except OSError:
self._close_required = True
if not read and self._close_required:
raise ClientClosedError()
return read
def write(self, msg):
try:
self.ws.write(msg)
except OSError:
self._close_required = True
def close(self):
self._s.setsockopt(socket.SOL_SOCKET, 0x14, None)
self._s.close()
self._s = None
self.ws.close()
self.ws = None
self._close_clb(self) # remove client from the client list of the server
def process(self):
# this method implements the routine that will be called while the client is connected.
# should be be overridden (i. e. implemented) by the a subclass.
pass
class WebSocketServer:
def __init__(self, client_cls=WebSocketClient, default_page=None, client_limit=1):
# """ Creates a WebSocket server that is able to accept a maximum of <client_limit> connections. <client_cls>
# specifies the class of the clients, which should be a subclass of WebSocketClient that implements the process
# method and offers an empty constructor (__init__ without args). <default_page> is a HTML file that is provided
# to the client if connected directly (http://<this_server>) without a websocket (None = no file). """
self.Client = client_cls # subclass of WebSocketClient implementing the process() method
self._page = default_page # this html file will be sent if connected without websocket (None = disable)
self._c_max = client_limit # maximum number of connections at the same time (None = no limit)
self._c = [] # list of currently connected client objects
self._s = None # socket (listener)
self._acc_cleartime = 5000 # whenever the client limit is reached and a new connection is recognized,
self._acc_timer = 0 # the new socket has to be closed. this needs some time in memory (not gc;
self._acc_todo = False # see esp.freemem()). if new connections are accepted too fast, this will
# cause an error (ENOMEM). therefore the accept handler will only accept new
# connections every <acc_cleartime> ms. <acc_timer> saves last accept time.
# <_acc_todo> is True is a call has to be accepted.
def start(self, port=80):
if self._s is not None: # started already
self.stop()
self._s = socket.socket()
self._s.settimeout(0.5) # do not wait forever
self._s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # allow reuse of address for bind() method
self._s.bind(socket.getaddrinfo("0.0.0.0", port)[0][-1]) # bind socket to 0.0.0.0:port
self._s.setsockopt(socket.SOL_SOCKET, 0x14, self._accept) # setup accept handler for new connections
self._s.listen(1) # queue only a single connect request
def stop(self):
if self._s is not None:
self._s.close() # close socket
self._s = None
for c in self._c:
c.close() # will also remove client from list
def _accept_handler(self, self_s):
# """ handler for new incoming connections """
self._acc_todo = True
def _accept(self, self_s):
print("free:", esp.freemem())
if len(self._c) >= self._c_max: # -> no more connections allowed
print("no more allowed")
return
if time.ticks_diff(time.ticks_ms(), self._acc_timer) < self._acc_cleartime: # -> wait
return
self._acc_todo = False
print("try")
c_sock, c_addr = self._s.accept() # client socket, client address (self_s == self._s, passed as param)
print("New connection:", c_addr)
print("free :", esp.freemem())
if len(self._c) >= self._c_max: # -> no more connections allowed
print("no more allowed")
try:
c_sock.setblocking(True) # block until the following operations are finished (or system returns error)
c_sock.write("HTTP/1.1 503 Too many open connections\nStatus: 503 Too many open connections\n\n")
#c_sock.write("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\n"
# "<html><head><title>Error 503</title></head><body>Too many connections.</body></html>")
pass
except OSError:
print("OSError xyz")
print(esp.freemem())
pass
c_sock.close()
self._acc_timer = time.ticks_ms()
return
try:
print("Performing handshake with", c_addr)
websocket_helper.server_handshake(c_sock)
except OSError: # handshake failed (no websocket connection)
self._provide_default(c_sock)
else:
print("Success. Adding client...")
c = self.Client()
c.setup(c_sock, c_addr, self._c.remove)
self._c.append(c)
def _provide_default(self, c_sock):
if self._page is not None: # -> provide default page for new connection
print("Provide default page...")
try:
c_sock.write('HTTP/1.1 200 OK\n'
'Connection: close\n'
'Server: WebSocket Server\n'
'Content-Type: text/html\n')
c_sock.write('Content-Length: {}\n\n'.format(os.stat(self._page)[6]))
with open(self._page, 'r') as f:
for ln in f:
c_sock.write(ln)
print("Serving complete!")
except OSError as e: # e. g. ENOMEM (104) while serving content or default file not found
print("Error while serving:", e.args[0])
self._acc_timer = time.ticks_ms()
c_sock.close()
print("Client socket closed")
def process(self):
for c in self._c: # for every client
c.process()
#if self._acc_todo:
# self._accept()
Code: Select all
from webserver import WebSocketServer, WebSocketClient, ClientClosedError
import utime as time
import esp
class Client(WebSocketClient):
def __init__(self):
super().__init__()
def process(self): # process routine
try:
self.routine()
except ClientClosedError:
self.close()
def routine(self):
msg = self.read()
if msg is not None:
msg = msg.decode("utf-8")
items = msg.split(" ")
cmd = items[0]
if cmd == "Question":
self.write(cmd + " Answer")
print("Q A")
else:
self.write(str(self.address) + "; time: %d; freemem: %d" % (time.ticks_ms(), esp.freemem()))
def main():
server = WebSocketServer(Client, "test.html", 2)
server.start()
try:
while True:
server.process()
except KeyboardInterrupt:
pass
server.stop()
if __name__ == "__main__":
main()
Code: Select all
<!DOCTYPE html>
<html>
<head>
<title>ESP8266 WebSocket Client</title>
<meta charset="UTF-8">
</head>
<body>
<header>
<h1>ESP8266</h1>
<h2>WebSocket Client</h2>
</header>
Received: <br/>
<div id="response">
(nothing)
</div>
<div id="in_0" class="input_bt">IN0<div id="in_0_state">--</div></div>
<div id="out_0" class="output_bt" onclick="ws.send('Question')">OUT0<div id="out_0_state">--</div></div>
<script>
var ws = null;
connect();
function connect() {
ws = new WebSocket("ws://" + "192.168.178.51" + ":80"); //location.hostname
ws.onopen = function() {
console.log("Start")
ws.send("Question");
}
ws.onmessage = function(evt) {
// JSON.parse(evt.data);
update(evt.data)
}
ws.onclose = function(evt) {
console.log("Closed");
document.getElementById("response").innerHTML = "Closed";
}
}
function update(data) {
console.log("Update")
document.getElementById("response").innerHTML = data;
}
</script>
</body>
</html>