Skip to content

Commit 0576849

Browse files
authored
WebSocket example (#5)
1 parent 018bce8 commit 0576849

9 files changed

Lines changed: 244 additions & 6 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# BlackSheep-Examples
22
Various examples for BlackSheep.
33

4-
| Example | Description |
5-
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6-
| [./testing-api/](./testing-api/) | Shows how to test a BlackSheep API using `pytest` and the provided `TestClient` (see also [testing](https://www.neoteroi.dev/blacksheep/testing/)) |
7-
| [./piccolo-admin/](./piccolo-admin/) | Shows how to use the mount feature to use [Piccolo Admin](https://github.com/piccolo-orm/piccolo_admin) in BlackSheep |
8-
| [./jwt-validation](./jwt-validation) | Shows how to configure a BlackSheep API that uses JWTs to implement authentication and authorization for users |
9-
| [./aad-machine-to-machine](./aad-machine-to-machine) | Shows how to configure an API that requires access tokens issued by Azure Active Directory, and how to obtain access tokens using [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) for a confidential client (machine to machine communication). |
4+
| Example | Description |
5+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
6+
| [./testing-api/](./testing-api/) | Shows how to test a BlackSheep API using `pytest` and the provided `TestClient` (see also [testing](https://www.neoteroi.dev/blacksheep/testing/)) |
7+
| [./piccolo-admin/](./piccolo-admin/) | Shows how to use the mount feature to use [Piccolo Admin](https://github.com/piccolo-orm/piccolo_admin) in BlackSheep |
8+
| [./jwt-validation](./jwt-validation) | Shows how to configure a BlackSheep API that uses JWTs to implement authentication and authorization for users |
9+
| [./aad-machine-to-machine](./aad-machine-to-machine) | Shows how to configure an API that requires access tokens issued by Azure Active Directory, and how to obtain access tokens using [MSAL for Python](https://github.com/AzureAD/microsoft-authentication-library-for-python) for a confidential client (machine to machine communication) |
10+
| [./websocket-chat](./websocket-chat) | Shows how to use WebSocket with BlackSheep, the example consists of a simple chat application built using WebSocket and VueJS |

websocket-chat/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Chat app example
2+
3+
This folder contains a simple chat application built using WebSocket and VueJS.
4+
You can use it as a starting point to build your own real-time application using
5+
WebSocket.
6+
7+
Bear in mind, though, that this is merely an example. In the real world, you would
8+
probably like to use a message queue like Redis to broadcast messages to the clients
9+
and some persistent storage like PostgreSQL or MongoDB to store your messages, users, etc.
10+
11+
## Getting started
12+
13+
1. Create a Python virtual environment
14+
2. Activate the virtual environment
15+
3. Install dependencies in `requirements.txt`
16+
4. Run the application using `uvicorn --reload server:app`
17+
5. Navigate to `http://localhost:8000` in your browser and try sending
18+
some messages. You can open it in multiple tabs to simulate
19+
multiple clients connected.
20+
21+
```bash
22+
# create a Python virtual environment
23+
python -m venv venv
24+
25+
# activate
26+
source venv/bin/activate # (Linux)
27+
28+
venv\Scripts\activate # (Windows)
29+
30+
# install dependencies
31+
pip install -r requirements.txt
32+
33+
# run app
34+
uvicorn --reload server:app
35+
```

websocket-chat/app/__init__.py

Whitespace-only changes.

websocket-chat/app/connection.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from datetime import datetime
2+
from typing import List
3+
4+
from blacksheep import WebSocket
5+
from .message import Message
6+
7+
8+
class Connection:
9+
def __init__(self, socket: WebSocket, client_id: str):
10+
self.socket = socket
11+
self.client_id = client_id
12+
13+
async def receive(self) -> Message:
14+
data = await self.socket.receive_json()
15+
message = Message(**data)
16+
return message
17+
18+
async def send(self, message: Message):
19+
await self.socket.send_json(message.asdict())
20+
21+
22+
class ConnectionManager:
23+
def __init__(self):
24+
self.active_connections: List[Connection] = []
25+
26+
async def connect(self, websocket: WebSocket, client_id: str) -> Connection:
27+
await websocket.accept()
28+
connection = Connection(websocket, client_id)
29+
await self.greet(connection)
30+
self.active_connections.append(connection)
31+
return connection
32+
33+
async def disconnect(self, connection: Connection):
34+
self.active_connections.remove(connection)
35+
await self.bye(connection)
36+
37+
async def manage(self, connection: Connection):
38+
message = await connection.receive()
39+
await self.broadcast(message)
40+
41+
async def broadcast(self, message: Message):
42+
print('Broadcast to %s connections' % len(self.active_connections))
43+
for connection in self.active_connections:
44+
await connection.send(message)
45+
46+
async def greet(self, connection: Connection):
47+
message = Message(
48+
author='Server',
49+
timestamp=datetime.now().isoformat(),
50+
text=f'{connection.client_id} enters the chat',
51+
)
52+
await self.broadcast(message)
53+
54+
async def bye(self, connection: Connection):
55+
message = Message(
56+
author='Server',
57+
timestamp=datetime.now().isoformat(),
58+
text=f'{connection.client_id} disconnected',
59+
)
60+
await self.broadcast(message)

websocket-chat/app/main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pathlib
2+
3+
from blacksheep import Application, WebSocket, WebSocketDisconnectError
4+
from blacksheep.server.responses import redirect
5+
from .connection import ConnectionManager
6+
7+
APP_PATH = pathlib.Path(__file__).parent / 'static'
8+
9+
app = Application()
10+
app.serve_files(APP_PATH, root_path='app')
11+
12+
manager = ConnectionManager()
13+
14+
15+
@app.router.ws('/ws/{client_id}')
16+
async def ws(websocket: WebSocket, client_id: str):
17+
conn = await manager.connect(websocket, client_id)
18+
19+
try:
20+
while True:
21+
await manager.manage(conn)
22+
except WebSocketDisconnectError:
23+
await manager.disconnect(conn)
24+
25+
26+
@app.router.get('/')
27+
def index():
28+
return redirect('/app')

websocket-chat/app/message.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import dataclasses
2+
3+
4+
@dataclasses.dataclass
5+
class Message:
6+
author: str
7+
text: str
8+
timestamp: str
9+
10+
def asdict(self):
11+
return dataclasses.asdict(self)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>BlackSheep WebSocket Example</title>
6+
<script src="https://unpkg.com/vue@next"></script>
7+
<style>
8+
.controls {
9+
display: flex;
10+
column-gap: 3px;
11+
}
12+
</style>
13+
</head>
14+
<body>
15+
<div id="app">
16+
<p><b>Your client ID is {{ CLIENT_ID }}</b></p>
17+
<p>Status: {{ status }}</p>
18+
<p>Messages:</p>
19+
<ul>
20+
<li v-for="message in messages" :key="message.timestamp">
21+
<div><b>From:</b> {{ message.author }}</div>
22+
<div><b>Sent:</b> {{ renderTimestamp(message.timestamp) }}</div>
23+
<div><b>Text:</b> {{ message.text }}</div>
24+
</li>
25+
</ul>
26+
<div class="controls" @keypress.enter.prevent="submit">
27+
<input type="text" placeholder="Your message" v-model="messageText">
28+
<button type="button" :disabled="!ws" @click.prevent="submit">Submit</button>
29+
<button type="button" :disabled="ws" @click.prevent="connect(WS_URL)">Connect</button>
30+
<button type="button" :disabled="!ws" @click.prevent="disconnect">Disconnect</button>
31+
</div>
32+
</div>
33+
<script>
34+
CLIENT_ID = crypto.randomUUID()
35+
WS_URL = `ws://${location.host}/ws/${CLIENT_ID}`
36+
37+
const app = {
38+
data() {
39+
return {
40+
messages: [],
41+
messageText: "",
42+
ws: null,
43+
status: "Disconnected",
44+
WS_URL,
45+
CLIENT_ID
46+
}
47+
},
48+
methods: {
49+
makeMessage() {
50+
return {
51+
author: this.CLIENT_ID,
52+
text: this.messageText,
53+
timestamp: new Date().toISOString()
54+
}
55+
},
56+
submit() {
57+
const message = this.makeMessage()
58+
this.ws.send(JSON.stringify(message))
59+
this.messageText = ""
60+
},
61+
connect(url) {
62+
const ws = new WebSocket(url)
63+
64+
ws.addEventListener("open", (evt) => {
65+
this.status = "Connected"
66+
console.log("Open", evt)
67+
})
68+
69+
ws.addEventListener("message", (evt) => {
70+
this.messages.push(JSON.parse(evt.data))
71+
console.log("Message", evt)
72+
})
73+
74+
ws.addEventListener("close", (evt) => {
75+
this.status = "Disconnected"
76+
console.log("Close", evt)
77+
})
78+
79+
ws.addEventListener("error", (evt) => {
80+
this.status = "Error (see console)"
81+
console.error("Error", evt)
82+
})
83+
84+
this.ws = ws
85+
},
86+
disconnect() {
87+
this.ws.close()
88+
this.ws = null
89+
this.messages = []
90+
},
91+
renderTimestamp(timestamp) {
92+
return new Date(timestamp).toLocaleTimeString()
93+
}
94+
}
95+
}
96+
97+
Vue.createApp(app).mount("#app")
98+
</script>
99+
</body>
100+
</html>

websocket-chat/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
uvicorn[standard]
2+
blacksheep

websocket-chat/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from app.main import app

0 commit comments

Comments
 (0)