Live Webserver using websockets (How to block new connections?)

All ESP8266 boards running MicroPython.
Official boards are the Adafruit Huzzah and Feather boards.
Target audience: MicroPython users with an ESP8266 board.
Post Reply
crizeo
Posts: 42
Joined: Sun Aug 06, 2017 12:55 pm
Location: Germany

Live Webserver using websockets (How to block new connections?)

Post by crizeo » Thu Aug 31, 2017 11:19 am

EDIT:

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
After a few requests, there can happen different problems (and these are the problems in my code as well)
  • 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
Does anybody have an idea about whats going on here and How can I prevent this?


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


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()
And the main file using WebSocketServer [main.py]:

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()
This is my HTML file (client side) [test.html]:

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>
Here how it look when it's working (two clients connected, time updated live):
test.PNG
test.PNG (71.53 KiB) Viewed 4318 times
When I reload one of the above tabs, I can't connect again (seems like new connection is tried to be established before old one is closed). If I open a new tab (I normally use Internet Explorer, because Firefox seems to send ten requests when blocked), I won't get any debug messages on the WebREPL any more and if I then close the working tabs and try to open a new one, I cannot connect any more...

crizeo
Posts: 42
Joined: Sun Aug 06, 2017 12:55 pm
Location: Germany

Re: Live Webserver using websockets (How to block new connections?)

Post by crizeo » Thu Aug 31, 2017 2:47 pm

Probably found a workaround. When the last possible client has been added successfully I call self._s.setsockopt(socket.SOL_SOCKET, 0x14, None), which will remove the accept handler completely. When a client is closed, I therefore have to reactivacte the handler.

Unfortunately, I will of course not be able to return the 503 status code if no more connections are possible. But no one will be able to crash the server now - still better.

Edit: Not working...

Post Reply