技术人生,  网络编程

使用Tornado WebSocket实现多人网页版聊天程序

Tornado Websocket可以实现http服务器主动向客户端发送消息,进而实现多人聊天程序。

引入所需的包和库。包含Tornado http服务器和websocket服务器相关包。time库用于用户发送消息时间的获取,os库用于加载和写入用户聊天记录的文件操作。

from tornado import ioloop
from tornado.web import Application,RequestHandler
from tornado.websocket import WebSocketHandler
from tornado.httpserver import HTTPServer
import time
import os

搭建Tornado服务器application框架并写入路由。分为首页http处理和后台websocket处理。

app = Application([(r"/", index_handler),
                    (r"/ws", ws_handler)],
                static_path="statics",
                template_path="templates")

http_server = HTTPServer(app)
http_server.listen(8888)
ioloop.IOLoop.current().start()

首先定义首页http处理handler。当用户访问首页时返回登录页面html,要求用户输入用户名登录,用户点击登录按钮后向服务器post登录信息,这里检测用户名输入是否为空,判断该用户是否已经登录,若出现上述情况则返回相应的错误弹窗,若验证通过则允许用户进入聊天室页面。

class index_handler(RequestHandler):
    def get(self):
        self.render('login.html')

    def post(self):
        account = self.get_argument('account')
        if not account:
            self.render('error.html', error_alert='用户名不能为空')
            return
        if account in ws_handler.user_dict.values():
            self.render('error.html', error_alert='该用户已登录')
            return
        self.render('chat.html', account=account)

登录页面html如下。仅有一个输入框供用户输入用户名。点击登录按钮后提交。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>十里琅居聊天室</title>
    </head>
    <body background="/static/bg.jpg" style="background-size:100% 100%; background-repeat: no-repeat; background-attachment:fixed;">
        <marquee style="color:coral; font-size: 30px;">
            <b><i>十里琅居聊天室</i></b>
        </marquee>
        <br>
        <br>
        <div style="text-align:center;">
            <marquee style="color:blue;">用户登录</marquee>
            <br>
            <br>
            <form action="/" method="POST">
                <input placeholder="用户名" type="input" name="account">
                <br>
                <br>
                <input value="登录" type="submit">
            </form>
        </div>
        <br>
    </body>
</html>
登录页

错误页html如下。通过模板html获取错误信息,并通过网页弹窗警告的方式返回错误信息。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>十里琅居聊天室</title>
        <script>
            window.alert("{{error_alert}}");
            window.location.href="/";
        </script>
    </head>
</html>
错误警告

聊天室页面html如下。Websocket前端代码全部在这里定义,当网页加载时建立websocket对象并连接后端websocket服务器,同时向服务器发送当前登录账号(在账号末尾添加特定词缀来和普通消息进行区分)来将账号和当前websocket对象建立联系。当前端接收到消息时会更新页面文本框的内容显示聊天消息并将文本框滚动至最底部以显示最新消息。当用户点击发送按钮后会获取用户输入框的内容并发送至后端处理。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>十里琅居聊天室</title>
        <script>
            var ws;

            function onLoad(){
                ws = new WebSocket("ws://"+window.location.host+"/ws");
                ws.onopen = function(){
                    ws.send('{{account}}**#*');
                }
                ws.onmessage = function(e){
                    var his_box = document.getElementById('history');
                    his_box.innerHTML = his_box.value + e.data;
                    his_box.scrollTop = his_box.scrollHeight;
                }
                ws.onclose = function(){
                    alert("连接断开");
                    window.location.href="/";
                }
            }

            function sendMsg(){
                ws.send(document.getElementById('msg').value);
                document.getElementById('msg').value = "";
            }

            function log_out(){
                window.location.href="/";
            }
        </script>
    </head>
    <body onLoad='onLoad();' background="/static/bg.jpg" style="background-size:100% 100%; background-repeat: no-repeat; background-attachment:fixed;">
        <marquee style="color:coral; font-size: 30px;">
            <b><i>十里琅居聊天室</i></b>
        </marquee>
        <br>
        <br>
        <div style="text-align:right;">
            <input type="button" value="注销" onclick="log_out();">
        </div>
        <div style="text-align:center;">
            <marquee style="color:blue;">欢迎您,{{account}}</marquee>
            <br>
            <br>
            <textarea id="history" rows="15" cols="50" disabled='disabled' style="background: white; color: black;"></textarea>
            <br>
            <br>
            <input placeholder="请输入消息内容" type="text" id="msg">
            <input type="button" value="发送" onclick="sendMsg();">
        </div>
        <br>
    </body>
</html>

后端websocket处理函数如下。

首选创建websocket类,初始化用户集合和用户字典。用户集合用于存储所有的当前连接的websocket对象,用户字典用于将websocket对象和用户名形成一一映射关系。

class ws_handler(WebSocketHandler):
    users = set()
    user_dict = {}

当建立连接时,将当前websocket对象加入集合。

    def open(self):
        self.users.add(self)

当收到消息时,首先判断发来的是否是账号信息(以“**#*”结尾),如果是则将账号加入用户字典,并判断是否存在该用户的聊天记录文件(聊天记录为以用户名命名的txt文件),若存在则返回聊天记录。再向所有用户发送消息某用户已进入聊天室,同时记录聊天记录。如果消息非账号信息则是普通的聊天信息,在消息之前添加时间和发送者后转发给所有用户并记录聊天记录。

    def on_message(self, message):
        if message.endswith('**#*'):
            self.user_dict[self] = message[:-4]
            if os.path.exists('history\\'+self.user_dict[self]+'.txt'):
                fp = open('history\\'+self.user_dict[self]+'.txt','r',encoding='utf-8')
                history = fp.read()
                fp.close()
                self.write_message(history)
            send_message = time.asctime(time.localtime(time.time()))[-13:-4]+self.user_dict[self]+' 已进入聊天室!'+'\n'
            for user in self.users:
                user.write_message(send_message)
                fp = open('history\\'+self.user_dict[user]+'.txt','a',encoding='utf-8')
                fp.write(send_message)
                fp.close()
        else:
            send_message = time.asctime(time.localtime(time.time()))[-13:-4]+self.user_dict[self]+':'+message+'\n'
            for user in self.users:
                user.write_message(send_message)
                fp = open('history\\'+self.user_dict[user]+'.txt','a',encoding='utf-8')
                fp.write(send_message)
                fp.close()

当一个连接关闭时在用户集合中删除该用户websocket对象,将离开聊天室的消息加入他的聊天记录,向所有在线用户发送该用户离开的消息并记录聊天记录,最后从用户字典中删除该用户。

    def on_close(self):
        self.users.discard(self)
        send_message = time.asctime(time.localtime(time.time()))[-13:-4]+self.user_dict[self]+' 已离开聊天室!'+'\n'
        fp = open('history\\'+self.user_dict[self]+'.txt','a',encoding='utf-8')
        fp.write(send_message)
        fp.close()
        for user in self.users:
            user.write_message(send_message)
            fp = open('history\\'+self.user_dict[user]+'.txt','a',encoding='utf-8')
            fp.write(send_message)
            fp.close()
        self.user_dict.pop(self)
多人聊天和记录还原

至此,全部前后端聊天程序编写完成。

A WindRunner. VoyagingOne

留言

您的邮箱地址不会被公开。 必填项已用 * 标注