这篇文章的目的在于对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 。
源代码
该项目由 github 托管,你可以通过如下命令获得,虽然通过阅读这篇文章你也可以不需要它是吧。
$ git clone git://github.com/facebook/tornado.git
在 tornado 的子目录中,每个模块都应该有一个.py文件,你可以通过检查他们来判断你是否从已经从代码仓库中完整的迁出了项目。在每个源代码的文件中,你都可以发现至少一个大段落的用来解释该模块的doc string,doc string中给出了一到两个关于如何使用该模块的例子。
IOLoop模块
让我们通过查看 ioloop.py 文件直接进入服务器的核心。这个模块是异步机制的核心。它包含了一系列已经打开的文件描述符和每个描述符的处理器(handlers)。它的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器(一种IO多路复用的实现,其实就是socket众多IO模型中的select模型,在Java中就是NIO,译者注)。
可以通过调用 add_handler() 方法将一个socket加入 IOLoop 中:
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 这个字典类型的变量保存着文件描述符到当该文件描述符准备好时需要调用的方法的映射(在Tornado中,该方法被称为处理器)。然后,文件描述符被注册到epoll列表中。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 字典中取出,所以即使 fd 被选择到了,它的处理器也不会被调用。
def remove_handler(self, fd): """Stop listening for events on fd.""" self._handlers.pop(fd, None) self._events.pop(fd, None) try: self._impl.unregister(fd) except OSError: logging.debug("Error deleting fd from IOLoop", exc_info=True)
(意义不大的)循环结束技巧
怎么让这个主循环停止是很有技巧性的。 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): try: 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() 阻塞完后,下一个定时器刚好过期,译者注)。
self._running = True while True: poll_timeout = 0.2 [ ... ] if self._timeouts: now = time.time() while self._timeouts and self._timeouts[0].deadline <= now: timeout = self._timeouts.pop(0) self._run_callback(timeout.callback) if self._timeouts: milliseconds = self._timeouts[0].deadline - now poll_timeout = min(milliseconds, poll_timeout) [ ... poll ]
选择select方案
让我们现在快速的看一下poll和select这两种select方案的实现代码。Python已经在版本2.6的标准库中支持了epoll,你可以通过在 select 模块上使用 hasattr() 方法检测当前Python是否支持epoll。如果python版本小于2.6,Tornado将用它自己的基于C的epoll模块。你可以在 tornado/epoll.c 文件中找到它源代码。如果最后这也不行(因为epoll不是每个Linux都有的),它将回退到select。 _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 else: try: # Linux systems with our C module installed import epoll _poll = _EPoll except: # All other systems import sys if "linux" in sys.platform: logging.warning("epoll module not found; using select()") _poll = _Select
通过上述阅读,我们的介绍已经涵盖了大部分 IOLoop 模块。正如广告中介绍的那样,它是一段优雅而又简单的代码。
从sockets到流
让我们来看看 IOStream 模块。它的目的是提供一个对非阻塞sockets的轻量级抽象,它提供了三个方法:
- read_until() ,从socket中读取直到遇到指定的字符串。这为在读取HTTP头时遇到空行分隔符自动停止提供了方便。
- read_bytes() ,从socket中读取指定数量的字节。这为读取HTTP消息的body部分提供了方便。
- write() ,将指定的buffer写入socket并持续监测直到这个buffer被发送。
所有上述的方法都可以通过异步方式在它们完成时触发回调函数。
write() 方法提供了将调用者提供的数据加以缓冲直到 IOLoop 调用了它的处理器的功能,因为到那时候就说明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被发送完毕:
def _handle_write(self): while self._write_buffer: try: num_bytes = self.socket.send(self._write_buffer) self._write_buffer = self._write_buffer[num_bytes:] except socket.error, e: if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN): break else: logging.warning("Write error on %d: %s", self.socket.fileno(), e) self.close() return if not self._write_buffer and self._write_callback: callback = self._write_callback self._write_callback = None callback()
读数据的方法和上述过程正好相反。读事件的处理器持续读取数据直到缓冲区被填满为止。这就意味着要么读取指定数量的字节(如果调用的是 read_bytes() ),要么读取的内容中包含了指定的分隔符(如果调用的是 read_util()):
def _handle_read(self): try: 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() 方法是为了确保在要求的返回值中不会包含多余的来自流的数据,并且保证后续的读操作会从当前字节的下一个字节开始:
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中搜索分隔符的,所以如果攻击者以小块形式发送大量的数据,服务端不得不做很多次搜索工作。归根结底,你应该想要调整这个参数,除非你真的需要接收这么多的数据并且能满足硬件条件。
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(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"这两个标志位。前者在应用程序创建子进程的时候特别有用。在这种情况下,我们不想让套接字保持打开的状态。后者用来避免在服务器重启的时候发生“该地址已被使用”这种错误时很有用。
正如你所见到的,后备连接所允许的最大数目是128。这意味着如果有128个连接正在等待被接受,那么直到服务器有时间将前面128个连接中的某几个被接受了,新的连接都将被拒绝。我建议你在做性能测试的时候将该参数调高,因为当新的连接被抛弃的时候将直接影响你做测试的准确性。
在上述代码中注册的 _handle_events() 处理器用来接受新连接,并创建相关的 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)
可以看到这个方法在一次迭代中接受了所有正在等待处理的连接。也就是说直到 EWOULDBLOCK 异常发生时 while True循环才会退出,这也就意味着当前没有需要接受的连接了。
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框架还有很大一部分我们没有探索,比如 web.py 这个实际与你应用打交道的模块,又或者是 template engine 。如果我有足够兴趣的话,我也会介绍这些部分。可以通过订阅我的 RSS feed 来鼓励我。
相关推荐
【标题】:“Tornado源码分析之http服务器篇” 这篇博客主要探讨了Tornado框架的HTTP服务器部分,Tornado是一个Python编写的异步网络库,它最初由FriendFeed团队开发,后来被Facebook收购并开源。Tornado因其高性能...
Tornado Web 服务器概览 详细文档
tornado作为用户产品后台服务器核心框架 redis 保存session数据、短时间房源信息、地域信息等,保存页面缓存数据,提高服务器响应速度 采用前后端完全分离架构,采用ajax异步调用、json数据传输,使后端接口可以...
Python Tornado 框架允许开发者在同一程序中同时运行Web服务器和Socket服务器,并实现它们之间的交互,这在处理实时通信和高性能网络应用时非常有用。本文将详细介绍如何使用Tornado来构建这样的系统。 首先,我们...
【Tornado Asyntool:让TornadoWeb异步运行的利器】 Tornado Asyntool 是一个专门针对 TornadoWeb 框架设计的工具,它的主要目标是简化异步代码的编写和执行,使得开发者能更高效地利用 Tornado 的非阻塞I/O模型。...
通过对Tornado 1.2.0源码的分析,我们可以看到其高效、灵活的设计,这对于理解和优化Web服务器的性能,或者开发基于Tornado的复杂Web应用具有重要的指导意义。学习源码不仅能帮助我们更好地运用这个框架,还能够提升...
Tornado 是一个强大的 Python Web 开发框架,以其高性能和异步网络I/O闻名。它最初由 FriendFeed 团队开发,后来被 Facebook 收购并开源。Tornado 的设计目标是处理大量的并发连接,尤其适合长连接和实时Web应用,如...
**Tornado框架详解** Tornado是一款高性能、异步网络库,...通过深入理解并熟练运用其核心概念和技术,开发者能够构建出高效、可靠的网络服务。在实际项目中,结合最佳实践和持续学习,Tornado将成为你强大的工具箱。
通过研究这个实例源码,初学者可以深入理解Python和Tornado如何协同工作,以及如何构建实际的Web应用程序。同时,对于有经验的开发者来说,这是一个学习和借鉴不同设计模式和最佳实践的好机会。
作为Web服务器,Tornado 有较为出色的抗负载能力,官方用nginx反向代理的方式部署Tornado和其它 Python web应用框架进行对比,结果最大浏览量超过第二名近40%。 性能: Tornado有 着优异的性能。它试图解决C10k问题,...
Tornado Web Server,简称Tornado,是一个开源的、高性能的Web服务器和异步网络库,最初由FriendFeed团队开发,并在2009年开源。后来,Facebook收购了FriendFeed,Tornado也因此成为了Facebook的一部分。Tornado以其...
Tornado是一个Python编写的开源Web框架和异步网络库,最初由FriendFeed公司开发。它支持非阻塞网络I/O,使得能够处理成千上万的开放连接,非常适合长轮询、WebSocket和其他需要长期与每个用户保持连接的应用。...
### Tornado Web 服务器入门详解 #### 一、Tornado Web 服务器简介 Tornado 是一个开源的Python Web 框架以及异步网络库,主要用于编写高性能的Web应用程序和服务。与传统的同步Web框架(如Django)不同,Tornado...
除了基础使用指南,Tornado文档还包括关于异步网络编程的章节,这部分内容将为开发者提供如何利用Tornado进行高性能网络应用开发的深入知识。异步网络编程章节会涵盖Tornado的核心概念,例如协程(coroutines)和...
AsyncTorndb, 用于 Tornado的异步mysql客户端 AsyncTorndb ( 非活动)面向 Tornado的异步mysql客户端,基于 PyMySQL 。文档AsyncTorndb行为几乎与torndb行为相似,但异步。 请参考 torndb 并尝试。要求使用最新版本的...
【标题】:“qt聊天室tornado服务器”是一个项目,它结合了Tornado Web服务器与Qt图形用户界面(GUI)框架,构建了一个实时的在线聊天应用。这个应用允许用户通过网络进行交互,提供了一种基本的即时通讯功能。 ...
这个"**Tornado框架课件资料.rar**"压缩包包含了深入学习Tornado框架所需的重要资源,包括源码分析、文档资料和操作截图,旨在帮助用户全面理解和掌握Python Web开发以及与数据库的交互。 首先,**04源码.rar**可能...
Tornado 是一个强大的、高性能的 Web 服务器和异步网络库,由 Python 语言编写。尽管您的描述提到“tornado 是一款纯 Java 开发的”,但实际上是错误的,Tornado 实际上是 Python 的一个开源项目。它以其非阻塞 I/O ...
【关于服务器——安装配置Tornado】 Tornado 是一个开源的 Python ...Tornado 的强大之处在于它的异步特性和灵活性,可以适应各种复杂的应用场景。在实际开发中,还可以结合数据库、缓存服务等构建完整的 Web 应用。