0
# Event System
1
2
Comprehensive event-driven API covering all WebSocket operations including handshake, messages, control frames, and connection lifecycle. The event system provides a type-safe, structured approach to WebSocket protocol handling.
3
4
## Capabilities
5
6
### Base Event Class
7
8
All wsproto events inherit from the base Event class, providing a common interface for protocol operations.
9
10
```python { .api }
11
class Event(ABC):
12
"""
13
Base class for wsproto events.
14
"""
15
pass
16
```
17
18
### Handshake Events
19
20
Events related to WebSocket connection establishment and rejection.
21
22
```python { .api }
23
@dataclass(frozen=True)
24
class Request(Event):
25
"""
26
The beginning of a WebSocket connection, the HTTP Upgrade request.
27
28
This event is fired when a SERVER connection receives a WebSocket
29
handshake request (HTTP with upgrade header).
30
"""
31
host: str # Required hostname or host header value
32
target: str # Required request target (path and query string)
33
extensions: Union[Sequence[Extension], Sequence[str]] = field(default_factory=list) # Proposed extensions
34
extra_headers: Headers = field(default_factory=list) # Additional request headers
35
subprotocols: List[str] = field(default_factory=list) # Proposed subprotocols list
36
37
@dataclass(frozen=True)
38
class AcceptConnection(Event):
39
"""
40
The acceptance of a WebSocket upgrade request.
41
42
This event is fired when a CLIENT receives an acceptance response
43
from a server. It is also used to accept an upgrade request when
44
acting as a SERVER.
45
"""
46
subprotocol: Optional[str] = None # The accepted subprotocol to use
47
extensions: List[Extension] = field(default_factory=list) # Accepted extensions
48
extra_headers: Headers = field(default_factory=list) # Additional response headers
49
50
@dataclass(frozen=True)
51
class RejectConnection(Event):
52
"""
53
The rejection of a WebSocket upgrade request, the HTTP response.
54
55
The RejectConnection event sends appropriate HTTP headers to communicate
56
that the handshake has been rejected. You may send an HTTP body by setting
57
has_body to True and following with RejectData events.
58
"""
59
status_code: int = 400 # HTTP response status code
60
headers: Headers = field(default_factory=list) # Response headers
61
has_body: bool = False # Whether response has body (see RejectData)
62
63
@dataclass(frozen=True)
64
class RejectData(Event):
65
"""
66
The rejection HTTP response body.
67
68
The caller may send multiple RejectData events. The final event should
69
have the body_finished attribute set to True.
70
"""
71
data: bytes # Required raw body data
72
body_finished: bool = True # True if this is the final chunk of body data
73
```
74
75
### Message Events
76
77
Events for WebSocket data messages, supporting both text and binary data with fragmentation control.
78
79
```python { .api }
80
@dataclass(frozen=True)
81
class Message(Event, Generic[T]):
82
"""
83
The WebSocket data message base class.
84
"""
85
data: T # Required message data (str for text, bytes for binary)
86
frame_finished: bool = True # Whether frame is finished (for fragmentation)
87
message_finished: bool = True # True if this is the last frame of message
88
89
@dataclass(frozen=True)
90
class TextMessage(Message[str]):
91
"""
92
This event is fired when a data frame with TEXT payload is received.
93
94
The data represents a single chunk and may not be a complete WebSocket
95
message. You need to buffer and reassemble chunks using message_finished
96
to get the full message.
97
"""
98
data: str # Text message data as string
99
100
@dataclass(frozen=True)
101
class BytesMessage(Message[bytes]):
102
"""
103
This event is fired when a data frame with BINARY payload is received.
104
105
The data represents a single chunk and may not be a complete WebSocket
106
message. You need to buffer and reassemble chunks using message_finished
107
to get the full message.
108
"""
109
data: bytes # Binary message data as bytes
110
```
111
112
### Control Frame Events
113
114
Events for WebSocket control frames including connection close, ping, and pong.
115
116
```python { .api }
117
@dataclass(frozen=True)
118
class CloseConnection(Event):
119
"""
120
The end of a WebSocket connection, represents a closure frame.
121
122
wsproto does not automatically send a response to a close event. To comply
123
with the RFC you MUST send a close event back to the remote WebSocket if
124
you have not already sent one.
125
"""
126
code: int # Required integer close code indicating why connection closed
127
reason: Optional[str] = None # Optional additional reasoning for closure
128
129
def response(self) -> "CloseConnection":
130
"""
131
Generate an RFC-compliant close frame to send back to the peer.
132
133
Returns:
134
CloseConnection event with same code and reason
135
"""
136
137
@dataclass(frozen=True)
138
class Ping(Event):
139
"""
140
The Ping event can be sent to trigger a ping frame and is fired when received.
141
142
wsproto does not automatically send a pong response to a ping event. To comply
143
with the RFC you MUST send a pong event as soon as practical.
144
"""
145
payload: bytes = b"" # Optional payload to emit with the ping frame
146
147
def response(self) -> "Pong":
148
"""
149
Generate an RFC-compliant Pong response to this ping.
150
151
Returns:
152
Pong event with same payload
153
"""
154
155
@dataclass(frozen=True)
156
class Pong(Event):
157
"""
158
The Pong event is fired when a Pong is received.
159
"""
160
payload: bytes = b"" # Optional payload from the pong frame
161
```
162
163
## Usage Examples
164
165
### Handling Handshake Events
166
167
```python
168
from wsproto import WSConnection, ConnectionType
169
from wsproto.events import Request, AcceptConnection, RejectConnection
170
171
# Server handling handshake
172
ws = WSConnection(ConnectionType.SERVER)
173
ws.receive_data(handshake_data)
174
175
for event in ws.events():
176
if isinstance(event, Request):
177
print(f"WebSocket request for {event.target} from {event.host}")
178
print(f"Subprotocols: {event.subprotocols}")
179
print(f"Extensions: {event.extensions}")
180
181
# Accept the connection
182
if event.target == '/chat':
183
accept_data = ws.send(AcceptConnection(
184
subprotocol='chat.v1' if 'chat.v1' in event.subprotocols else None
185
))
186
else:
187
# Reject the connection
188
reject_data = ws.send(RejectConnection(
189
status_code=404,
190
headers=[(b'content-type', b'text/plain')],
191
has_body=True
192
))
193
reject_body = ws.send(RejectData(
194
data=b'Path not found',
195
body_finished=True
196
))
197
```
198
199
### Handling Message Events
200
201
```python
202
from wsproto.events import TextMessage, BytesMessage
203
import json
204
205
# Process different message types
206
for event in ws.events():
207
if isinstance(event, TextMessage):
208
print(f"Received text: {event.data}")
209
210
# Handle JSON messages
211
try:
212
json_data = json.loads(event.data)
213
print(f"JSON payload: {json_data}")
214
except json.JSONDecodeError:
215
print("Not valid JSON")
216
217
# Handle fragmented messages
218
if not event.message_finished:
219
# Buffer this chunk and wait for more
220
text_buffer += event.data
221
else:
222
# Complete message received
223
complete_message = text_buffer + event.data
224
text_buffer = ""
225
226
elif isinstance(event, BytesMessage):
227
print(f"Received {len(event.data)} bytes")
228
229
# Handle binary data
230
if event.data.startswith(b'\x89PNG'):
231
print("Received PNG image")
232
233
# Handle fragmented binary messages
234
if not event.message_finished:
235
binary_buffer += event.data
236
else:
237
complete_binary = binary_buffer + event.data
238
binary_buffer = b""
239
```
240
241
### Handling Control Frame Events
242
243
```python
244
from wsproto.events import Ping, Pong, CloseConnection
245
from wsproto.frame_protocol import CloseReason
246
247
# Handle control frames
248
for event in ws.events():
249
if isinstance(event, Ping):
250
print(f"Received ping with payload: {event.payload}")
251
# Must respond with pong
252
pong_data = ws.send(event.response())
253
socket.send(pong_data)
254
255
elif isinstance(event, Pong):
256
print(f"Received pong with payload: {event.payload}")
257
# Handle pong response (e.g., measure round-trip time)
258
259
elif isinstance(event, CloseConnection):
260
print(f"Connection closing: code={event.code}, reason={event.reason}")
261
262
# Must respond to close frame
263
if ws.state != ConnectionState.LOCAL_CLOSING:
264
close_response = ws.send(event.response())
265
socket.send(close_response)
266
267
# Handle different close codes
268
if event.code == CloseReason.NORMAL_CLOSURE:
269
print("Normal closure")
270
elif event.code == CloseReason.GOING_AWAY:
271
print("Server going away")
272
elif event.code == CloseReason.PROTOCOL_ERROR:
273
print("Protocol error occurred")
274
```
275
276
### Sending Events
277
278
```python
279
from wsproto.events import TextMessage, BytesMessage, CloseConnection, Ping
280
281
# Send text message
282
text_data = ws.send(TextMessage(data="Hello, WebSocket!"))
283
socket.send(text_data)
284
285
# Send binary message
286
binary_data = ws.send(BytesMessage(data=b"Binary payload"))
287
socket.send(binary_data)
288
289
# Send fragmented message
290
fragment1 = ws.send(TextMessage(data="Start of ", message_finished=False))
291
fragment2 = ws.send(TextMessage(data="long message", message_finished=True))
292
socket.send(fragment1 + fragment2)
293
294
# Send ping
295
ping_data = ws.send(Ping(payload=b"ping-payload"))
296
socket.send(ping_data)
297
298
# Close connection
299
close_data = ws.send(CloseConnection(code=1000, reason="Goodbye"))
300
socket.send(close_data)
301
```
302
303
### Event-Driven WebSocket Echo Server
304
305
```python
306
import socket
307
from wsproto import WSConnection, ConnectionType
308
from wsproto.events import (
309
Request, AcceptConnection, TextMessage, BytesMessage,
310
CloseConnection, Ping
311
)
312
313
def handle_client(client_socket):
314
ws = WSConnection(ConnectionType.SERVER)
315
316
while True:
317
try:
318
data = client_socket.recv(4096)
319
if not data:
320
break
321
322
ws.receive_data(data)
323
324
for event in ws.events():
325
if isinstance(event, Request):
326
# Accept all connections
327
response = ws.send(AcceptConnection())
328
client_socket.send(response)
329
330
elif isinstance(event, TextMessage):
331
# Echo text messages
332
echo_data = ws.send(TextMessage(data=f"Echo: {event.data}"))
333
client_socket.send(echo_data)
334
335
elif isinstance(event, BytesMessage):
336
# Echo binary messages
337
echo_data = ws.send(BytesMessage(data=b"Echo: " + event.data))
338
client_socket.send(echo_data)
339
340
elif isinstance(event, Ping):
341
# Respond to pings
342
pong_data = ws.send(event.response())
343
client_socket.send(pong_data)
344
345
elif isinstance(event, CloseConnection):
346
# Respond to close
347
close_data = ws.send(event.response())
348
client_socket.send(close_data)
349
return
350
351
except Exception as e:
352
print(f"Error: {e}")
353
break
354
355
client_socket.close()
356
357
# Server setup
358
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
359
server.bind(('localhost', 8765))
360
server.listen(1)
361
362
while True:
363
client, addr = server.accept()
364
handle_client(client)
365
```