Tornado 搭建基于 WebSocket 的聊天服务

这年头 Python web 框架是有点泛滥了. 下面要介绍的是 facebook 的开源框架 tornado. 这东西比较简单, 而且自带 WebSocket 支持, 可以用它做个简单的聊天室.

Tornado 搭建基于 WebSocket 的聊天服务

读者最好已具备 Javascript 与 WebSocket 的基础知识.

安装

使用 easy_install 能很方便地爬到 tornado. 或者, 下载源代码, 解包后在源码目录执行

1
2
$ python setup . py build 
# python setup.py install

即可.

首先还是来个 hello world.

1
2
3
4
5
6
7
8
9
10
11
12
13
import tornado.web
import tornado.ioloop
 
class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('<html><body>Hello, world!')
 
if __name__ == '__main__':
    app = tornado.web.Application([
        ('/', Index),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

保存为 main.py, 然后执行

1
$ python main.py

并访问 http://localhost:8000/ 即可看到页面中的 "Hello, world!".

在分支中定义的app在构造时接受的一个列表参数

1
2
3
[ 
     ( '/' ,   Index ), 
]

用来配置 URL 映射, 比如这里访问根路径则映射至Index实例去处理, 在Index实例中, 定义的get方法将会处理请求,处理 WebSocket 连接。

接下来就进入 WebSocket 环节. 先修改返回的页面, 让这个页面在加载后连接服务器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Index(tornado.web.RequestHandler):
    def get(self):
        self.write('''
<html>
<head>
<script>
var ws = new WebSocket('ws://localhost:8000/soc');
ws.onmessage = function(event) {
    document.getElementById('message').innerHTML = event.data;
};
</script>
</head>
<body>
<p id='message'></p>
        ''')

修改这个类后, 然后在控制台中止服务器 (猛击 Ctrl-C), 并重新启动之. 现在, 访问 http://localhost:8000/ 会遇到 404 错误, 因为 WebSocket 请求的 URL "ws://localhost:8000/soc" 还没有映射任何处理器, 因此这里需要再添加一个, 用于处理 WebSocket 请求的类.

1
2
3
4
5
import tornado.websocket
 
class SocketHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        self.write_message('Welcome to WebSocket')

并为这个类加上 URL 映射

1
2
3
4
5
6
7
if __name__ == '__main__':
    app = tornado.web.Application([
        ('/', Index),
        ('/soc', SocketHandler),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()

然后重启服务器, 并访问 http://localhost:8000/ 就可以在页面上看到服务器传来的信息了.

使用模板

在进一步完善聊天功能之前, 先整理一下代码. 让大坨的 HTML 出现在 Python 源码文件中显然是件不合适的事情. 使用render函数可以处理模板 HTML 文件并传递给客户端.

1
2
3
class Index(tornado.web.RequestHandler):
    def get(self):
        self.render('templates/index.html')

然后把先前的 HTML 内容放入 templates 目录下的 index.html 文件中. 再重启服务器, 这样就将 HTML 内容分离出去了.

管理连接者

接下来要做的一件事情是记录客户端的连接. 在SocketHandler类里面放置一个集合, 用来记录开启着的连接.

1
2
3
4
5
6
7
8
9
class SocketHandler(tornado.websocket.WebSocketHandler):
    clients = set()
 
    def open(self):
        self.write_message('Welcome to WebSocket')
        SocketHandler.clients.add(self)
 
    def on_close(self):
        SocketHandler.clients.remove(self)

然后再改改, 安排每个连接者给其它连接者打个招呼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SocketHandler(tornado.websocket.WebSocketHandler):
    clients = set()
 
    @staticmethod
    def send_to_all(message):
        for c in SocketHandler.clients:
            c.write_message(message)
 
    def open(self):
        self.write_message('Welcome to WebSocket')
        SocketHandler.send_to_all(str(id(self)) + ' has joined')
        SocketHandler.clients.add(self)
 
    def on_close(self):
        SocketHandler.clients.remove(self)
        SocketHandler.send_to_all(str(id(self)) + ' has left')

把首页给改改

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<script>
var ws = new WebSocket('ws://localhost:8000/soc');
ws.onmessage = function(event) {
    var table = document.getElementById('message');
    table.insertRow().insertCell().innerHTML = event.data;
};
</script>
</head>
<body>
<table id='message'></table>

再来一发, 多开浏览器标签页访问网站, 那么先进入的则会看到后连上的客户端连入信息. (刷新页面则会产生一次离开一次进入.) 聊天功能

更换协议

之前, WebSocket 处理程序都是直接将字符串写回给客户端, 这样的问题是, 客户端很难区分是聊天信息还是系统信息. 下面规定一个简单的通信协议.

1
2
3
4
5
传递给客户端的将是一个 JSON 字典
字典中至少包含键 "type"
当有用户连接或离开聊天室时, "type" 对应的值为 "sys", 并且字典中还将包含键 "message", 值为连接或离开的信息
当有用户输入聊天信息时, "type" 对应的值为 "user", 且字典中还将包含键 "id" 对应聊天用户的 id, 以及键 "message" 表示聊天内容
用户输入的聊天信息为字符串

现在按照这个协议来修改 HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ws.onmessage = function(event) {
    var table = document.getElementById('message');
    var data = eval('(' + event.data + ')');
    ({
        'sys': function() {
            var cell = table.insertRow().insertCell();
            cell.colSpan = 2;
            cell.innerHTML = data['message'];
        },
        'user': function() {
            var row = table.insertRow();
            row.insertCell().innerHTML = data['message'];
            row.insertCell().innerHTML = data['id'];
        },
    }[data['type']])();
};

然后改SocketHandler(这里需要用到 json 库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import json
 
# ...
 
@staticmethod
def send_to_all(message):
    for c in SocketHandler.clients:
        c.write_message(json.dumps(message))
 
def open(self):
    self.write_message(json.dumps({
        'type': 'sys',
        'message': 'Welcome to WebSocket',
    }))
    SocketHandler.send_to_all({
        'type': 'sys',
        'message': str(id(self)) + ' has joined',
    })
    SocketHandler.clients.add(self)
 
def on_close(self):
    SocketHandler.clients.remove(self)
    SocketHandler.send_to_all({
        'type': 'sys',
        'message': str(id(self)) + ' has left',
    })

聊天能力, 展开!

这样每次有连接新加入或者断开, 其它页面都可以接收到消息了. 现在要做的则是接受用户聊天信息. 那么要添加一些小玩意儿到 HTML 来具备发信能力.

1
2
3
4
5
6
7
8
9
10
11
<script>
/* ... */
function send() {
    ws.send(document.getElementById('chat').value);
    document.getElementById('chat').value = '';
}
</script>
</head>
<body>
<input id='chat'><button onclick='send()'>Send</button>
<table id='message' border='1'></table>

再为SocketHandler加上消息处理函数

1
2
3
4
5
6
7
8
9
class SocketHandler(tornado.websocket.WebSocketHandler):
    # ...
 
    def on_message(self, message):
        SocketHandler.send_to_all({
            'type': 'user',
            'id': id(self),
            'message': message,
        })

现在重启, 然后访问 http://localhost:8000/ , 一个简易的聊天室就弄好了.

本文网址: https://pylist.com/topic/57.html 转摘请注明来源

Suggested Topics

tornado websocket 客户端与服务器端示例

最近在网上找了些websocket的资料看了下,node和tornado等等本身已经实现了websocket的封装,所以使用起来会比较简单,php如果想要写websocket还需要自己跑一整套流程,比较麻烦。...

Golang 服务之坑:too many open files

出现这个问题是因为服务的文件句柄超出系统限制。当Go服务程序出现这个问题,首先应该看系统设置,然后再看程序本身。...

Leave a Comment