tornado可以用作游戏工作室服务器服务器么

在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
本人想用 tornado做服务器端,用HTTP json API来与iOS Native App交互,这种方式是否可行?
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
首先用HTTP json API和iOS Native App做交互绝对是我目前看到过的最好的方式
至于容器用什么,完全取决于你的熟悉程度,选择你最习惯的技术。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
可以,目前知道有公司用tornado作为安卓后台,支撑了每日上亿请求
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
.NET 原生APP服务器项目源码 淘宝开卖500块本项目源码实现APP原生.NET服务器项目源码,使用TCP通信与指令树模式开发,附带java安卓APP客户端通信库。
.NET 分布式全局会话中心服务器与会话节点项目源码 淘宝开卖500块本代码实现ASP.NET会话节点,实现.NET Windows会话节点,实现java web会话节点(java web会话节点有个过期bug还没处理完成),支持原生APP与web app与web群集等集中会话管理全局会话漫游,会话Id可以在任意节点漫游。
.NET VC++ TM4交易信号服务项目代码 淘宝开卖500块本项目代码实现MT4通过EA将计算实时通过中间服务器处理分发实现计算EA脚本与实时交易数据同步到中间服务器实现代码。
VC++实现EA API对接通过.NET C#实现实时获取市场数据结算结果在中间服务器后又中间服务器提供给客户群作为交易提示的技术实现。
物联网.NET APP车联网服务器项目源码 淘宝开卖500块本项目实现车联网与APP实时监控车辆状态并且通过APP控制车辆锁车、解锁、启动、熄火、GPS上报、车辆其它状态参数上报等中间服务器实现代码。
.NET 半成品TCP通信与Plugin与控件等代码库 淘宝开卖100块
ASP.NET Ajax 后台管理服务器控件源码 淘宝开卖100块一个控件搞定后台布局一套ASP.NET Ajax 后台管理服务器控件源码,支持UpdatePanel上传。效果展示地址:
该答案已被忽略,原因:
同步到新浪微博
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:1112被浏览178267分享邀请回答wiki.python.org/moin/WebFrameworks)A Web framework is a collection of packages or modules which allow developers to write Web applications or services without having to handle such low-level details as protocols, sockets or process/thread management. Tornado作为web server,提供了web framework的api,可以来直接构建自己的web程序。同时,Tornado支持WSGI (
),也就是说它可以有能力其它的一些python的框架一起使用,比如django, bottle, flask等。不妨看下bottle在不同server下的性能评测,其中就有tornado(
)。额外说的是,WSGI的框架是不支持异步的,所以如果有异步调用的逻辑的web程序,Tornado也是选择之一。没有使用过Node.js,但官网说:Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. 可以把Node.js理解成基于V8的javascript运行环境或工具包,它属于偏底层的抽象,扩展了javascript写服务端程序的能力。所以基于它也会有不同的web framework。从这个角度来看,Node.js和Tornado其实不在一个层次上。不过相同的是 Node.js 和 Tornado都采用的的单进程单线程异步IO的网络模型。它们都可以写异步非阻塞的程序,不过我觉得因为javascript的语言特性,Node.js对异步回调非阻塞的风格坚持比Tornado更彻底一点。不过通常多进程时,一个进程的短时间阻塞也是可以接受的。使用Nginx是因为一些单进程服务无法利用多核CPU,同时也会有单机的限制,所以通常会在多个服务器启动多个进程实例,使用Nginx在前端作为反向代理服务器来分发web请求,同时负载均衡。Nginx是C写的,有更好的并发性,可配置性,对静态文件也有更好的支持。当然这是Nginx的常用情景,其实也可以直接使用Nginx来构建web应用,可以参考OpenResty项目(
)。其实网络模型、web server、web framework是三个不同层次,它们之间并不冲突,是选择的关系。理解前,不妨先理清概念:)868 条评论分享收藏感谢收起/c10k.html 其实,你也可以说nginx是一个framework,不过是针对C的。裸WebServer表示他支持HTTP协议,既然支持HTTP协议,你前面放啥反向都无所谓,nginx apache squid varnish。或者也可以用wsgi模块,只是采用了特殊的通讯协议。为什么要在tornado前面放nginx,原因在于,Python虽然有多线程,但是Python的解释器有GIL
这点非常影响了Python和Tornado利用多核的能力,所以只能通过多进程来利用多核。既然多进程,一般就需要在前端放置nginx haproxy做为负载均衡的反向代理,或是使用这些应用服务器的wsgi模块来管理进程的生命周期等等。另外对于静态文件的服务,也是nginx之类的更具有优势。(他们直接把请求转发到tornado的进程中,你根本不用管请求怎么来的,专心写你的程序。)其他很多脚本语言的框架都有类似问题,比如ruby,而Nodejs更加是连线程都没有了。Nodejs的核心在于,他使用了JavaScript天生擅长的事件/回调机制来实现异步。Python对于匿名函数和闭包支持不够好,所以Tornado通过了类来和回调方法来实现异步,这里没有事件的概念。(没深入研究过Tornado,大致是这么了解的)另外Tornado封装了很多常用的诸如静态文件处理、Router、模板等等,而Nodejs基本上是一个纯粹的协议框架,这些功能都需要自己去写。Nodejs+Express差不多就可以等于Tornado的功能了。而如果是要做一个中间件或中间层,做一些高级点的协议处理,可能nodejs更加能胜任。在tornado里面去调用用php或者nodejs所开发的服务,这是没问题的,主要要看架构上如何设计。225 条评论分享收藏感谢收起查看更多回答一、自定义的异步非阻塞的客户端
#!/usr/bin/env python
# -*- coding: utf8 -*-
# __Author: "Skiler Hao"
import select
import socket
import pprint
自定义了异步IO模块
利用非阻塞的socket,不等待连接是否成功,不等待请求的响应
select模块,去监听创建的套接字,是否有准备写,准备读的
class HttpResponse:
def __init__(self, response_data):
self.raw_data = response_data
self.data = str(response_data, encoding='utf-8')
self.Header = {}
self.GET = {}
self.BODY = {}
self.response_info = ''
self.initialize()
def initialize(self):
header_data, body_data = self.data.split('\r\n\r\n', 1)
self.BODY = body_data
header_list = header_data.split('\r\n')
# print(header_list)
for header_item in header_list:
header_byte = header_item.split(':', 1)
if len(header_byte) == 2:
self.Header[header_byte[0]] = header_byte[1]
self.response_info = header_byte
def __str__(self):
return self.response_info
class HttpRequest:
对HttpRequest的简单封装,将socket,host和callback封装成一个对象
def __init__(self, sk, host, callback):
收到socket,host, callback回调函数,将其封装成HttRequest对象
:param sk:一个socket对象
:param host:主机名或者域名
:param callback:socket结束后的回调函数
self.socket = sk
self.host = host
self.callback = callback
def fileno(self):
select可以直接监听
定义一个fileno()并返回一个,可以将HttpRequest()对象直接放在select直接去轮训,监听
:return:fileno()文件描述符
return self.socket.fileno()
class AsyncHttpClient:
异步Http客户端
def __init__(self):
self.conn = []
self.connection = []
def add_request(self, host, callback):
传过来一个host和callback,自动将其封装HttpRequest对象,然后将其加入到self.conn和self.connection中
:param host: 传过来一个host,一个回调函数callback
:param callback:
sk = socket.socket()
sk.setblocking(0)
# 创建一个非阻塞的socket
sk.connect((host, 80))
# 这个socket去连指定的主机的80端口
except BlockingIOError as e:
# 将socket,host,callback封装成一个对象
http_request = HttpRequest(sk, host, callback)
# 将对象追加到conn和connection
self.conn.append(http_request)
self.connection.append(http_request)
def run(self):
# 任务调用的接口
while True:
# 监听套接字的信号
# select(rlist,wlist,xlist)
# 三个列表,select分别监听 三个列表,rlist是否有读信号,wlist是否写信号,xlist是否有异常信号,最后是timeout
# 一旦有某个对象相应的信号,就将当前的当前的对象返回给对应的位置
rlist, wlist, xlist = select.select(self.conn, self.connection, self.conn, 0.05)
# 一旦有写信号
for w in wlist:
# 首次就是服务器,告知连接成功,你可以往里面写数据啦
# print(w.host,'连接成功,赶紧写数据吧!')
# 在套接字赶紧写上我要获取你的首页
tpl = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n" % (w.host,)
w.socket.send(bytes(tpl, encoding='utf-8'))
# select就不用监听这个套接字是否有写入信号,直接移除掉
self.connection.remove(w)
# 一旦有读信号
for r in rlist:
# print(r.host,'开始收到数据啦~~~')
rec_data = bytes()
while True:
# 开始收数据,直到接收不到数据
chunk = r.socket.recv(8096)
# 一次接受8096个字节的数据
rec_data += chunk
except BlockingIOError as e:
# print(r.host,rec_data)
# 执行打包中的callback方法,将接收到数据传过去
r.callback(rec_data)
r.socket.close()
# 获取到相应的相应数据,就不在监听其是否有读数据
self.conn.remove(r)
if len(self.conn) == 0:
# 如果没有要监听是否有读信号的套接字,就可以跳出循环任务结束了
def f1(rec_data):
response = HttpResponse(rec_data)
print(response.BODY)
def f2(data):
print('输出到屏幕')
# 定义一个字典列表,字典列表包含主机名和回调函数
url_list = [
{'host': '', 'callback': f1},
{'host': '', 'callback': f1},
{'host': '', 'callback': f1},
# 声明一个异步Async
requestclient = AsyncHttpClient()
for item in url_list:
requestclient.add_request(item['host'], item['callback'])
requestclient.run()
二、自定义的异步非阻塞的服务端
#!/usr/bin/env python
# -*- coding: utf8 -*-
# __Author: "Skiler Hao"
import socket, select
EOL1 = b'/r/n'
EOL2 = b'/r/n/r/n'
# 拼接成的response
response = b'HTTP/1.0 200 OK/r/nDate: Mon, 1 Jan :01 GMT/r/n'
response += b'Content-Type: text/plain/r/nContent-Length: 13/r/n/r/n'
response += b'Hello, world!'
# 创建一个服务端的socket,来监听是否有请求过来
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
serversocket.setblocking(0)
# 设置时非阻塞
print(serversocket.fileno())
epoll = select.epoll()
# 准备设计一个IO多路复用
epoll.register(serversocket.fileno(), select.EPOLLIN)
# 把上面的serversocket的fileno() 和 监听准备读信号 注册到epoll中去
# 设置三个空字典
connections = {}
requests = {}
responses = {}
while True:
# 查看epoll是否有信号有的话,就放在events中
events = epoll.poll(1)
# 循环events,分别拿到 文件描述号 和对应的事件
for fileno, event in events:
# 如果当前的文件描述号是serversocket,那么说明有新的连接
if fileno == serversocket.fileno():
# 所以就得接受,创建了 连接,拿到了对方的IP地址
connection, address = serversocket.accept()
# connection就是客户端连接过来建立的socket,设置为非阻塞
connection.setblocking(0)
# 客户端建立的socket也注册到select模块的IO多路复用中去
epoll.register(connection.fileno(), select.EPOLLIN)
# 以Connection的文件描述号 作为键 socket作为值保存在connections中
connections[connection.fileno()] = connection
# 同时在requests和responses字典中,
# requests中 以connection.fileno() 作为键 以请求的内容作为值
# responses中 以connection.fileno() 作为键 以相应的内容作为值,这个我们返回的是固定的,仅仅返回hello world
requests[connection.fileno()] = b''
responses[connection.fileno()] = response
# 如果请求的数据不是socketserver,那肯定是客户端的,判断是否是准备读的信号
elif event & select.EPOLLIN:
# 立马来开始读取数据,加到requests对象套接字的内容中去
requests[fileno] += connections[fileno].recv(1024)
# 判断换行 和 两个换行是否在接收过来的数据中
if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
# 如果是的话,就将这个套接字的监听更新为准备写
epoll.modify(fileno, select.EPOLLOUT)
# 打印40个-,然后换行,加上请求的内容
print('-' * 40 + '/n' + requests[fileno].decode()[:-2])
# 如果请求的数据不是socketserver,那肯定是客户端的,判断是否是准备写的信号
elif event & select.EPOLLOUT:
# 立马来开始读取数据,就将response的对应的套接字号对应的值拿出来,其实就是hello world,O(&_&)O哈哈~
# 并统计发送了多少个字节
byteswritten = connections[fileno].send(responses[fileno])
# 更新response对应套接字内容为剩下的内容
responses[fileno] = responses[fileno][byteswritten:]
# 如果内容发完了,剩下的长度就是0,如果长度是0
if len(responses[fileno]) == 0:
# 就修改select,不监听该套接字的内容,其就变成了EPOLLHUP
epoll.modify(fileno, 0)
# 然后关闭该socket的管道
connections[fileno].shutdown(socket.SHUT_RDWR)
elif event & select.EPOLLHUP:
# 如果不监听该套接字的内容,就将其注销掉
epoll.unregister(fileno)
# 关闭该套接字
connections[fileno].close()
# 从连接中删除该文件描述符
del connections[fileno]
# 最后关闭serversocket服务器套接字
epoll.unregister(serversocket.fileno())
# 关闭epoll
epoll.close()
# 套接字关闭
serversocket.close()
阅读(...) 评论()博客访问: 5532198
博文数量: 757
博客积分: 2150
博客等级: 上尉
技术积分: 12991
注册时间:
IT168企业级官微
微信号:IT168qiye
系统架构师大会
微信号:SACC2013
分类: Python/Ruby
本人的第一次翻译,转载请注明出处:/yiwenshengmei/archive//understanding_tornado.html
原文地址:http://golubenco.org/?p=16
这篇文章的目的在于对Tornado这个异步服务器软件的底层进行一番探索。
我采用自底向上的方式进行介绍,从轮巡开始,向上一直到应用层,指出我认为有趣的部分。
所以,如果你有打算要阅读Tornado这个web框架的源码,又或者是你对一个异步web服务器是如何工作的感兴趣,我可以在这成为你的指导。
通过阅读这篇文章,你将可以:
自己写一个Comet架构程序的服务器端部分,即使你是从拷贝别人的代码开始。
如果你想在Tornado框架上做开发,通过这篇文章你将更好的理解Tornado web框架。
在Tornado和Twisted的争论上,你将更有见解。
假设你还不知道Tornado是什么也不知道为什么应该对它感兴趣,那我将用简短的话来介绍Tornado这个项目。
如果你已经对它有了兴趣,你可以跳去看下一节内容。
Tornado是一个用Python编写的异步HTTP服务器,同时也是一个web开发框架。
该框架服务于FriendFeed网站,最近Facebook也在使用它。
FriendFeed网站有用户数多和应用实时性强的特点,所以性能和可扩展性是很受重视的。
由于现在它是开源的了(这得归功于Facebook),我们可以彻底的对它是如何工作的一探究竟。
我觉得对非阻塞式IO (nonblocking IO) 和异步IO (asynchronous IO &AIO)很有必要谈一谈。
如果你已经完全知道他们是什么了,可以跳去看下一节。我尽可能的使用一些例子来说明它们是什么。
让我们假设你正在写一个需要请求一些来自其他服务器上的数据(比如数据库服务,再比如新浪微博的open api)的应用程序,
然后呢这些请求将花费一个比较长的时间,假设需要花费5秒钟。大多数的web开发框架中处理请求的代码大概长这样:
def handler_request(self, request):
& & answ = self.remote_server.query(request) # this takes 5 seconds
& & request.write_response(answ)
如果这些代码运行在单个线程中,你的服务器只能每5秒接收一个客户端的请求。
在这5秒钟的时间里,服务器不能干其他任何事情,所以,你的服务效率是每秒0.2个请求,哦,这太糟糕了。&
当然,没人那么天真,大部分服务器会使用多线程技术来让服务器一次接收多个客户端的请求,
我们假设你有20个线程,你将在性能上获得20倍的提高,所以现在你的服务器效率是每秒接受4个请求,
但这还是太低了,当然,你可以通过不断地提高线程的数量来解决这个问题,
但是,线程在内存和调度方面的开销是昂贵的,我怀疑如果你使用这种提高线程数量的方式将永远不可能达到每秒100个请求的效率。
如果使用AIO,达到每秒上千个请求的效率是非常轻松的事情。服务器请求处理的代码将被改成这样:
def handler_request(self, request):
& & self.remote_server.query_async(request, self.response_received) & &&
def response_received(self, request, answ): & &# this is called 5 seconds later
& & request.write(answ)
AIO的思想是当我们在等待结果的时候不阻塞,
转而我们给框架一个回调函数作为参数,让框架在有结果的时候通过回调函数通知我们。
这样,服务器就可以被解放去接受其他客户端的请求了。
然而这也是AIO不太好的地方:代码有点不直观了。
还有,如果你使用像Tornado这样的单线程AIO服务器软件,你需要时刻小心不要去阻塞什么,
因为所有本该在当前返回的请求都会像上述处理那样被延迟返回。
关于异步IO,比当前这篇过分简单的介绍更好的学习资料请看 The C10K problem(/c10k.html)
二、源代码
该项目由github托管,你可以通过如下命令获得,虽然通过阅读这篇文章你也可以不需要它是吧。
git clone git:///facebook/tornado.git
在tornado的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。
在每个源代码的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,
doc string中给出了一到两个关于如何使用该模块的例子。
三、IOLoop模块
让我们通过查看ioloop.py文件直接进入服务器的核心。这个模块是异步机制的核心。
它包含了一系列已经打开的文件描述符(译者:也就是文件指针)和每个描述符的处理器(handlers)。
它的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器
(一种IO多路复用的实现,其实就是socket众多IO模型中的select模型,在Java中就是NIO,译者注)。
可以通过调用add_handler()方法将一个socket加入IO循环中:
def add_handler(self, fd, handler, events):
& & """Registers the given handler to receive the given events for fd."""
& & self._handlers[fd] = handler
& & self._impl.register(fd, events | self.ERROR)
_handlers这个字典类型的变量保存着文件描述符(其实就是socket,译者注)到当该文件描述符准备好时需要调用的方法的映射
(在Tornado中,该方法被称为处理器)。然后,文件描述符被注册到epoll(unix中的一种IO轮询机制,貌似,译者注)列表中。
Tornado关心三种类型的事件(指发生在文件描述上的事件,译者注):READ,WRITE 和 ERROR。
正如你所见,ERROR是默认为你自动添加的。
self._impl是select.epoll()和selet.select()两者中的一个。我们稍后将看到Tornado是如何在它们之间进行选择的。
现在让我们来看看实际的主循环,不知何故,这段代码被放在了start()方法中:
def start(self):
& & """Starts the I/O loop.
& & The loop will run until one of the I/O handlers calls stop(), which
& & will make the loop stop after the current event iteration completes.
& & self._running = True
& & while True:
& & [ ... ]
& & & & if not self._running:
& & & & & & break
& & & & [ ... ]
& & & & try:
& & & & & & event_pairs = self._impl.poll(poll_timeout)
& & & & except Exception, e:
& & & & & & if e.args == (4, "Interrupted system call"):
& & & & & & & & logging.warning("Interrupted system call", exc_info=1)
& & & & & & & & continue
& & & & & & else:
& & & & & & & & raise
& & & & # Pop one fd at a time from the set of pending fds and run
& & & & # its handler. Since that handler may perform actions on
& & & & # other file descriptors, there may be reentrant calls to
& & & & # this IOLoop that update self._events
& & & & self._events.update(event_pairs)
& & & & while self._events:
& & & & & & fd, events = self._events.popitem()
& & & & & & try:
& & & & & & & & self._handlers[fd](fd, events)
& & & & & & except KeyboardInterrupt:
& & & & & & & & raise
& & & & & & except OSError, e:
& & & & & & & & if e[0] == errno.EPIPE:
& & & & & & & & & & # Happens when the client closes the connection
& & & & & & & & & & pass
& & & & & & & & else:
& & & & & & & & & & logging.error("Exception in I/O handler for fd %d",
& & & & & & & & & & & & & & & & & fd, exc_info=True)
& & & & & & except:
& & & & & & & & logging.error("Exception in I/O handler for fd %d",
& & & & & & & & & & & & & & & fd, exc_info=True)
poll()方法返回一个形如(fd: events)的键值对,并赋值给event_pairs变量。
由于当一个信号在任何一个事件发生前到来时,C函数库中的poll()方法会返回EINTR(实际是一个值为4的数值),
所以"Interrupted system call"这个特殊的异常需要被捕获。更详细的请查看man poll。
在内部的while循环中,event_pairs中的内容被一个一个的取出,然后相应的处理器会被调用。
pipe 异常在这里默认不进行处理。为了让这个类适应更一般的情况,
在http处理器中处理这个异常是一个更好的方案,但是选择现在这样处理或许是因为更容易一些。
注释中解释了为什么使用字典的popitem()方法,而不是使用更普遍一点的下面这种做法(指使用迭代,译者注):
& for fd, events in self._events.items():
原因很简单,在主循环期间,这个_events字典变量可能会被处理器所修改。
比如remove_handler()处理器。
这个方法把fd(即文件描述符,译者注)从_events字典中取出(extracts,意思是取出并从_events中删除,译者注),
所以即使fd被选择到了,它的处理器也不会被调用(作者的意思是,如果使用for迭代循环_events,
那么在迭代期间_events就不能被修改,否则会产生不可预计的错误,
比如,明明调用了remove_handler()方法删除了某个&fd, handler&键值对,但是该handler还是被调用了,译者注)。
四、(意义不大的)循环结束技巧
怎么让这个主循环停止是很有技巧性的。
self._running变量被用来在运行时从主循环中跳出,处理器可以通过调用stop()方法把它设置为False。
通常情况下,这就能让主循环停止了,但是stop()方法还能被一个信号处理器所调用,
所以,如果
1)主循环正阻塞在poll()方法处,
2)服务端没有接收到任何来自客户端的请求
3)信号没有被OS投递到正确的线程中,你将不得不等待poll()方法出现超时情况后才会返回。
考虑到这些情况并不时常发生,还有poll()方法的默认超时时间只不过是0.2秒,所以这种让主循环停止的方式还算过得去。
但不管怎样,Tornado的开发者为了让主循环停止,还是额外的创建了一个没有名字的管道和对应的处理器,
并把管道的一端放在了轮询文件描述符列表中。
当需要停止时,在管道的另一端随便写点什么,
这能高效率的(意思是马上,译者注)唤醒主循环在poll()方法处的阻塞(貌似Java NIO的Windows实现就用了这种方法,译者注)。
这里节选了一些代码片段:
def __init__(self, impl=None):
& & # Create a pipe that we send bogus data to when we want to wake
& & # the I/O loop when it is idle
& & r, w = os.pipe()
& & self._set_nonblocking(r)
& & self._set_nonblocking(w)
& & self._waker_reader = os.fdopen(r, "r", 0)
& & self._waker_writer = os.fdopen(w, "w", 0)
& & self.add_handler(r, self._read_waker, self.WRITE)
def _wake(self):
& & & & self._waker_writer.write("x")
& & except IOError:
& & & & pass
实际上,上述代码中存在一个bug:
那个只读文件描述符r,虽然是用来读的,但在注册时却附加上了WRITE类型的事件,这将导致该注册实际不会被响应。
正如我先前所说的,用不用专门找个方法其实没什么的,所以我对他们没有发现这个方法不起作用的事实并不感到惊讶。
我在mail list中报告了这个情况,但是尚未收到答复。
五、定时器
另外一个在IOLoop模块中很有特点的设计是对定时器的简单实现。
一系列的定时器会被以是否过期的形式来维护和保存,这用到了python的bisect模块:
def add_timeout(self, deadline, callback):
& & """Calls the given callback at the time deadline from the I/O loop."""
& & timeout = _Timeout(deadline, callback)
& & bisect.insort(self._timeouts, timeout)
& & return timeout
在主循环中,所有过期了的定时器的回调会按照过期的顺序被触发。
poll()方法中的超时时间会动态的进行调整,调整的结果就是如果没有新的客户端请求,
那么下一个定时器就好像没有延迟一样的被触发(意思是如果没有新的客户端的请求,
poll()方法将被阻塞直到超时,这个超时时间的设定会根据下一个定时器与当前时间之间的间隔进行调整,
调整后,超时的时间会等同于距离下一个定时器被触发的时间,这样在poll()阻塞完后,下一个定时器刚好过期,译者注)。
六、选择select方案
让我们现在快速的看一下poll和select这两种select方案的实现代码。
Python已经在版本2.6的标准库中支持了epoll,你可以通过在select模块上使用hasattr()方法检测当前Python是否支持epoll。
如果python版本小于2.6,Tornado将用它自己的基于C的epoll模块。
你可以在tornado/epoll.c文件中找到它源代码。如果最后这也不行(因为epoll不是每个Linux都有的),
它将回退到selec._Select并把_EPoll类包装成和select.epoll一样的api接口。
在你做性能测试之前,请确定你能使用epoll,因为select在有大量文件描述符情况下的效率非常低。
# Choose a poll implementation. Use epoll if it is available, fall back to
# select() for non-Linux platforms
if hasattr(select, "epoll"):
& & # Python 2.6+ on Linux
& & _poll = select.epoll
& & & & # Linux systems with our C module installed
& & & & import epoll
& & & & _poll = _EPoll
& & except:
& & & & # All other systems
& & & & import sys
& & & & if "linux" in sys.platform:
& & & & & & logging.warning("ep using select()")
& & & & _poll = _Select
通过上述阅读,我们的介绍已经涵盖了大部分IOLoop模块。正如广告中介绍的那样,它是一段优雅而又简单的代码。
七、从sockets到流
让我们来看看IOStream模块。它的目的是提供一个对非阻塞式sockets的轻量级抽象,它提供了三个方法:
& .read_until(),从socket中读取直到遇到指定的字符串。这为在读取HTTP头时遇到空行分隔符自动停止提供了方便。
& .read_bytes(),从socket中读取指定数量的字节。这为读取HTTP消息的body部分提供了方便。
& .write(), & & 将指定的buffer写入socket并持续监测直到这个buffer被发送。
所有上述的方法都可以通过异步方式在它们完成时触发回调函数。
write()方法提供了将调用者提供的数据加以缓冲直到IOLoop调用了它的(指write方法的,译者注)处理器的功能,
因为到那时候就说明socket已经为写数据做好了准备:
def write(self, data, callback=None):
& & """Write the given data to this stream.
& & If callback is given, we call it when all of the buffered write
& & data has been successfully written to the stream. If there was
& & previously buffered write data and an old write callback, that
& & callback is simply overwritten with this new callback.
& & self._check_closed()
& & self._write_buffer += data
& & self._add_io_state(self.io_loop.WRITE)
& & self._write_callback = callback
该方法只是用socket.send()来处理WRITE类型的事件,直到EWOULDBLOCK异常发生或者buffer被发送完毕。
读数据的方法和上述过程正好相反。读事件的处理器持续读取数据直到缓冲区被填满为止。
这就意味着要么读取指定数量的字节(如果调用的是read_bytes()),
要么读取的内容中包含了指定的分隔符(如果调用的是read_util()):
def _handle_read(self):
& & & & chunk = self.socket.recv(self.read_chunk_size)
& & except socket.error, e:
& & & & if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
& & & & & & return
& & & & else:
& & & & & & logging.warning("Read error on %d: %s",
& & & & & & & & & & & & & & self.socket.fileno(), e)
& & & & & & self.close()
& & & & & & return
& & if not chunk:
& & & & self.close()
& & & & return
& & self._read_buffer += chunk
& & if len(self._read_buffer) &= self.max_buffer_size:
& & & & logging.error("Reached maximum read buffer size")
& & & & self.close()
& & & & return
& & if self._read_bytes:
& & & & if len(self._read_buffer) &= self._read_bytes:
& & & & & & num_bytes = self._read_bytes
& & & & & & callback = self._read_callback
& & & & & & self._read_callback = None
& & & & & & self._read_bytes = None
& & & & & & callback(self._consume(num_bytes))
& & elif self._read_delimiter:
& & & & loc = self._read_buffer.find(self._read_delimiter)
& & & & if loc != -1:
& & & & & & callback = self._read_callback
& & & & & & delimiter_len = len(self._read_delimiter)
& & & & & & self._read_callback = None
& & & & & & self._read_delimiter = None
& & & & & & callback(self._consume(loc + delimiter_len))
如下所示的_consume方法是为了确保在要求的返回值中不会包含多余的来自流的数据,
并且保证后续的读操作会从当前字节的下一个字节开始(先将流中的数据读到self.read_buffer中,
然后根据要求进行切割,返回切割掉的数据,保留切割后的数据供下一次的读取,译者注):
def _consume(self, loc):
& & result = self._read_buffer[:loc]
& & self._read_buffer = self._read_buffer[loc:]
& & return result
还值得注意的是在上述_handle_read()方法中read buffer的上限——self.max_buffer_size。
默认值是100MB,这似乎对我来说是有点大了。
举个例子,如果一个攻击者和服务端建立了100个连接,并持续发送不带头结束分隔符的头信息,
那么Tornado需要10GB的内存来处理这些请求。即使内存ok,这种数量级数据的复制操作
(比如像上述_consume()方法中的代码)很可能使服务器超负荷。
我们还注意到在每次迭代中_handle_read()方法是如何在这个buffer中搜索分隔符的,
所以如果攻击者以小块形式发送大量的数据,服务端不得不做很多次搜索工作。
归根结底,你应该想要将这个参数和谐掉,除非你真的很希望那样
(Bottom of line, you might want to tune this parameter unless you really expect requests that big&
不大明白怎么翻译,译者注)并且你有足够的硬件条件。
八、HTTP 服务器
有了IOLoop模块和IOStream模块的帮助,写一个异步的HTTP服务器只差一步之遥,
这一步就在httpserver.py中完成。
HTTPServer类它自己只负责处理将接收到的新连接的socket添加到IOLoop中。
该监听型的socket自己也是IOLoop的一部分,正如在listen()方法中见到的那样:
def listen(self, port, address=""):
& & assert not self._socket
& & self._socket = socket.(socket.AF_INET, socket.SOCK_STREAM, 0)
& & flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
& & flags |= fcntl.FD_CLOEXEC
& & fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
& & self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
& & self._socket.setblocking(0)
& & self._socket.bind((address, port))
& & self._socket.listen(128)
& & self.io_loop.add_handler(self._socket.fileno(), self._handle_events,
& & & & & & & & & & & & & & &self.io_loop.READ)
除了绑定给定的地址和端口外,上述代码还设置了"close on exec"和"reuse address"这两个标志位。
前者在应用程序创建子进程的时候特别有用。在这种情况下,我们不想让套接字保持打开的状态
(任何设置了"close on exec"标志位的文件描述符,都不能被使用exec函数方式创建的子进程读写,
因为该文件描述符在exec函数调用前就会被自动释放,译者注)。
后者用来避免在服务器重启的时候发生“该地址以被使用”这种错误时很有用。
正如你所见到的,后备连接所允许的最大数目是128(注意,listen方法并不是你想象中的“开始在128端口上监听”的意思,译者注)。
这意味着如果有128个连接正在等待被accept,那么直到服务器有时间将前面128个连接中的某几个accept了
,新的连接都将被拒绝。我建议你在做性能测试的时候将该参数调高,因为当新的连接被抛弃的时候将直接影响你做测试的准确性。
在上述代码中注册的_handle_events()处理器用来accept新连接,并创建相关的IOStream对象和初始化一个HTTPConnection对象,
HTTPConnection对象负责处理剩下的交互部分:
def _handle_events(self, fd, events):
& & while True:
& & & & try:
& & & & & & connection, address = self._socket.accept()
& & & & except socket.error, e:
& & & & & & if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
& & & & & & & & return
& & & & & & raise
& & & & try:
& & & & & & stream = iostream.IOStream(connection, io_loop=self.io_loop)
& & & & & & HTTPConnection(stream, address, self.request_callback,
& & & & & & & & & & & & & &self.no_keep_alive, self.xheaders)
& & & & except:
& & & & & & logging.error("Error in connection callback", exc_info=True)
可以看到这个方法在一次迭代中accept了所有正在等待处理的连接。
也就是说直到EWOULDBLOCK异常发生while True循环才会退出,
这也就意味着当前没有需要处理accept的连接了。
HTTP头的部分的解析工作开始于HTTPConnection类的构造函数__init()__():
def __init__(self, stream, address, request_callback, no_keep_alive=False,
& & & & & & &xheaders=False):
& & self.stream = stream
& & self.address = address
& & self.request_callback = request_callback
& & self.no_keep_alive = no_keep_alive
& & self.xheaders = xheaders
& & self._request = None
& & self._request_finished = False
& & self.stream.read_until("rnrn", self._on_headers)
如果你很想知道xheaders参数的意义,请看这段注释:
如果xheaders为True,我们将支持把所有请求的HTTP头解析成X-Real-Ip和X-Scheme格式,
而原先我们将HTTP头解析成remote IP和HTTP scheme格式。
这种格式的HTTP头在Tornado运行于反向代理或均衡负载服务器的后端时将非常有用。
_on_headers()回调函数实际用来解析HTTP头,并在有请求内容的情况下通过使用read_bytes()来读取请求的内容部分。
_on_request_body()回调函数用来解析POST的参数并调用应用层提供的回调函数:
def _on_headers(self, data):
& & eol = data.find("rn")
& & start_line = data[:eol]
& & method, uri, version = start_line.split(" ")
& & if not version.startswith("HTTP/"):
& & & & raise Exception("Malformed HTTP version in HTTP Request-Line")
& & headers = HTTPHeaders.parse(data[eol:])
& & self._request = HTTPRequest(
& & & & connection=self, method=method, uri=uri, version=version,
& & & & headers=headers, remote_ip=self.address[0])
& & content_length = headers.get("Content-Length")
& & if content_length:
& & & & content_length = int(content_length)
& & & & if content_length & self.stream.max_buffer_size:
& & & & & & raise Exception("Content-Length too long")
& & & & if headers.get("Expect") == "100-continue":
& & & & & & self.stream.write("HTTP/1.1 100 (Continue)rnrn")
& & & & self.stream.read_bytes(content_length, self._on_request_body)
& & & & return
& & self.request_callback(self._request)
def _on_request_body(self, data):
& & self._request.body = data
& & content_type = self._request.headers.get("Content-Type", "")
& & if self._request.method == "POST":
& & & & if content_type.startswith("application/x-www-form-urlencoded"):
& & & & & & arguments = cgi.parse_qs(self._request.body)
& & & & & & for name, values in arguments.iteritems():
& & & & & & & & values = [v for v in values if v]
& & & & & & & & if values:
& & & & & & & & & & self._request.arguments.setdefault(name, []).extend(
& & & & & & & & & & & & values)
& & & & elif content_type.startswith("multipart/form-data"):
& & & & & & boundary = content_type[30:]
& & & & & & if boundary: self._parse_mime_body(boundary, data)
& & self.request_callback(self._request)
将结果写回客户端的工作在HTTPRequest类中处理,你可以在上面的_on_headers()方法中看到具体的实现。
HTTPRequest类仅仅将写回的工作代理给了stream对象。
def write(self, chunk):
& & assert self._request, "Request closed"
& & self.stream.write(chunk, self._on_write_complete)
未完待续?
通过这篇文章,我已经涵盖了从socket到应用层的所有方面。这应该能给你关于Tornado是如何工作的一个清晰的理解。
总之,我认为Tornado的代码是非常友好的,我希望你也这样认为。
Tornado框架还有很大一部分我们没有探索,比如wep.py(应该是web.py,译者注)
这个实际与你应用打交道的模块,又或者是template engine模块。如果我有足够兴趣的话,
我也会介绍这些部分。可以通过订阅我的RSS feed来鼓励我。
阅读(11881) | 评论(0) | 转发(0) |
相关热门文章
给主人留下些什么吧!~~
请登录后评论。}

我要回帖

更多关于 tornado做游戏五福段 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信