WebSocket
WebSocket is a protocol for full-duplex communication over a single TCP connection. A client initiates an HTTP Upgrade handshake (101 Switching Protocols), and after that both sides can send messages at any time without re-establishing the connection. Tornado has native WebSocket support, making it well-suited for building chat rooms, real-time data feeds, multi-user collaboration, and similar applications.
This article introduces the core interface of Tornado’s WebSocketHandler and demonstrates its use through two complete examples: a basic echo connection and a multi-user chat room.
WebSocketHandler Core Interface
Inherit from tornado.websocket.WebSocketHandler and override the following methods to handle WebSocket connections:
| Method | When it fires |
|---|---|
open(*args, **kwargs) | WebSocket handshake succeeds; connection is established |
on_message(message) | A message is received from the client (text is str, binary is bytes) |
on_close() | The connection is closed (can be triggered by either client or server) |
on_ping(data) | A Ping frame is received from the client (optional override) |
on_pong(data) | A Pong frame is received from the client (optional override) |
Sending messages and closing the connection:
| Method | Description |
|---|---|
write_message(message, binary=False) | Send a message to the client (async; use await) |
close(code=None, reason=None) | Actively close the connection |
ping(data) | Send a Ping frame |
Other commonly used attributes:
self.close_code/self.close_reason: Status code and reason when the connection closesself.request: The HTTP request object from the handshake (you can read Headers, Cookies, etc.)
Cross-Origin Handling
When making a WebSocket handshake, browsers include an Origin header. By default, Tornado only allows requests from the same domain. To allow cross-origin connections, override check_origin():
class MyWebSocketHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True # Allow all origins (development environment only)
# In production, check against an origin whitelistBasic Example
Server:
import tornado.ioloop
import tornado.web
import tornado.websocket
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
async def open(self):
print(f"New connection: {self.request.remote_ip}")
await self.write_message("Welcome to the Echo service!")
async def on_message(self, message):
print(f"Received: {message}")
await self.write_message(f"You sent: {message}")
def on_close(self):
print(f"Connection closed, code={self.close_code}")
def make_app():
return tornado.web.Application([
(r"/ws", EchoWebSocket),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()Client (browser JavaScript):
const ws = new WebSocket("ws://localhost:8888/ws");
ws.onopen = () => {
console.log("Connected");
ws.send("Hello, Tornado!");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};
ws.onerror = (error) => {
console.error("Connection error:", error);
};
ws.onclose = (event) => {
console.log(`Connection closed, code=${event.code}`);
};Chat Room Example
The core of a chat room is maintaining a global set of online users and broadcasting each new message to all of them.
Server
import tornado.ioloop
import tornado.web
import tornado.websocket
class ChatHandler(tornado.websocket.WebSocketHandler):
users = set() # All active connections
def check_origin(self, origin):
return True
def open(self):
ChatHandler.users.add(self)
self.nickname = self.get_argument("name", f"User{id(self) % 10000}")
self.broadcast(f"[System] {self.nickname} joined the chat room")
async def on_message(self, message):
self.broadcast(f"[{self.nickname}] {message}")
def on_close(self):
ChatHandler.users.discard(self)
self.broadcast(f"[System] {self.nickname} left the chat room")
def broadcast(self, message):
for user in ChatHandler.users:
try:
user.write_message(message)
except tornado.websocket.WebSocketClosedError:
ChatHandler.users.discard(user)
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render("chat.html")
def make_app():
return tornado.web.Application(
[
(r"/", IndexHandler),
(r"/ws", ChatHandler),
],
template_path="templates",
debug=True,
)
if __name__ == "__main__":
app = make_app()
app.listen(8888)
print("Chat room started: http://localhost:8888")
tornado.ioloop.IOLoop.current().start()Client Page templates/chat.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado Chat Room</title>
<style>
#messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 8px; }
#input-row { display: flex; gap: 8px; margin-top: 8px; }
#msg-input { flex: 1; }
</style>
</head>
<body>
<h2>Tornado Chat Room</h2>
<div id="messages"></div>
<div id="input-row">
<input id="msg-input" type="text" placeholder="Type a message..." autofocus>
<button onclick="sendMessage()">Send</button>
</div>
<script>
const name = prompt("Enter your nickname:") || "Anonymous";
const ws = new WebSocket(`ws://${location.host}/ws?name=${encodeURIComponent(name)}`);
const messages = document.getElementById("messages");
const input = document.getElementById("msg-input");
ws.onmessage = (event) => {
const div = document.createElement("div");
div.textContent = event.data;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
};
ws.onclose = () => {
const div = document.createElement("div");
div.textContent = "[Connection closed]";
div.style.color = "gray";
messages.appendChild(div);
};
function sendMessage() {
const text = input.value.trim();
if (text && ws.readyState === WebSocket.OPEN) {
ws.send(text);
input.value = "";
}
}
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") sendMessage();
});
</script>
</body>
</html>Heartbeat / Keep-Alive
When no data is transmitted for an extended period, intermediate network devices (proxies, NAT) may drop the connection. Use the Ping/Pong mechanism to keep the connection alive:
import asyncio
class ChatHandler(tornado.websocket.WebSocketHandler):
HEARTBEAT_INTERVAL = 30 # seconds
def open(self):
self._heartbeat = asyncio.get_event_loop().call_later(
self.HEARTBEAT_INTERVAL, self._send_ping
)
def _send_ping(self):
try:
self.ping(b"ping")
self._heartbeat = asyncio.get_event_loop().call_later(
self.HEARTBEAT_INTERVAL, self._send_ping
)
except tornado.websocket.WebSocketClosedError:
pass
def on_close(self):
if hasattr(self, "_heartbeat"):
self._heartbeat.cancel()write_message() is a coroutine. In high-concurrency broadcast scenarios, use asyncio.gather() to send messages in parallel rather than awaiting each one serially, to avoid blocking.