1#!/usr/bin/env python
3import asyncio
5from websockets.asyncio.server import serve
7async def hello(websocket):
8 name = await websocket.recv()
9 print(f"<<< {name}")
11 greeting = f"Hello {name}!"
13 await websocket.send(greeting)
14 print(f">>> {greeting}")
16async def main():
17 async with serve(hello, "localhost", 8765):
18 await asyncio.get_running_loop().create_future() # run forever
20if __name__ == "__main__":
21 asyncio.run(main())
serve()
executes the connection handler coroutine
hello()
once for each WebSocket connection. It closes the WebSocket
connection when the handler returns.
Here’s a corresponding WebSocket client.
It sends a name to the server, receives a greeting, and closes the connection.
client.py
1#!/usr/bin/env python
3import asyncio
5from websockets.asyncio.client import connect
7async def hello():
8 uri = "ws://localhost:8765"
9 async with connect(uri) as websocket:
10 name = input("What's your name? ")
12 await websocket.send(name)
13 print(f">>> {name}")
15 greeting = await websocket.recv()
16 print(f"<<< {greeting}")
18if __name__ == "__main__":
19 asyncio.run(hello())
Using connect()
as an asynchronous context manager ensures
the WebSocket connection is closed.
Encrypt connections
Secure WebSocket connections improve confidentiality and also reliability
because they reduce the risk of interference by bad proxies.
The wss
protocol is to ws
what https
is to http
. The
connection is encrypted with TLS (Transport Layer Security). wss
requires certificates like https
.
TLS vs. SSL
TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an
earlier encryption protocol; the name stuck.
Here’s how to adapt the server to encrypt connections. You must download
localhost.pem
and save it
in the same directory as server_secure.py
.
server_secure.py
1#!/usr/bin/env python
3import asyncio
4import pathlib
5import ssl
7from websockets.asyncio.server import serve
9async def hello(websocket):
10 name = await websocket.recv()
11 print(f"<<< {name}")
13 greeting = f"Hello {name}!"
15 await websocket.send(greeting)
16 print(f">>> {greeting}")
18ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
19localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
20ssl_context.load_cert_chain(localhost_pem)
22async def main():
23 async with serve(hello, "localhost", 8765, ssl=ssl_context):
24 await asyncio.get_running_loop().create_future() # run forever
26if __name__ == "__main__":
27 asyncio.run(main())
Here’s how to adapt the client similarly.
client_secure.py
1#!/usr/bin/env python
3import asyncio
4import pathlib
5import ssl
7from websockets.asyncio.client import connect
9ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
10localhost_pem = pathlib.Path(__file__).with_name("localhost.pem")
11ssl_context.load_verify_locations(localhost_pem)
13async def hello():
14 uri = "wss://localhost:8765"
15 async with connect(uri, ssl=ssl_context) as websocket:
16 name = input("What's your name? ")
18 await websocket.send(name)
19 print(f">>> {name}")
21 greeting = await websocket.recv()
22 print(f"<<< {greeting}")
24if __name__ == "__main__":
25 asyncio.run(hello())
In this example, the client needs a TLS context because the server uses a
self-signed certificate.
When connecting to a secure WebSocket server with a valid certificate — any
certificate signed by a CA that your Python installation trusts — you can simply
pass ssl=True
to connect()
.
Configure the TLS context securely
This example demonstrates the ssl
argument with a TLS certificate shared
between the client and the server. This is a simplistic setup.
Please review the advice and security considerations in the documentation of
the ssl
module to configure the TLS context securely.
Connect from a browser
The WebSocket protocol was invented for the web — as the name says!
Here’s how to connect to a WebSocket server from a browser.
Run this script in a console:
show_time.py
1#!/usr/bin/env python
3import asyncio
4import datetime
5import random
7from websockets.asyncio.server import serve
9async def show_time(websocket):
10 while True:
11 message = datetime.datetime.utcnow().isoformat() + "Z"
12 await websocket.send(message)
13 await asyncio.sleep(random.random() * 2 + 1)
15async def main():
16 async with serve(show_time, "localhost", 5678):
17 await asyncio.get_running_loop().create_future() # run forever
19if __name__ == "__main__":
20 asyncio.run(main())
Save this file as show_time.html
:
show_time.html
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <title>WebSocket demo</title>
5 </head>
6 <body>
7 <script src="show_time.js"></script>
8 </body>
9</html>
Save this file as show_time.js
:
show_time.js
1window.addEventListener("DOMContentLoaded", () => {
2 const messages = document.createElement("ul");
3 document.body.appendChild(messages);
5 const websocket = new WebSocket("ws://localhost:5678/");
6 websocket.onmessage = ({ data }) => {
7 const message = document.createElement("li");
8 const content = document.createTextNode(data);
9 message.appendChild(content);
10 messages.appendChild(message);
11 };
12});
Then, open show_time.html
in several browsers. Clocks tick irregularly.
Broadcast messages
Let’s change the previous example to send the same timestamps to all browsers,
instead of generating independent sequences for each client.
Stop the previous script if it’s still running and run this script in a console:
show_time_2.py
1#!/usr/bin/env python
3import asyncio
4import datetime
5import random
7from websockets.asyncio.server import broadcast, serve
9CONNECTIONS = set()
11async def register(websocket):
12 CONNECTIONS.add(websocket)
13 try:
14 await websocket.wait_closed()
15 finally:
16 CONNECTIONS.remove(websocket)
18async def show_time():
19 while True:
20 message = datetime.datetime.utcnow().isoformat() + "Z"
21 broadcast(CONNECTIONS, message)
22 await asyncio.sleep(random.random() * 2 + 1)
24async def main():
25 async with serve(register, "localhost", 5678):
26 await show_time()
28if __name__ == "__main__":
29 asyncio.run(main())
Refresh show_time.html
in all browsers. Clocks tick in sync.
Manage application state
A WebSocket server can receive events from clients, process them to update the
application state, and broadcast the updated state to all connected clients.
Here’s an example where any client can increment or decrement a counter. The
concurrency model of asyncio
guarantees that updates are serialized.
Run this script in a console:
counter.py
1#!/usr/bin/env python
3import asyncio
4import json
5import logging
6from websockets.asyncio.server import broadcast, serve
8logging.basicConfig()
10USERS = set()
12VALUE = 0
14def users_event():
15 return json.dumps({"type": "users", "count": len(USERS)})
17def value_event():
18 return json.dumps({"type": "value", "value": VALUE})
20async def counter(websocket):
21 global USERS, VALUE
22 try:
23 # Register user
24 USERS.add(websocket)
25 broadcast(USERS, users_event())
26 # Send current state to user
27 await websocket.send(value_event())
28 # Manage state changes
29 async for message in websocket:
30 event = json.loads(message)
31 if event["action"] == "minus":
32 VALUE -= 1
33 broadcast(USERS, value_event())
34 elif event["action"] == "plus":
35 VALUE += 1
36 broadcast(USERS, value_event())
37 else:
38 logging.error("unsupported event: %s", event)
39 finally:
40 # Unregister user
41 USERS.remove(websocket)
42 broadcast(USERS, users_event())
44async def main():
45 async with serve(counter, "localhost", 6789):
46 await asyncio.get_running_loop().create_future() # run forever
48if __name__ == "__main__":
49 asyncio.run(main())
Save this file as counter.html
:
counter.html
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <title>WebSocket demo</title>
5 <link href="counter.css" rel="stylesheet">
6 </head>
7 <body>
8 <div class="buttons">
9 <div class="minus button">-</div>
10 <div class="value">?</div>
11 <div class="plus button">+</div>
12 </div>
13 <div class="state">
14 <span class="users">?</span> online
15 </div>
16 <script src="counter.js"></script>
17 </body>
18</html>
Save this file as counter.css
:
counter.css
1body {
2 font-family: "Courier New", sans-serif;
3 text-align: center;
5.buttons {
6 font-size: 4em;
7 display: flex;
8 justify-content: center;
10.button, .value {
11 line-height: 1;
12 padding: 2rem;
13 margin: 2rem;
14 border: medium solid;
15 min-height: 1em;
16 min-width: 1em;
18.button {
19 cursor: pointer;
20 user-select: none;
22.minus {
23 color: red;
25.plus {
26 color: green;
28.value {
29 min-width: 2em;
31.state {
32 font-size: 2em;
Save this file as counter.js
:
counter.js
1window.addEventListener("DOMContentLoaded", () => {
2 const websocket = new WebSocket("ws://localhost:6789/");
4 document.querySelector(".minus").addEventListener("click", () => {
5 websocket.send(JSON.stringify({ action: "minus" }));
6 });
8 document.querySelector(".plus").addEventListener("click", () => {
9 websocket.send(JSON.stringify({ action: "plus" }));
10 });
12 websocket.onmessage = ({ data }) => {
13 const event = JSON.parse(data);
14 switch (event.type) {
15 case "value":
16 document.querySelector(".value").textContent = event.value;
17 break;
18 case "users":
19 const users = `${event.count} user${event.count == 1 ? "" : "s"}`;
20 document.querySelector(".users").textContent = users;
21 break;
22 default:
23 console.error("unsupported event", event);
24 }
25 };
26});
Then open counter.html
file in several browsers and play with [+] and [-].