用 Tornado 撸一个实时聊天应用:从原理到实战,搞定异步 Web 开发

部署运行你感兴趣的模型镜像

目录

引言:异步场景的“利器”

一、为什么 Tornado 适合做实时应用?先搞懂它的 “独门绝技”

二、环境搭建:3 步搞定 Tornado 开发环境(附版本选择)

步骤 1:确认 Python 版本

步骤 2:创建虚拟环境(避免依赖冲突)

步骤 3:安装 Tornado

三、5 行代码跑通第一个 Tornado 应用:感受异步的 “快”

步骤 1:写一个基础服务器

步骤 2:启动服务器,测试效果

四、核心概念拆解:用 “餐厅” 模型理解 Tornado 的异步逻辑

五、实战:开发实时聊天应用(从单聊到群聊,附完整代码)

阶段 1:实现基础 WebSocket 连接

步骤 1:编写 WebSocket 处理器

步骤 2:创建聊天页面模板

步骤 3:测试基础聊天功能

阶段 2:添加用户认证(昵称登录)

步骤 1:修改处理器,添加登录逻辑

步骤 2:创建登录页面模板

步骤 3:修改聊天页面,显示用户信息

步骤 4:测试登录和聊天功能

阶段 3:优化体验(在线用户实时更新 + 消息时间戳)

步骤 1:修改 WebSocket 处理器,推送用户列表

步骤 2:更新聊天页面,处理 JSON 消息

六、Tornado 异步进阶:用协程处理耗时操作

示例:用协程处理模拟的耗时任务

七、部署与优化:让你的聊天应用抗住高并发

部署注意事项:

高并发优化技巧:

八、常见问题与避坑指南(新手必看)

九、学习资源推荐:从入门到精通 Tornado

总结:Tornado 的适用场景与学习路径


 

class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

引言:异步场景的“利器”

如果你玩过在线游戏的实时聊天频道,或者用过股票行情的实时刷新功能,可能会好奇:“这些高并发、低延迟的实时应用,到底是用什么技术做的?”

今天要聊的 Tornado,就是这类场景的 “利器”。作为 Python 生态中少有的 “异步非阻塞” 原生框架,它能单线程处理成千上万的并发连接,尤其适合需要长连接、实时交互的场景。

但很多人学 Tornado 时会被 “异步”“协程” 这些概念吓退,觉得太复杂。其实不然 —— 今天这篇文章,我会用一个可运行的实时聊天应用作为案例,从基础到实战,全程用大白话解释原理,代码直接能跑,哪怕是刚接触异步编程的新手也能跟上。

一、为什么 Tornado 适合做实时应用?先搞懂它的 “独门绝技”

第一次用 Tornado 时,我最直观的感受是:它处理并发的思路和 Django、Flask 完全不一样

我们先回忆下传统 Web 框架(比如 Flask)的工作方式:假设服务器用 10 个线程处理请求,同一时间最多只能处理 10 个请求。如果其中一个请求要等待数据库返回(比如耗时 5 秒),这个线程就会 “卡着” 不动,其他请求只能排队 —— 这就是 “同步阻塞” 模型,效率很低。

而 Tornado 的 “异步非阻塞” 模型就像一个 “聪明的服务员”:他不需要守着一桌客人点完菜再服务下一桌,而是记下单子后去招呼其他客人,等厨房做好了再回来上菜。这种模式下,一个线程就能同时处理成百上千个连接,尤其适合:

  • 实时聊天(WebSocket 长连接);
  • 实时数据推送(如股票行情、监控数据);
  • 高并发 API 服务(如短信验证码接口)。

具体来说,Tornado 有三个核心优势:

  1. 原生异步支持:从底层就是为异步设计的,不用依赖额外库;
  2. 内置 WebSocket:完美支持长连接,不用像 Flask 那样装扩展;
  3. 单线程高并发:通过 IOLoop(事件循环)高效调度任务,资源占用少。

简单说:如果你的应用需要 “同时和很多用户保持对话”,选 Tornado 准没错

二、环境搭建:3 步搞定 Tornado 开发环境(附版本选择)

开始写代码前,先把环境搭好。Tornado 对 Python 版本有要求(至少 3.5+,推荐 3.7+),这里以 “Windows 10+Python 3.9” 为例,macOS 和 Linux 操作基本一致。

步骤 1:确认 Python 版本

打开 cmd(Windows)或终端(macOS/Linux),输入python --version,确保显示 “Python 3.7.x” 及以上。如果版本太低,去官网下载最新版(https://www.python.org/),安装时勾选 “Add Python to PATH”。

步骤 2:创建虚拟环境(避免依赖冲突)

和其他框架一样,建议用虚拟环境隔离项目依赖:

  1. 新建项目文件夹(比如D:\tornado_chat);
  2. 打开 cmd,cd 到该文件夹:cd D:\tornado_chat
  3. 创建虚拟环境:python -m venv venv
  4. 激活虚拟环境:
    • Windows:venv\Scripts\activate(激活后命令行前有(venv));
    • macOS/Linux:source venv/bin/activate

步骤 3:安装 Tornado

Tornado 的稳定版本是 6.x(截止 2024 年),直接用 pip 安装:pip install tornado==6.3.3

验证安装:输入python -c "import tornado; print(tornado.version)",显示 “6.3.3” 就说明成功了。

小提醒:如果后续代码中用到async/await(Python 3.5 + 的异步语法),确保 Python 版本不低于 3.5,否则会报错。

三、5 行代码跑通第一个 Tornado 应用:感受异步的 “快”

先从最简单的 “Hello World” 开始,感受 Tornado 的基本用法和异步特性。

步骤 1:写一个基础服务器

tornado_chat文件夹下新建app.py,内容如下:

import tornado.ioloop
import tornado.web

# 定义处理请求的Handler(类似Flask的视图函数)
class MainHandler(tornado.web.RequestHandler):
    def get(self):  # 处理GET请求
        self.write("Hello Tornado! 这是我的第一个异步应用~")

# 路由配置:URL路径对应哪个Handler
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),  # 访问根路径时,由MainHandler处理
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)  # 监听8888端口
    print("服务器启动,访问 http://127.0.0.1:8888")
    tornado.ioloop.IOLoop.current().start()  # 启动事件循环

步骤 2:启动服务器,测试效果

在 cmd 中运行python app.py,看到 “服务器启动,访问 http://127.0.0.1:8888” 后,打开浏览器访问该地址,会显示 “Hello Tornado! 这是我的第一个异步应用~”。

关键区别:和 Flask 不同,Tornado 启动后不会自动重启(修改代码需要手动重启),但你可以在启动时加autoreload=True参数实现热重载:

# 修改make_app和启动部分
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ], autoreload=True)  # 开发时开启自动重载

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("服务器启动,访问 http://127.0.0.1:8888")
    tornado.ioloop.IOLoop.current().start()

四、核心概念拆解:用 “餐厅” 模型理解 Tornado 的异步逻辑

很多人学 Tornado 卡在用 “同步思维” 理解 “异步逻辑”。我们用 “餐厅运营” 来类比,彻底搞懂它的工作原理:

  • IOLoop(事件循环):相当于餐厅的 “前台总调度”,负责接收所有客人(请求),并安排服务员处理;
  • Handler(处理器):相当于 “服务员”,负责处理具体的客人需求(比如点单、上菜);
  • 异步操作(如网络请求、数据库查询):相当于 “厨房做菜”,服务员不用盯着,做好了会通知前台;
  • 协程(coroutine):相当于 “服务员的工作流程”,遇到需要等待的任务(比如等菜),先去服务其他客人,等通知再回来。

举个具体例子:

  • 同步模式:1 个服务员服务 1 桌客人,点完菜就站在厨房门口等,期间其他客人没人管;
  • 异步模式:1 个服务员同时服务 10 桌客人,点完菜就记下来交给前台,然后去服务下一桌,厨房做好后前台通知服务员来上菜。

Tornado 的 “快”,本质就是通过 IOLoop 高效调度任务,让处理器(服务员)从不 “闲着”,把等待的时间用在处理其他请求上。

五、实战:开发实时聊天应用(从单聊到群聊,附完整代码)

我们来开发一个功能完整的实时聊天应用,包含:

  • 支持多用户同时连接;
  • 发送消息后所有人实时收到;
  • 显示在线用户列表;
  • 简单的用户认证(输入昵称登录)。

这个案例能完美体现 Tornado 处理 WebSocket 长连接的优势,全程分阶段实现,每一步都能看到效果。

阶段 1:实现基础 WebSocket 连接

WebSocket 是一种允许客户端和服务器 “持续对话” 的协议(不同于 HTTP 的 “一次请求一次响应”),Tornado 内置了对 WebSocket 的支持,不用额外装扩展。

步骤 1:编写 WebSocket 处理器

修改app.py,添加 WebSocket 相关代码:

import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.options import define, options, parse_command_line

# 存储所有连接的客户端(WebSocket对象)
connected_clients = set()

# HTTP请求处理器:返回聊天页面
class ChatIndexHandler(tornado.web.RequestHandler):
    def get(self):
        # 渲染聊天页面(后面会创建这个HTML文件)
        self.render("chat.html")

# WebSocket处理器:处理实时消息
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        """新客户端连接时调用"""
        print("新客户端连接")
        connected_clients.add(self)  # 把当前连接加入集合

    def on_message(self, message):
        """收到客户端消息时调用"""
        print(f"收到消息:{message}")
        # 把消息广播给所有连接的客户端
        for client in connected_clients:
            client.write_message(message)  # 发送消息给客户端

    def on_close(self):
        """客户端断开连接时调用"""
        print("客户端断开连接")
        connected_clients.remove(self)  # 从集合中移除

    def check_origin(self, origin):
        """允许跨域请求(开发时方便测试,生产环境需限制)"""
        return True

def make_app():
    return tornado.web.Application([
        (r"/", ChatIndexHandler),  # 聊天页面
        (r"/ws", ChatWebSocketHandler),  # WebSocket连接地址
    ], autoreload=True, template_path="templates")  # 指定模板文件夹

if __name__ == "__main__":
    parse_command_line()
    app = make_app()
    app.listen(8888)
    print("聊天服务器启动,访问 http://127.0.0.1:8888")
    tornado.ioloop.IOLoop.current().start()

代码说明:

  • connected_clients:用集合存储所有在线的 WebSocket 连接,方便广播消息;
  • ChatIndexHandler:处理 HTTP 请求,返回聊天页面;
  • ChatWebSocketHandler:继承自WebSocketHandler,重写三个核心方法:
    • open:客户端连接时触发(比如记录连接);
    • on_message:收到消息时触发(这里实现广播,发给所有在线用户);
    • on_close:客户端断开时触发(移除连接);
  • check_origin:开发时允许跨域,否则本地测试可能连不上。

步骤 2:创建聊天页面模板

在项目根目录下创建templates文件夹,然后新建chat.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Tornado实时聊天</title>
    <style>
        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        #messages {
            border: 1px solid #ccc;
            height: 400px;
            overflow-y: auto;
            margin-bottom: 10px;
            padding: 10px;
        }
        .message {
            margin: 5px 0;
            padding: 8px;
            background: #f0f0f0;
            border-radius: 4px;
        }
        #messageInput {
            width: 70%;
            padding: 8px;
        }
        #sendBtn {
            width: 25%;
            padding: 8px;
            background: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>实时聊天</h1>
        <div id="messages"></div>
        <input type="text" id="messageInput" placeholder="输入消息...">
        <button id="sendBtn">发送</button>
    </div>

    <script>
        // 连接WebSocket服务器(注意协议是ws,不是http)
        const ws = new WebSocket('ws://' + window.location.host + '/ws');

        // 收到服务器消息时显示
        ws.onmessage = function(event) {
            const messagesDiv = document.getElementById('messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message';
            messageDiv.textContent = event.data;
            messagesDiv.appendChild(messageDiv);
            // 滚动到最新消息
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        };

        // 发送消息
        document.getElementById('sendBtn').addEventListener('click', function() {
            const input = document.getElementById('messageInput');
            const message = input.value.trim();
            if (message) {
                ws.send(message);  // 发送到服务器
                input.value = '';  // 清空输入框
            }
        });

        // 按回车发送消息
        document.getElementById('messageInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                document.getElementById('sendBtn').click();
            }
        });
    </script>
</body>
</html>

页面逻辑:

  • 用 JavaScript 的WebSocket对象连接服务器的/ws地址;
  • 输入消息后,点击按钮或按回车发送到服务器;
  • 收到服务器广播的消息时,显示在消息列表中。

步骤 3:测试基础聊天功能

启动服务器python app.py,打开两个浏览器窗口(或隐私模式)访问http://127.0.0.1:8888,在一个窗口发送消息,另一个窗口会实时收到 —— 基础的群聊功能就实现了!

阶段 2:添加用户认证(昵称登录)

现在所有消息都是匿名的,我们给应用加个简单的登录页面,让用户输入昵称后才能进入聊天。

步骤 1:修改处理器,添加登录逻辑

更新app.py,新增登录相关的 Handler 和用户信息存储:

import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.options import define, options, parse_command_line

# 存储客户端连接和对应的用户昵称:{websocket对象: 昵称}
clients = {}  # 替代之前的connected_clients

# 登录页面处理器
class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("login.html")  # 显示登录页

    def post(self):
        # 获取表单提交的昵称
        nickname = self.get_argument("nickname", "").strip()
        if nickname and len(nickname) <= 10:  # 简单验证:不为空且长度<=10
            # 记录用户昵称到cookie(有效期1天)
            self.set_cookie("nickname", nickname, expires_days=1)
            self.redirect("/chat")  # 跳转到聊天页
        else:
            self.render("login.html", error="昵称不能为空且长度不超过10字")

# 聊天页面处理器(需要登录才能访问)
class ChatIndexHandler(tornado.web.RequestHandler):
    def get(self):
        nickname = self.get_cookie("nickname")
        if not nickname:
            self.redirect("/")  # 没登录就跳转到登录页
        self.render("chat.html", nickname=nickname)

# WebSocket处理器(关联用户昵称)
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        # 从cookie获取用户昵称
        nickname = self.get_cookie("nickname")
        if not nickname:
            self.close(code=1008, reason="未登录")  # 没登录就关闭连接
            return
        # 存储连接和昵称
        clients[self] = nickname
        # 广播“xxx加入聊天”
        self.broadcast(f"系统消息:{nickname}加入了聊天")

    def on_message(self, message):
        nickname = clients.get(self)
        if nickname and message:
            # 发送带昵称的消息
            self.broadcast(f"{nickname}: {message}")

    def on_close(self):
        if self in clients:
            nickname = clients[self]
            del clients[self]  # 移除连接
            self.broadcast(f"系统消息:{nickname}离开了聊天")

    def broadcast(self, message):
        """广播消息给所有在线用户"""
        for client in clients:
            try:
                client.write_message(message)
            except:
                # 处理可能的连接错误
                print("发送消息失败,可能客户端已断开")

    def check_origin(self, origin):
        return True

def make_app():
    return tornado.web.Application([
        (r"/", LoginHandler),  # 登录页
        (r"/chat", ChatIndexHandler),  # 聊天页
        (r"/ws", ChatWebSocketHandler),  # WebSocket连接
    ], autoreload=True, template_path="templates")

if __name__ == "__main__":
    parse_command_line()
    app = make_app()
    app.listen(8888)
    print("聊天服务器启动,访问 http://127.0.0.1:8888")
    tornado.ioloop.IOLoop.current().start()

核心变化:

  • clients字典关联 WebSocket 连接和用户昵称,替代之前的集合;
  • 新增LoginHandler:处理登录请求,验证昵称并写入 cookie;
  • ChatIndexHandler:检查 cookie,未登录则跳转到登录页;
  • WebSocket 处理器中,消息会带上用户昵称,用户加入 / 离开时广播系统消息。

步骤 2:创建登录页面模板

templates文件夹下新建login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录 - Tornado聊天</title>
    <style>
        .login-box {
            max-width: 300px;
            margin: 100px auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        .error {
            color: red;
            margin-bottom: 10px;
        }
        input[type="text"] {
            width: 100%;
            padding: 8px;
            margin: 10px 0;
            box-sizing: border-box;
        }
        button {
            width: 100%;
            padding: 8px;
            background: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="login-box">
        <h2>请输入昵称登录</h2>
        {% if error %}
        <div class="error">{{ error }}</div>
        {% endif %}
        <form method="post">
            <input type="text" name="nickname" placeholder="你的昵称" required>
            <button type="submit">进入聊天</button>
        </form>
    </div>
</body>
</html>

步骤 3:修改聊天页面,显示用户信息

更新chat.html,添加在线用户列表(简化版):

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Tornado实时聊天</title>
    <style>
        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            display: flex;
            gap: 20px;
        }
        .chat-area {
            flex: 3;
        }
        .user-list {
            flex: 1;
            border: 1px solid #ccc;
            padding: 10px;
        }
        #messages {
            border: 1px solid #ccc;
            height: 400px;
            overflow-y: auto;
            margin-bottom: 10px;
            padding: 10px;
        }
        .message {
            margin: 5px 0;
            padding: 8px;
            border-radius: 4px;
        }
        .system {
            background: #e3f2fd;
            color: #0d47a1;
        }
        .user {
            background: #f0f0f0;
        }
        #messageInput {
            width: 70%;
            padding: 8px;
        }
        #sendBtn {
            width: 25%;
            padding: 8px;
            background: #4CAF50;
            color: white;
            border: none;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="chat-area">
            <h1>实时聊天</h1>
            <div id="messages"></div>
            <input type="text" id="messageInput" placeholder="输入消息...">
            <button id="sendBtn">发送</button>
        </div>
        <div class="user-list">
            <h3>在线用户</h3>
            <ul id="users"></ul>
        </div>
    </div>

    <script>
        const nickname = "{{ nickname }}";  // 从模板获取当前用户昵称
        const ws = new WebSocket('ws://' + window.location.host + '/ws');

        // 收到消息时处理
        ws.onmessage = function(event) {
            const message = event.data;
            const messagesDiv = document.getElementById('messages');
            const messageDiv = document.createElement('div');
            
            // 区分系统消息和用户消息(加不同样式)
            if (message.startsWith("系统消息:")) {
                messageDiv.className = 'message system';
                // 系统消息可能是用户加入/离开,需要刷新在线列表
                updateUserList();
            } else {
                messageDiv.className = 'message user';
            }
            
            messageDiv.textContent = message;
            messagesDiv.appendChild(messageDiv);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        };

        // 发送消息
        document.getElementById('sendBtn').addEventListener('click', sendMessage);
        document.getElementById('messageInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') sendMessage();
        });

        function sendMessage() {
            const input = document.getElementById('messageInput');
            const message = input.value.trim();
            if (message) {
                ws.send(message);
                input.value = '';
            }
        }

        // 简单模拟更新在线用户列表(实际项目需要服务器推送用户列表)
        function updateUserList() {
            // 这里只是示例,真实场景中应该由服务器发送在线用户数据
            const usersList = document.getElementById('users');
            usersList.innerHTML = '';  // 清空现有列表
            // 临时逻辑:假设当前用户一定在线(实际需从服务器获取)
            const li = document.createElement('li');
            li.textContent = nickname;
            usersList.appendChild(li);
        }
    </script>
</body>
</html>

页面变化:

  • 分左右两栏:左侧聊天区,右侧在线用户列表;
  • 系统消息和用户消息用不同样式区分;
  • 用户加入 / 离开时触发updateUserList(简化版,实际需服务器推送完整列表)。

步骤 4:测试登录和聊天功能

启动服务器后,访问http://127.0.0.1:8888,会先显示登录页,输入昵称后进入聊天。用两个不同的昵称登录(比如 “张三” 和 “李四”),发送消息会显示昵称,加入 / 离开时会有系统提示,基本的带用户身份的群聊就实现了。

阶段 3:优化体验(在线用户实时更新 + 消息时间戳)

现在的用户列表只是模拟的,我们让服务器实时推送在线用户列表,并给每条消息加上时间戳,让体验更完善。

步骤 1:修改 WebSocket 处理器,推送用户列表

更新app.pyChatWebSocketHandler

import time  # 新增:用于时间戳

class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        nickname = self.get_cookie("nickname")
        if not nickname:
            self.close(code=1008, reason="未登录")
            return
        clients[self] = nickname
        # 广播加入消息
        self.broadcast({
            "type": "system",
            "message": f"{nickname}加入了聊天",
            "time": self.get_timestamp()
        })
        # 推送最新在线用户列表
        self.send_user_list()

    def on_message(self, message):
        nickname = clients.get(self)
        if nickname and message:
            # 发送带类型、时间戳的用户消息
            self.broadcast({
                "type": "user",
                "nickname": nickname,
                "message": message,
                "time": self.get_timestamp()
            })

    def on_close(self):
        if self in clients:
            nickname = clients[self]
            del clients[self]
            self.broadcast({
                "type": "system",
                "message": f"{nickname}离开了聊天",
                "time": self.get_timestamp()
            })
            self.send_user_list()  # 离开后更新用户列表

    def broadcast(self, data):
        """广播JSON格式的数据(支持更多信息)"""
        import json  # 用JSON序列化数据
        message = json.dumps(data)
        for client in clients:
            try:
                client.write_message(message)
            except:
                print("发送消息失败")

    def send_user_list(self):
        """推送在线用户列表给所有客户端"""
        import json
        user_list = list(clients.values())  # 提取所有昵称
        data = {
            "type": "user_list",
            "users": user_list
        }
        message = json.dumps(data)
        for client in clients:
            try:
                client.write_message(message)
            except:
                print("发送用户列表失败")

    def get_timestamp(self):
        """获取格式化的时间戳(如:2024-05-20 15:30:22)"""
        return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())

    # check_origin不变
    def check_origin(self, origin):
        return True

核心优化:

  • 消息用 JSON 格式发送,包含type(系统消息 / 用户消息 / 用户列表)、time(时间戳)等字段;
  • 新增send_user_list方法:当有用户加入 / 离开时,向所有客户端推送最新在线用户列表;
  • get_timestamp方法:生成人类可读的时间戳,附在每条消息上。

步骤 2:更新聊天页面,处理 JSON 消息

修改chat.html的 JavaScript 部分,解析 JSON 格式的消息:

<!-- 省略样式部分,保持不变 -->
<script>
    const nickname = "{{ nickname }}";
    const ws = new WebSocket('ws://' + window.location.host + '/ws');

    ws.onmessage = function(event) {
        // 解析JSON格式的消息
        const data = JSON.parse(event.data);
        
        switch(data.type) {
            case "system":
            case "user":
                // 显示系统消息或用户消息
                showMessage(data);
                break;
            case "user_list":
                // 更新在线用户列表
                updateUserList(data.users);
                break;
        }
    };

    // 显示消息
    function showMessage(data) {
        const messagesDiv = document.getElementById('messages');
        const messageDiv = document.createElement('div');
        messageDiv.className = `message ${data.type}`;
        
        // 格式化消息:[时间] 内容(用户消息显示昵称)
        let content = `[${data.time}] `;
        if (data.type === "user") {
            content += `${data.nickname}: ${data.message}`;
        } else {
            content += data.message;
        }
        
        messageDiv.textContent = content;
        messagesDiv.appendChild(messageDiv);
        messagesDiv.scrollTop = messagesDiv.scrollHeight;
    }

    // 更新在线用户列表
    function updateUserList(users) {
        const usersList = document.getElementById('users');
        usersList.innerHTML = '';
        users.forEach(user => {
            const li = document.createElement('li');
            // 当前用户的昵称标红
            if (user === nickname) {
                li.style.color = 'red';
                li.textContent = `${user}(你)`;
            } else {
                li.textContent = user;
            }
            usersList.appendChild(li);
        });
    }

    // 发送消息函数不变
    function sendMessage() {
        const input = document.getElementById('messageInput');
        const message = input.value.trim();
        if (message) {
            ws.send(message);
            input.value = '';
        }
    }

    document.getElementById('sendBtn').addEventListener('click', sendMessage);
    document.getElementById('messageInput').addEventListener('keypress', function(e) {
        if (e.key === 'Enter') sendMessage();
    });
</script>

现在测试:

  • 新用户加入,所有人能看到带时间戳的系统消息,在线列表实时更新;
  • 发送消息会显示 “[时间] 昵称:内容”;
  • 自己的昵称在列表中会标红显示 “(你)”,体验更友好。

六、Tornado 异步进阶:用协程处理耗时操作

Tornado 的核心优势是异步处理,如果你的应用中有耗时操作(比如数据库查询、网络请求),一定要用协程(async/await)避免阻塞 IOLoop。

示例:用协程处理模拟的耗时任务

app.py中新增一个演示异步处理的 Handler:

import asyncio  # 用于模拟异步耗时操作

class AsyncDemoHandler(tornado.web.RequestHandler):
    async def get(self):
        """异步处理示例:同时执行两个耗时任务"""
        self.write("开始处理任务...<br>")
        
        # 用asyncio.gather同时运行两个异步任务(并行执行)
        result1, result2 = await asyncio.gather(
            self.async_task(1, 2),  # 任务1:耗时2秒
            self.async_task(2, 3)   # 任务2:耗时3秒
        )
        
        self.write(f"任务1结果:{result1}<br>")
        self.write(f"任务2结果:{result2}<br>")
        self.write("所有任务处理完成!")

    async def async_task(self, task_id, seconds):
        """模拟异步耗时任务:等待指定秒数后返回结果"""
        await asyncio.sleep(seconds)  # 异步等待(不阻塞IOLoop)
        return f"任务{task_id}完成(耗时{seconds}秒)"

# 在make_app的路由中添加
def make_app():
    return tornado.web.Application([
        (r"/", LoginHandler),
        (r"/chat", ChatIndexHandler),
        (r"/ws", ChatWebSocketHandler),
        (r"/async-demo", AsyncDemoHandler),  # 新增异步演示路由
    ], autoreload=True, template_path="templates")

访问http://127.0.0.1:8888/async-demo,会看到:

  • 页面先显示 “开始处理任务...”;
  • 等待 3 秒(而不是 2+3=5 秒)后,同时显示两个任务的结果 —— 因为两个任务是并行执行的。

关键区别:如果用同步代码(time.sleep),两个任务会串行执行(总耗时 5 秒),期间 IOLoop 被阻塞,其他请求无法处理;而用asyncio.sleep(异步等待),IOLoop 在等待时可以处理其他请求,效率极大提升。

七、部署与优化:让你的聊天应用抗住高并发

开发完成后,需要考虑部署和优化,让应用能在生产环境稳定运行。

部署注意事项:

  1. 关闭自动重载:生产环境中autoreload=True会消耗额外资源,必须关闭;
  2. 设置适当的进程数:Tornado 是单线程的,可以启动多个进程(通常等于 CPU 核心数)充分利用多核:
# 生产环境启动方式(替换main函数)
if __name__ == "__main__":
    import tornado.httpserver
    app = make_app()
    server = tornado.httpserver.HTTPServer(app)
    server.bind(8888)  # 绑定端口
    server.start(4)  # 启动4个进程(根据CPU核心数调整)
    print("生产环境服务器启动,端口8888")
    tornado.ioloop.IOLoop.current().start()
  1. 使用 Nginx 作为反向代理:处理静态文件、负载均衡、SSL 终止(HTTPS),配置示例:
server {
    listen 80;
    server_name yourdomain.com;  # 你的域名

    location / {
        proxy_pass http://127.0.0.1:8888;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";  # 支持WebSocket
        proxy_set_header Host $host;
    }

    # 静态文件由Nginx直接处理(如果有)
    location /static/ {
        alias /path/to/your/static/files/;
    }
}

高并发优化技巧:

  1. 限制单 IP 连接数:防止恶意攻击占用过多连接:
class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        client_ip = self.request.remote_ip
        # 简单限制:同一IP最多10个连接
        ip_connections = [c for c in clients if c.request.remote_ip == client_ip]
        if len(ip_connections) > 10:
            self.close(code=1008, reason="连接数过多")
            return
        # 其他逻辑...
  1. 消息队列缓冲:当并发消息量大时,用队列异步处理广播,避免阻塞:
from tornado.queues import Queue

message_queue = Queue()

async def message_worker():
    """异步消费消息队列,处理广播"""
    while True:
        message = await message_queue.get()
        for client in clients:
            try:
                client.write_message(message)
            except:
                pass
        message_queue.task_done()

# 在服务器启动时启动工作线程
if __name__ == "__main__":
    # ... 其他代码
    tornado.ioloop.IOLoop.current().spawn_callback(message_worker)
    tornado.ioloop.IOLoop.current().start()
  1. 定期清理无效连接:对长时间无活动的连接进行清理,释放资源。

八、常见问题与避坑指南(新手必看)

  1. WebSocket 连接失败,报 403 错误原因:跨域限制。解决方法:重写check_origin方法,生产环境中指定允许的域名(不要直接返回True)。

  2. 异步函数没加await,导致不执行比如调用async_task(1, 2)时忘记加await,函数会变成普通的协程对象,不会实际执行。记住:调用异步函数必须加await

  3. 使用同步库(如requests)导致阻塞Tornado 的异步是 “非阻塞” 的,但如果在协程中调用同步阻塞的库(如requests.get),会阻塞整个 IOLoop。解决方法:用异步库替代,比如用aiohttp替代requests

  4. 开发时修改代码不生效原因:没开启autoreload=True,或修改了IOLoop相关代码(自动重载对这类代码支持不好)。解决方法:手动重启服务器。

  5. 部署后 WebSocket 连接频繁断开可能是 Nginx 配置中缺少 WebSocket 支持,确保添加了proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";

九、学习资源推荐:从入门到精通 Tornado

想深入学习 Tornado,这些资源亲测有用:

  • 官方文档https://www.tornadoweb.org/ (最权威,示例简洁,必看);
  • 《Tornado Web 框架实战》:李辉著,适合新手入门,案例实用;
  • Tornado 源码:Tornado 的源码简洁易懂,尤其是ioloop.pywebsocket.py,读源码能加深对异步的理解;
  • 实战项目:GitHub 搜索 “tornado chat”,看别人的聊天应用实现,学习代码组织和优化技巧。

总结:Tornado 的适用场景与学习路径

回顾整个开发过程,我们从基础的 WebSocket 连接,到带用户认证、实时列表的聊天应用,再到异步协程的优化,一步步体验了 Tornado 处理实时场景的优势。

Tornado 不是 “万能框架”,它的强项在高并发实时应用,如果你的项目是简单的 CRUD(增删改查)网站,用 Django 或 Flask 可能更高效;但如果需要处理大量长连接、实时数据交互,Tornado 会是更好的选择。

对于新手,我的学习建议是:

  1. 先掌握基础用法(Handler、路由、模板),跑通一个简单的 HTTP 应用;
  2. 学习 WebSocket,实现一个简单的实时功能(如聊天、计数器);
  3. 理解IOLoop和协程的工作原理,用async/await处理异步任务;
  4. 最后学习部署和优化,理解生产环境的注意事项。

Tornado 的异步编程思维需要一点时间适应,但一旦掌握,你会发现处理高并发场景变得非常轻松。就像我们开发的聊天应用,即使同时有上千人在线,也能保持流畅的实时交互 —— 这就是异步非阻塞的魅力。

祝你在 Tornado 的学习路上,写出既高效又稳定的实时应用!

 

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值