tornado框架的学习与应用

来源:互联网 发布:java string变量类型 编辑:程序博客网 时间:2024/05/17 20:26

简单介绍一下所使用的高并发框架tornado,它是一个用python编写的可扩展的非阻塞式web服务器及其相关工具的开源框架,在处理严峻的网络流量时表现得足够强健,但却在创建和编写时有着足够的轻量级,并能够被用在大量的应用和工具中。
先简单介绍一下
用tornado实现的经典helloworld程序如下:

import tornado.ioloopimport tornado.webclass MainHandler(tornado.web.RequestHandler):    def get(self):        self.write("Hello, world")application = tornado.web.Application([    (r"/", MainHandler),])if __name__ == "__main__":    application.listen(8880)    tornado.ioloop.IOLoop.instance().start()

服务器程序运行之后,电脑访问本地回环地址即可看见:
hello

另一个全功能的helloworld程序如下:

import tornado.httpserverimport tornado.ioloopimport tornado.optionsimport tornado.webfrom tornado.options import define, optionsdefine("port", default=8000, help="run on the given port", type=int)class IndexHandler(tornado.web.RequestHandler):    def get(self):        greeting = self.get_argument('greeting', 'Hello')        self.write(greeting + ', friendly user!')if __name__ == "__main__":    tornado.options.parse_command_line()    app = tornado.web.Application(handlers=[(r"/", IndexHandler)])    http_server = tornado.httpserver.HTTPServer(app)    http_server.listen(options.port)    tornado.ioloop.IOLoop.instance().start()

命令行运行:
$ python hello.py --port=8000
即可在指定端口启动服务器
同样,无参访问服务器地址会显示默认参数:
1

携带参数访问,如输入:localhost:8000/?dairen会显示如下:
2

下面看一个处理字符串的服务器程序:

import textwrapimport tornado.httpserverimport tornado.ioloopimport tornado.optionsimport tornado.webfrom tornado.options import define, optionsdefine("port", default=8000, help="run on the given port", type=int)class ReverseHandler(tornado.web.RequestHandler):    def get(self, input):        self.write(input[::-1])class WrapHandler(tornado.web.RequestHandler):    def post(self):        text = self.get_argument('text')        width = self.get_argument('width', 40)        self.write(textwrap.fill(text, int(width)))if __name__ == "__main__":    tornado.options.parse_command_line()    app = tornado.web.Application(        handlers=[            (r"/reverse/(\w+)", ReverseHandler),            (r"/wrap", WrapHandler)        ]    )    http_server = tornado.httpserver.HTTPServer(app)    http_server.listen(options.port)    tornado.ioloop.IOLoop.instance().start()

这个程序有2个handler,一个是反转输入的字符串,参数input将包含匹配处理函数正则表达式第一个括号里的字符串。一个是以指定的宽度装饰文本(默认40),并将结果字符串写回到HTTP响应中。
同样运行服务器:
$ python string_service.py --port=8000
可以用2种方式访问:

$ curl http://localhost:8000/reverse/dairenneriad

3

$ http://localhost:8000/wrap -d text=Lorem+ipsum+dolor+sit+amet,+consectetuer+adipiscing+elit.Lorem ipsum dolor sit amet, consectetueradipiscing elit.

tornado的状态码:
你可以使用RequestHandler类的set_status()方法显式地设置HTTP状态码。然而,你需要记住在某些情况下,Tornado会自动地设置HTTP状态码。下面是一个常用情况的纲要:

404 Not Found
Tornado会在HTTP请求的路径无法匹配任何RequestHandler类相对应的模式时返回404(Not Found)响应码。

400 Bad Request
如果你调用了一个没有默认值的get_argument函数,并且没有发现给定名称的参数,Tornado将自动返回一个400(Bad Request)响应码。

405 Method Not Allowed
如果传入的请求使用了RequestHandler中没有定义的HTTP方法(比如,一个POST请求,但是处理函数中只有定义了get方法),Tornado将返回一个405(Methos Not Allowed)响应码。

500 Internal Server Error
当程序遇到任何不能让其退出的错误时,Tornado将返回500(Internal Server Error)响应码。你代码中任何没有捕获的异常也会导致500响应码。

200 OK
如果响应成功,并且没有其他返回码被设置,Tornado将默认返回一个200(OK)响应码。

更多tornado的内容,请参考:http://www.tornadoweb.cn/
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
下面介绍我的基于tornado的服务器项目:qq聊天群的实现
项目代码分为主要是2部分:
后台服务器python实现+前端(html静态文件+css格式+JavaScript客户端代码)

首先来看服务器的实现:
首先基于tornado.web.RequestHandler类实现自己的第一个请求处理类,如下:

class MainHandler(tornado.web.RequestHandler):    def get(self):        self.render("index.html", messages=ChatSocketHandler.cache, clients=ChatSocketHandler.waiters, username= "游客%d" % ChatSocketHandler.client_id)

其中用render方法引入模板index.html ,不仅仅是要引用模板网页,还要向这个网页传递一些数据,包括:
历史消息:messages
在线用户:clients
分配给当前访问的用户的用户名:username
由于想要实现的qq聊天群需要双向web通信,也就是不仅仅服务器响应客户端,客户端也能响应服务器发送的消息,所以又基于tornado.websocket.WebSocketHandler类实现了自己的websocket处理类,
Tornado在websocket模块中提供了一个WebSocketHandler类。这个类提供了和已连接的客户端通信的WebSocket事件和方法的钩子。当一个新的WebSocket连接打开时,open方法被调用,而on_message和on_close方法分别在连接接收到新的消息和客户端关闭时被调用。
此外,WebSocketHandler类还提供了write_message方法用于向客户端发送消息,close方法用于关闭连接。
在这里我把用户的消息类型分为3种,一种是上线:online,一种是下线:offline 一种是用户发消息了:message

每当有新的用户连接时,给其分配用户名,并把该用户加入到已连接用户列表中:

def open(self):    self.client_id = ChatSocketHandler.client_id    ChatSocketHandler.client_id = ChatSocketHandler.client_id + 1    self.username = "游客%d" % self.client_id    ChatSocketHandler.waiters.add(self)

然后封装一个用户上线的消息字典,专门去处理:

chat = {     "id": str(uuid.uuid4()),     "type": "online",     "client_id": self.client_id,     "username": self.username,     "datetime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")     } ChatSocketHandler.send_updates(chat)

同样,用户下线:删除用户,广播下线消息
用户发消息:更新历史消息列表,广播消息
用户发送的消息会以json的格式收到,所以要先解析,然后提取其中的信息,包装成我们自己的chat字典,然后群发:

parsed = tornado.escape.json_decode(message)self.username = parsed["username"]chat = {    "id": str(uuid.uuid4()),    "body": parsed["body"],    "type": "message",    "client_id": self.client_id,     "username": self.username,    "datetime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")    }

其中广播消息也就是向在线的每一个用户,使用websocket的功能发送一次消息。

写好了open on_close on_message类之后,下面考虑我想要实现的历史记录的实现,可以缓存1000条消息记录放在一个列表中(列表中每一项是一个chat的字典类型),每次新用户访问就把这个列表[]发给他,让他也能看到历史记录,每当记录满了,那就截取最后1000条保存下来:

if len(cls.cache) > cls.cache_size:            cls.cache = cls.cache[-cls.cache_size:]

这里的cache_size我定义为1000,也就是缓存1000条消息记录。

另外,为了完善对服务器运行过程的掌控,加入python日志模块,也就是导入logging模块,

 logging.info("sending message to %d waiters", len(cls.waiters))

这样控制台就可以观察到执行的一些操作了,当然也可以把日志写进文件中以后查阅。

至此,我们服务器的大致内容设计的差不多了,下面考虑前端显示问题,以及基于websocket通信的JavaScript客户端程序设计问题
首先想一想聊天室主页的显示情况,首先要有用户列表,实时显示在线的用户,还要有历史消息列表,显示我们缓存的1000条聊天记录,另外就是像qq群一样的聊天界面,你一句我一句,要有用户名,消息体,时间,然后最下面还要有我们自己的聊天信息输入框与发送的地方,差不多就这些吧
先分析主页模板html文件怎么写吧,用户列表:

        <div id="users">             在线用户:<p/>             <ul id="user_list">        {% for client in clients %}        <li id={{client.client_id}}>{{client.username}}</li>        {% end %}             </ul>        </div>

这样就会显示服务器传给html文件里的clients 的所有在线用户。再看我们聊天界面显示的每一条消息,显然按格式包装起来会更方便,所以每条消息就以一个格式包装与显示:

<div class="message" id="m{{ message["id"] }}">{{message["username"]}} 说:{% module linkify(message["body"]) %} ({{message["datetime"]}})</div>

再看历史消息的显示格式,历史消息也是新用户上线的时候服务器在渲染模板html的时候加入的参数,html模板中大致以如下格式进行渲染:

  <div id="inbox">    {% for message in messages %}      {% include "message.html" %}    {% end %}  </div>

最后就是我们的消息内容输入与发送,这一部分需要服务器JavaScript代码把我们用户输入的消息接收处理,然后发给我们的服务器,服务器进行解析后给与响应,html格式大致如下:

        <form action="/a/message/new" method="post" id="messageform">          <table>            <tr>          <td >用户名:</td>              <td><input name="username" id="username" style="width:100px" value="{{username}}"></td>        </tr>        <tr>          <td>输入消息:</td>          <td><input name="body" id="message" style="width:500px"></td>        </tr>        <tr>              <td style="padding-left:5px">                <input type="submit" value="提交">                <input type="hidden" name="next" value="{{ request.path }}">                {% module xsrf_form_html() %}              </td>            </tr>          </table>        </form>

这里使用了HTML表单,表单用于客户端收集用户在浏览器的输入,是实现客户端与服务器交互的核心方法,注意:

{% module xsrf_form_html() %}

这里我使用了tornado的防止跨站攻击的手段,非常简单,要么在实例化tornado.web.Application的时候传入参数:xsrf_cookies=True,要么在每个具有HTML表单的模板文件中,为所有HTML表单添加xsrf_form_html()函数标签,而{% module xsrf_form_html() %}这句话起到了为表单添加隐藏元素,以防止跨站请求的作用。
这一句话作用挺大,也可以看出tornado为开发者减轻了很多的负担,加快了开发效率,不然就要自己考虑很多细节性的工作。

至此,我们关于主页模板的内容基本介绍完毕,下面就是介绍客户端的JavaScript部分,我们的qq聊天室是基于websocket的全双工的,而websocket是html5的标准之一,所以主流的浏览器的JavaScript编程语言是支持websocket编程的,方法也很简单,就是围绕着websocket对象,使用:

socket = new WebSocket(url);

就可根据url,新建一个websocket连接,然后与之通信,然后响应websocket的如下事件:

websocket.onopen(),
websocket.onmessage(),
websocket.onerror(),
websocket.onclose(),被动关闭连接

然后websocket还有2种主动事件类型:
websocket.send()
websocket.close():主动关闭连接
下面开始我的客户端部分的设计,网页被加载的时候,浏览器会创建页面文档对象模型DOM树,JavaScript中几乎所有的内容都是基于DOM树来进行的,而DOM树的根节点是document,JavaScript通过操作DOM树,进行 增删改查,实现对html的操作,在本项目的JavaScript代码中,我使用了最优秀的js客户端框架库jQuery,jQuery能方便的处理html,响应事件,实现动画效果,可以把它认为是html css JavaScript的封装,使用jQuery可以让开发者更轻松地写JavaScript代码。
我这里直接在html源文件中引用internet上的jQuery库连接,更轻量:

<script src="{{ static_url("jquery.min.js") }}" type="text/javascript"></script>

而我们在JavaScript代码中引用jQuery库也很方便,就是一个美元的符号:$
形如:
$(selector).action()
这里selector是选择器,action就是要进行的操作。jQuery中的选择器与css中的选择器很类似,就是按标记名,id等进行选择,另外还可以根据特定的属性,根据标记相对于父标记的位置,元素内容等进行选择。
下面直奔主题,首先要处理我们的:文档全部加载完成的事件,也就是:
$(document).ready(function()
{
}
jQuery中把html事件中的on都给去掉了,譬如onclick事件,在jQuery中就是click事件,
这个函数,首先要实现发送表单:messageform的submit事件:

$("#messageform").live("submit", function() {    newMessage($(this));    return false;});

一旦有message提交了,立马执行newMessage函数,也就是给服务器发消息
下面同样的作用,只不过是监控keyCode == 13的按键,也就是我们键盘上的enter键

$("#messageform").live("keypress", function(e) {    if (e.keyCode == 13) {        newMessage($(this));        return false;    }});

其中的newmessage函数实现如下:

function newMessage(form) {    var message = form.formToDict();    updater.socket.send(JSON.stringify(message));    form.find("input[type=text]").val("").select();}

就是向服务器以字典的格式,发送一个新的消息,其中的formToDict 函数实现如下:

formToDict = function() {    var fields = this.serializeArray();    var json = {}    for (var i = 0; i < fields.length; i++) {        json[fields[i].name] = fields[i].value;    }    if (json.next) delete json.next;    return json;};

作用是把表单中所有的输入保存到json对象中去,最后返回客户端要发给服务器的消息字典,也是一个json对象。

另外我这里设置了一个,一旦选中了我们之前html文件里定义的id为message编辑框的控件,就开始发起一个连接,动作如下:

$("#message").select();updater.start();

其中,start()内容大致为:

function() {        var url = "ws://" + location.host + "/chatsocket";        updater.socket = new WebSocket(url);        updater.socket.onmessage = function(event) {            updater.showMessage(JSON.parse(event.data));        }}

这里一旦选中了编辑框,客户端js代码就开始新建一个websocket连接,其中url 就是我们服务器的地址,并且设置了我们的onmessage()函数,也就是响应服务器消息的函数,其内容大致如下:

showMessage: function(message) {    del(message.client_id);    if (message.type!="offline")    {        add(message.client_id, message.username);        if (message.body=="") return;        var existing = $("#m" + message.id);        if (existing.length > 0) return;        var node = $(message.html);        node.hide();        $("#inbox").append(node);        node.slideDown();    }}

当收到一个服务器的消息,先去分析消息的类型:online offline 还是 message类型,分别做不同的响应,我的实现是先不管消息类型,先把当前的用户给删了,调用del函数来动态删除在线用户列表,del函数如下:
动态删除:

function del(id){    $('#'+id).remove();}

然后再来分析消息类型是不是offline,如果是offline,那就结束了
如果不是offline,那就是上线或者发消息啦,所以再把刚刚删的用户添加到用户名列表中,调用add函数,如下:
动态添加:

function add(id,txt) {        var ul=$('#user_list');        var li= document.createElement("li");        li.innerHTML=txt;      li.id=id;      ul.append(li);    } 

其中$(‘#user_list’)就是使用jQuery的查找器进行元素定位,查找id为user_list的HTML标签,之后就可以通过append(),remove来动态的添加删除列表元素了。

接着判断消息是不是无效,也就是消息体为空,那就直接return,这样就实现了
用户下线:列表中删掉用户
用户上线(一旦动了编辑框就会上线),但没发消息:先删后加,刷新了一下
然后再实现用户发消息的过程,那就比较简单了,就是:

 var node = $(message.html); node.hide(); $("#inbox").append(node); node.slideDown();

先选中了消息,为新收到的消息建立一个新的节点node,先隐藏消息(因为发送出去要清空的啊),然后把消息添加到我们的html文件中的inbox标签的尾部,进行显示,inbox标签内容如下:

<div id="inbox">  {% for message in messages %}    {% include "message.html" %}  {% end %}</div>

然后把聊天界面下拉到最下面,至此就可以动态的处理:显示服务器发送的消息了,也就是所有的客户端都能正确显示服务器广播的消息了。

最后就是把我们的服务器JavaScript代码嵌入到主页的HTML中,一般的嵌入方式有内部嵌入和外部链接两种方式,我这里把JavaScript代码单独放在了一个文件里,使用了外部链接的方式嵌入到了html文件中:

<script src="{{ static_url("chat.js") }}" type="text/javascript"></script>

这里chat.js就是我的js代码文件。
还有别忘了在嵌入js之前,嵌入我们使用的jQuery库,所以应该是这个样子:

<script src="{{ static_url("jquery.min.js") }}" type="text/javascript"></script><script src="{{ static_url("chat.js") }}" type="text/javascript"></script>

至此,我的qq群的前端+后台已经基本实现,
python mychat.py --port=8880
在8880端口运行服务器之后,在本地用回环地址localhost:8880访问:
11

22

或者别的主机使用
ip:port的形式,都能访问该聊天群,服务器所在ip地址,也就是我的电脑ip是:115.156.245.122

我这里使用师兄的电脑登录了一下随便发送了一条消息:
33

44

同时由于我们服务器加入了logging日志模块,所以在控制台可以看到我想要打印的消息,例如:
55

》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》

至此,该项目以完成我想要的一个qq聊天群的所有基本功能了。但是项目改进的余地还很大,我总结一下,
不足主要有3点:
1、由于没有用户持久化的访问需求,都是立马连立马走,所以没有使用数据库,如果设计一个qq好友系统。可以考虑加入数据库,把用户以及用户关系,历史消息,用户名密码等等都保存在数据库中
2、由于前端知识只是略懂,所以界面略微简陋。。。有需要可以深入学习前端的知识
3、功能可以更加丰富化,可以加入群权限控制,使得有类似于管理员一样的存在,还可以加入群文件共享,群公告,群照片等等。。。

1 0