├── .gitignore
├── LICENSE
├── Multiserver
├── one.jpg
├── test.css
├── test.html
├── three.jpg
├── two.jpg
├── websocket_multi_demo.py
└── ws_multiserver.py
├── Poll (ESP32 fix)
├── ws_connection.py
└── ws_server.py
├── README.md
├── test.html
├── websocket_demo.py
├── ws_connection.py
└── ws_server.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ivan Sevcik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Multiserver/one.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/one.jpg
--------------------------------------------------------------------------------
/Multiserver/test.css:
--------------------------------------------------------------------------------
1 | #response {
2 | color: blue;
3 | font-weight: bold;
4 | }
5 |
6 | body {
7 | font-size: xx-large
8 | }
--------------------------------------------------------------------------------
/Multiserver/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WebSocket Client
6 |
7 |
8 |
9 |
10 |
15 |
16 | Received:
17 |
18 | (Received content)
19 |
20 |
21 |
22 |
35 |
36 |
--------------------------------------------------------------------------------
/Multiserver/three.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/three.jpg
--------------------------------------------------------------------------------
/Multiserver/two.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BetaRavener/upy-websocket-server/676c24ae196665bee1ae3df41190c66d34027e95/Multiserver/two.jpg
--------------------------------------------------------------------------------
/Multiserver/websocket_multi_demo.py:
--------------------------------------------------------------------------------
1 | from ws_connection import ClientClosedError
2 | from ws_server import WebSocketClient
3 | from ws_multiserver import WebSocketMultiServer
4 |
5 |
6 | class TestClient(WebSocketClient):
7 | def __init__(self, conn):
8 | super().__init__(conn)
9 |
10 | def process(self):
11 | try:
12 | msg = self.connection.read()
13 | if not msg:
14 | return
15 | msg = msg.decode("utf-8")
16 | items = msg.split(" ")
17 | cmd = items[0]
18 | if cmd == "Hello":
19 | self.connection.write(cmd + " World")
20 | print("Hello World")
21 | except ClientClosedError:
22 | self.connection.close()
23 |
24 |
25 | class TestServer(WebSocketMultiServer):
26 | def __init__(self):
27 | super().__init__("test.html", 10)
28 |
29 | def _make_client(self, conn):
30 | return TestClient(conn)
31 |
32 |
33 | server = TestServer()
34 | server.start()
35 | try:
36 | while True:
37 | server.process_all()
38 | except KeyboardInterrupt:
39 | pass
40 | server.stop()
41 |
--------------------------------------------------------------------------------
/Multiserver/ws_multiserver.py:
--------------------------------------------------------------------------------
1 | import os
2 | import websocket_helper
3 | from time import sleep
4 |
5 | from ws_server import WebSocketServer
6 | from ws_connection import WebSocketConnection
7 |
8 |
9 | class WebSocketMultiServer(WebSocketServer):
10 | http_codes = {
11 | 200: "OK",
12 | 404: "Not Found",
13 | 500: "Internal Server Error",
14 | 503: "Service Unavailable"
15 | }
16 |
17 | mime_types = {
18 | "jpg": "image/jpeg",
19 | "jpeg": "image/jpeg",
20 | "png": "image/png",
21 | "gif": "image/gif",
22 | "html": "text/html",
23 | "htm": "text/html",
24 | "css": "text/css",
25 | "js": "application/javascript"
26 | }
27 |
28 | def __init__(self, index_page, max_connections=1):
29 | super().__init__(index_page, max_connections)
30 | dir_idx = index_page.rfind("/")
31 | self._web_dir = index_page[0:dir_idx] if dir_idx > 0 else "/"
32 |
33 | def _accept_conn(self, listen_sock):
34 | cl, remote_addr = self._listen_s.accept()
35 | print("Client connection from:", remote_addr)
36 |
37 | if len(self._clients) >= self._max_connections:
38 | # Maximum connections limit reached
39 | cl.setblocking(True)
40 | self._generate_static_page(cl, 503, "503 Too Many Connections")
41 | return
42 |
43 | requested_file = None
44 | data = cl.recv(64).decode()
45 | if data and "Upgrade: websocket" not in data.split("\r\n") and "GET" == data.split(" ")[0]:
46 | # data should looks like GET /index.html HTTP/1.1\r\nHost: 19"
47 | # requested file is on second position in data, ignore all get parameters after question mark
48 | requested_file = data.split(" ")[1].split("?")[0]
49 | requested_file = self._page if requested_file in [None, "/"] else requested_file
50 |
51 | try:
52 | websocket_helper.server_handshake(cl)
53 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection)))
54 | except OSError:
55 | if requested_file:
56 | cl.setblocking(True)
57 | self._serve_file(requested_file, cl)
58 | else:
59 | self._generate_static_page(cl, 500, "500 Internal Server Error [2]")
60 |
61 | def _serve_file(self, requested_file, c_socket):
62 | print("### Serving file: {}".format(requested_file))
63 | try:
64 | # check if file exists in web directory
65 | path = requested_file.split("/")
66 | filename = path[-1]
67 | subdir = "/" + "/".join(path[1:-1]) if len(path) > 2 else ""
68 |
69 | if filename not in os.listdir(self._web_dir + subdir):
70 | self._generate_static_page(c_socket, 404, "404 Not Found")
71 | return
72 |
73 | # Create path based on web root directory
74 | file_path = self._web_dir + requested_file
75 | length = os.stat(file_path)[6]
76 | c_socket.sendall(self._generate_headers(200, file_path, length))
77 | # Send file by chunks to prevent large memory consumption
78 | chunk_size = 1024
79 | with open(file_path, "rb") as f:
80 | while True:
81 | data = f.read(chunk_size)
82 | c_socket.sendall(data)
83 | if len(data) < chunk_size:
84 | break
85 | sleep(0.1)
86 | c_socket.close()
87 | except OSError:
88 | self._generate_static_page(c_socket, 500, "500 Internal Server Error [2]")
89 |
90 | @staticmethod
91 | def _generate_headers(code, filename=None, length=None):
92 | content_type = "text/html"
93 |
94 | if filename:
95 | ext = filename.split(".")[1]
96 | if ext in WebSocketMultiServer.mime_types:
97 | content_type = WebSocketMultiServer.mime_types[ext]
98 |
99 | # Close connection after completing the request
100 | return "HTTP/1.1 {} {}\n" \
101 | "Content-Type: {}\n" \
102 | "Content-Length: {}\n" \
103 | "Server: ESPServer\n" \
104 | "Connection: close\n\n".format(
105 | code, WebSocketMultiServer.http_codes[code], content_type, length)
106 |
107 | @staticmethod
108 | def _generate_static_page(sock, code, message):
109 | sock.sendall(WebSocketMultiServer._generate_headers(code))
110 | sock.sendall("" + message + " ")
111 | sleep(0.1)
112 | sock.close()
113 |
--------------------------------------------------------------------------------
/Poll (ESP32 fix)/ws_connection.py:
--------------------------------------------------------------------------------
1 | import socket
2 | from websocket import websocket
3 | import uselect
4 |
5 |
6 | class ClientClosedError(Exception):
7 | pass
8 |
9 |
10 | class WebSocketConnection:
11 | def __init__(self, addr, s, close_callback):
12 | self.client_close = False
13 | self._need_check = False
14 |
15 | self.address = addr
16 | self.socket = s
17 | self.ws = websocket(s, True)
18 | self.poll = uselect.poll()
19 | self.close_callback = close_callback
20 |
21 | self.socket.setblocking(False)
22 | self.poll.register(self.socket, uselect.POLLIN)
23 |
24 | def read(self):
25 | poll_events = self.poll.poll(0)
26 |
27 | if not poll_events:
28 | return
29 |
30 | # Check the flag for connection hung up
31 | if poll_events[0][1] & uselect.POLLHUP:
32 | self.client_close = True
33 |
34 | msg_bytes = None
35 | try:
36 | msg_bytes = self.ws.read()
37 | except OSError:
38 | self.client_close = True
39 |
40 | # If no bytes => connection closed. See the link below.
41 | # http://stefan.buettcher.org/cs/conn_closed.html
42 | if not msg_bytes or self.client_close:
43 | raise ClientClosedError()
44 |
45 | return msg_bytes
46 |
47 | def write(self, msg):
48 | try:
49 | self.ws.write(msg)
50 | except OSError:
51 | self.client_close = True
52 |
53 | def is_closed(self):
54 | return self.socket is None
55 |
56 | def close(self):
57 | print("Closing connection.")
58 | self.poll.unregister(self.socket)
59 | self.socket.close()
60 | self.socket = None
61 | self.ws = None
62 | if self.close_callback:
63 | self.close_callback(self)
64 |
--------------------------------------------------------------------------------
/Poll (ESP32 fix)/ws_server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import socket
3 | import network
4 | import websocket_helper
5 | import uselect
6 | from time import sleep
7 | from ws_connection import WebSocketConnection, ClientClosedError
8 |
9 |
10 | class WebSocketClient:
11 | def __init__(self, conn):
12 | self.connection = conn
13 |
14 | def process(self):
15 | pass
16 |
17 |
18 | class WebSocketServer:
19 | def __init__(self, page, max_connections=1):
20 | self._listen_s = None
21 | self._listen_poll = None
22 | self._clients = []
23 | self._max_connections = max_connections
24 | self._page = page
25 |
26 | def _setup_conn(self, port):
27 | self._listen_s = socket.socket()
28 | self._listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
29 | self._listen_poll = uselect.poll()
30 |
31 | ai = socket.getaddrinfo("0.0.0.0", port)
32 | addr = ai[0][4]
33 |
34 | self._listen_s.bind(addr)
35 | self._listen_s.listen(1)
36 | self._listen_poll.register(self._listen_s)
37 | for i in (network.AP_IF, network.STA_IF):
38 | iface = network.WLAN(i)
39 | if iface.active():
40 | print("WebSocket started on ws://%s:%d" % (iface.ifconfig()[0], port))
41 |
42 | def _check_new_connections(self, accept_handler):
43 | poll_events = self._listen_poll.poll(0)
44 | if not poll_events:
45 | return
46 |
47 | if poll_events[0][1] & uselect.POLLIN:
48 | accept_handler()
49 |
50 | def _accept_conn(self):
51 | cl, remote_addr = self._listen_s.accept()
52 | print("Client connection from:", remote_addr)
53 |
54 | if len(self._clients) >= self._max_connections:
55 | # Maximum connections limit reached
56 | cl.setblocking(True)
57 | cl.sendall("HTTP/1.1 503 Too many connections\n\n")
58 | cl.sendall("\n")
59 | #TODO: Make sure the data is sent before closing
60 | sleep(0.1)
61 | cl.close()
62 | return
63 |
64 | try:
65 | websocket_helper.server_handshake(cl)
66 | except OSError:
67 | # Not a websocket connection, serve webpage
68 | self._serve_page(cl)
69 | return
70 |
71 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection)))
72 |
73 | def _make_client(self, conn):
74 | return WebSocketClient(conn)
75 |
76 | def _serve_page(self, sock):
77 | try:
78 | sock.sendall('HTTP/1.1 200 OK\nConnection: close\nServer: WebSocket Server\nContent-Type: text/html\n')
79 | length = os.stat(self._page)[6]
80 | sock.sendall('Content-Length: {}\n\n'.format(length))
81 | # Process page by lines to avoid large strings
82 | with open(self._page, 'r') as f:
83 | for line in f:
84 | sock.sendall(line)
85 | except OSError:
86 | # Error while serving webpage
87 | pass
88 | sock.close()
89 |
90 | def stop(self):
91 | if self._listen_poll:
92 | self._listen_poll.unregister(self._listen_s)
93 | self._listen_poll = None
94 | if self._listen_s:
95 | self._listen_s.close()
96 | self._listen_s = None
97 |
98 | for client in self._clients:
99 | client.connection.close()
100 | print("Stopped WebSocket server.")
101 |
102 | def start(self, port=80):
103 | if self._listen_s:
104 | self.stop()
105 | self._setup_conn(port)
106 | print("Started WebSocket server.")
107 |
108 | def process_all(self):
109 | self._check_new_connections(self._accept_conn)
110 |
111 | for client in self._clients:
112 | client.process()
113 |
114 | def remove_connection(self, conn):
115 | for client in self._clients:
116 | if client.connection is conn:
117 | self._clients.remove(client)
118 | return
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # upy-websocket-server
2 | Micropython (ESP8266) websocket server implementation.
3 |
4 | Upload all scripts and HTML page to device and execute the `websocket_demo.py` script.
5 |
6 | When client connects to the device, `test.html` is served to him, which in turn makes websocket connection to the device and greets it with `Hello`. The device acknowledges this and replies with ` World` appended, which makes client display `Hello World`.
7 |
8 | New implementations can be made by subclassing `WebSocketClient` and `WebSocketServer` as shown in `websocket_demo.py`.
9 |
10 | ### ESP32 users
11 | Because `setsockopt` can't be used on ESP32 to install event handlers, new implementation using `Poll` class has been made and is available in separate folder. From user perspective, both implementation behave the same. The folder contains only files that are different so don't forget to place other files from root folder on your ESP.
12 |
13 | Also, if you're having problems importing `websocket` class, make sure that it's available in your firmware using following commands. `websocket` class should be listed in the output.
14 | ```
15 | import websocket # This is importing module, not class
16 | dir(websocket) # List everything that module supports
17 | ```
18 |
19 | Micropython [revision 0d5bccad](https://github.com/micropython/micropython/commit/0d5bccad) is confirmed to be working correctly.
20 | Thanks for this goes to [@plugowski](https://github.com/plugowski).
21 |
22 | ### Support for multiple files
23 | It is recommended to keep the page served by server simple as the transfer speed is quite slow. Due to this, the basic implementation supports serving only one HTML page and websocket communication. However, if you really need to use multiple files (images, etc.) you can find extended implementation in `Multiserver` folder. You only need `ws_multiserver.py` file. The rest of files are for demo purposes. If you want to try it out, copy all files from the mentioned folder and run `websocket_multi_demo.py`. The demo page consists of 3 images and CSS file which are all served along the HTML page and websocket communication that happens after content is loaded.
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WebSocket Client
6 |
7 |
8 |
9 | Received:
10 |
11 | (Received content)
12 |
13 |
14 |
25 |
26 |
--------------------------------------------------------------------------------
/websocket_demo.py:
--------------------------------------------------------------------------------
1 | from ws_connection import ClientClosedError
2 | from ws_server import WebSocketServer, WebSocketClient
3 |
4 |
5 | class TestClient(WebSocketClient):
6 | def __init__(self, conn):
7 | super().__init__(conn)
8 |
9 | def process(self):
10 | try:
11 | msg = self.connection.read()
12 | if not msg:
13 | return
14 | msg = msg.decode("utf-8")
15 | items = msg.split(" ")
16 | cmd = items[0]
17 | if cmd == "Hello":
18 | self.connection.write(cmd + " World")
19 | print("Hello World")
20 | except ClientClosedError:
21 | self.connection.close()
22 |
23 |
24 | class TestServer(WebSocketServer):
25 | def __init__(self):
26 | super().__init__("test.html", 2)
27 |
28 | def _make_client(self, conn):
29 | return TestClient(conn)
30 |
31 |
32 | server = TestServer()
33 | server.start()
34 | try:
35 | while True:
36 | server.process_all()
37 | except KeyboardInterrupt:
38 | pass
39 | server.stop()
40 |
--------------------------------------------------------------------------------
/ws_connection.py:
--------------------------------------------------------------------------------
1 | import socket
2 | from websocket import websocket
3 |
4 |
5 | class ClientClosedError(Exception):
6 | pass
7 |
8 |
9 | class WebSocketConnection:
10 | def __init__(self, addr, s, close_callback):
11 | self.client_close = False
12 | self._need_check = False
13 |
14 | self.address = addr
15 | self.socket = s
16 | self.ws = websocket(s, True)
17 | self.close_callback = close_callback
18 |
19 | s.setblocking(False)
20 | s.setsockopt(socket.SOL_SOCKET, 20, self.notify)
21 |
22 | def notify(self, s):
23 | self._need_check = True
24 |
25 | def read(self):
26 | if self._need_check:
27 | self._check_socket_state()
28 |
29 | msg_bytes = None
30 | try:
31 | msg_bytes = self.ws.read()
32 | except OSError:
33 | self.client_close = True
34 |
35 | if not msg_bytes and self.client_close:
36 | raise ClientClosedError()
37 |
38 | return msg_bytes
39 |
40 | def write(self, msg):
41 | try:
42 | self.ws.write(msg)
43 | except OSError:
44 | self.client_close = True
45 |
46 | def _check_socket_state(self):
47 | self._need_check = False
48 | sock_str = str(self.socket)
49 | state_str = sock_str.split(" ")[1]
50 | state = int(state_str.split("=")[1])
51 |
52 | if state == 4:
53 | self.client_close = True
54 |
55 | def is_closed(self):
56 | return self.socket is None
57 |
58 | def close(self):
59 | print("Closing connection.")
60 | self.socket.setsockopt(socket.SOL_SOCKET, 20, None)
61 | self.socket.close()
62 | self.socket = None
63 | self.ws = None
64 | if self.close_callback:
65 | self.close_callback(self)
66 |
--------------------------------------------------------------------------------
/ws_server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import socket
3 | import network
4 | import websocket_helper
5 | from time import sleep
6 | from ws_connection import WebSocketConnection, ClientClosedError
7 |
8 |
9 | class WebSocketClient:
10 | def __init__(self, conn):
11 | self.connection = conn
12 |
13 | def process(self):
14 | pass
15 |
16 |
17 | class WebSocketServer:
18 | def __init__(self, page, max_connections=1):
19 | self._listen_s = None
20 | self._clients = []
21 | self._max_connections = max_connections
22 | self._page = page
23 |
24 | def _setup_conn(self, port, accept_handler):
25 | self._listen_s = socket.socket()
26 | self._listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
27 |
28 | ai = socket.getaddrinfo("0.0.0.0", port)
29 | addr = ai[0][4]
30 |
31 | self._listen_s.bind(addr)
32 | self._listen_s.listen(1)
33 | if accept_handler:
34 | self._listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
35 | for i in (network.AP_IF, network.STA_IF):
36 | iface = network.WLAN(i)
37 | if iface.active():
38 | print("WebSocket started on ws://%s:%d" % (iface.ifconfig()[0], port))
39 |
40 | def _accept_conn(self, listen_sock):
41 | cl, remote_addr = listen_sock.accept()
42 | print("Client connection from:", remote_addr)
43 |
44 | if len(self._clients) >= self._max_connections:
45 | # Maximum connections limit reached
46 | cl.setblocking(True)
47 | cl.sendall("HTTP/1.1 503 Too many connections\n\n")
48 | cl.sendall("\n")
49 | #TODO: Make sure the data is sent before closing
50 | sleep(0.1)
51 | cl.close()
52 | return
53 |
54 | try:
55 | websocket_helper.server_handshake(cl)
56 | except OSError:
57 | # Not a websocket connection, serve webpage
58 | self._serve_page(cl)
59 | return
60 |
61 | self._clients.append(self._make_client(WebSocketConnection(remote_addr, cl, self.remove_connection)))
62 |
63 | def _make_client(self, conn):
64 | return WebSocketClient(conn)
65 |
66 | def _serve_page(self, sock):
67 | try:
68 | sock.sendall('HTTP/1.1 200 OK\nConnection: close\nServer: WebSocket Server\nContent-Type: text/html\n')
69 | length = os.stat(self._page)[6]
70 | sock.sendall('Content-Length: {}\n\n'.format(length))
71 | # Process page by lines to avoid large strings
72 | with open(self._page, 'r') as f:
73 | for line in f:
74 | sock.sendall(line)
75 | except OSError:
76 | # Error while serving webpage
77 | pass
78 | sock.close()
79 |
80 | def stop(self):
81 | if self._listen_s:
82 | self._listen_s.close()
83 | self._listen_s = None
84 | for client in self._clients:
85 | client.connection.close()
86 | print("Stopped WebSocket server.")
87 |
88 | def start(self, port=80):
89 | if self._listen_s:
90 | self.stop()
91 | self._setup_conn(port, self._accept_conn)
92 | print("Started WebSocket server.")
93 |
94 | def process_all(self):
95 | for client in self._clients:
96 | client.process()
97 |
98 | def remove_connection(self, conn):
99 | for client in self._clients:
100 | if client.connection is conn:
101 | self._clients.remove(client)
102 | return
103 |
--------------------------------------------------------------------------------