如何理解python tornadoo

tornado(1)
tornado属于单线程单进程,阻塞、非阻塞只针对线程层面来说,简单理解为:
阻塞:tornado实例不能在接受其他API的请求
非阻塞:tornado实例可以接受其他API的请求
同步、异步,属于业务逻辑上的东西。假设:
你的业务逻辑API需要1,2,3步才能完成,按照同步的方法:
同步执行时,这个API只有完成第三步后,才能再次接受其他的请求。
异步执行时,比如把2,3步合并为4,把4交给内核调度,tornado的IOLoop循环来异步执行,yield func4(callback)注册一个回调函数,有结果通知/处理(见例1)。后面的代码会继续执行。那么这个API就是异步了,API在处理1之后,线程就属于空闲状态了,所以线程现在处于非阻塞,可以接受其他的请求。
但是如果你要等待4的结果返回后,也就是result=yield func4(callback),这时候,API后面的流程就变同步了,都在等待result(见例2)。但线程不会被阻塞。可以接受其他请求。
&& 上面提到的func4必须是异步的写法,也就是需要用到IOLoop的东西,而不是随便一个函数都可以,因为它需要交给IOLoop来循环执行。这样才是异步函数。
& & & & &例子1:
&异步回调,非阻塞:线程不被阻塞,API逻辑是异步执行的
class asyn_callback(BaseHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
client = AsyncHTTPClient()
client.fetch(&&)
def func(self, response):
print response
异步等待,非阻塞:线程不被阻塞,但API业务逻辑同步执行的,因为它要等结果
class asyn_callback(BaseHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
client = AsyncHTTPClient()
result = yield client.fetch(&&)
print result
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:5814次
排名:千里之外
原创:10篇
(3)(4)(7)(2)<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
您的访问请求被拒绝 403 Forbidden - ITeye技术社区
您的访问请求被拒绝
亲爱的会员,您的IP地址所在网段被ITeye拒绝服务,这可能是以下两种情况导致:
一、您所在的网段内有网络爬虫大量抓取ITeye网页,为保证其他人流畅的访问ITeye,该网段被ITeye拒绝
二、您通过某个代理服务器访问ITeye网站,该代理服务器被网络爬虫利用,大量抓取ITeye网页
请您点击按钮解除封锁&氓之蚩蚩Tornado(1)
氓之蚩蚩Python(6)
这里直接引用原文:
Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections.
An asynchronous networking library (IOLoop and IOStream), which serve as the building blocks for the HTTP components and can also be used to implement other protocols.
对于几种网络IO模型的理解可以参考:
个人觉得前两篇阐述得比较准确,第三篇有些词容易产生异议,比如异步阻塞I/O说的其实就是IO multiplexing,而异步非阻塞I/O则是Asynchronous I/O,使用后面的术语不易混淆些。这里引用几句话:
同步IO与异步IO:是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。
阻塞IO与非阻塞IO:简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,否则就可以理解为非阻塞。
这里有个比较容易疑惑的地方,这里的异步说进程触发IO操作后,直接返回,那么是否可以理解为异步IO一定是非阻塞IO呢?是这样的,这是四种不同的IO模型,它们有各自的定义和特点,不是包含非包含的关系。另外确定是否为异步,要看操作系统是否将用户数据主动的拷贝到用户(服务器)空间去供用户使用,而不是用户(服务器)主动监听。
我的理解:tornado基于epoll模型,属于IO multiplexing,并非异步IO模型,也不是非阻塞IO模型,事实上tornado里面说的asynchronous and non-blocking主要是针对函数和连接(socket)而言的:
An asynchronous function returns before it is finished.
In the context of Tornado we generally talk about blocking in the context of network I/O, although all kinds of blocking are to be minimized.
其实我们平常常说的同步异步并不一定是在说IO模型。
二、它如何工作
我用的tornado版本为3.1.1,python版本为2.7.3,下面是一个简单的例子:
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler, asynchronous
class MainHandler(RequestHandler):
@asynchronous
def get(self):
self.finish(&Hello, world&)
if __name__ == &__main__&:
http_server = HTTPServer(Application([(r&/&, MainHandler),]))
http_server.listen(8888)
IOLoop.instance().start()
这样一个高性能的web服务就完成了,核心只有一个IOLoop和一个HTTPServer,我们从上往下看,先看HTTPServer。
HTTPServer继承TCPServer,它只负责处理将接收到的新连接的socket添加到IOLoop中。
def listen(self, port, address=&&):
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets)
def add_sockets(self, sockets):
if self.io_loop is None:
self.io_loop = IOLoop.current()
for sock in sockets:
self._sockets[sock.fileno()] = sock
add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop)首先将HTTPServer这个监听型socket添加到IOLoop中,添加完成后在accept_handler接受新连接,接受到新连接后调用self._handle_connection。def add_accept_handler(sock, callback, io_loop=None):
if io_loop is None:
io_loop = IOLoop.current()
def accept_handler(fd, events):
while True:
connection, address = sock.accept()
except socket.error as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
if e.args[0] == errno.ECONNABORTED:
callback(connection, address)
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)在_handle_connection中创建一个IOStream对象,传给handle_stream,并且在handle_stream中初始化一个HTTPConnection对象。def _handle_connection(self, connection, address):
if self.ssl_options is not None:
assert ssl, &Python 2.6+ and OpenSSL required for SSL&
connection = ssl_wrap_socket(connection,
self.ssl_options,
server_side=True,
do_handshake_on_connect=False)
except ssl.SSLError as err:
if err.args[0] == ssl.SSL_ERROR_EOF:
return connection.close()
except socket.error as err:
if err.args[0] in (errno.ECONNABORTED, errno.EINVAL):
return connection.close()
if self.ssl_options is not None:
stream = SSLIOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size)
stream = IOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size)
self.handle_stream(stream, address)
except Exception:
app_log.error(&Error in connection callback&, exc_info=True)
def handle_stream(self, stream, address):
HTTPConnection(stream, address, self.request_callback, self.no_keep_alive, self.xheaders, self.protocol)到HTTPConnection初始化时,新的连接已经接受,并初始化了IOStream对象,就可以开始读请求过来的数据了,读完之后交给_header_callback,实际是交给_on_headers解析数据。
在_on_handlers解析完请求数据后创建HTTPRequest对象,并将该对象作为参数传给最初的那个request_callback(即在main方法中传给HttpServer的Application)的__call__方法,到此整个请求流程就很清晰了。def __init__(self, stream, address, request_callback, no_keep_alive=False,
xheaders=False, protocol=None):
self.stream = stream
self.address = address
self.address_family = stream.socket.family
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self.protocol = protocol
self._clear_request_state()
self._header_callback = stack_context.wrap(self._on_headers)
self.stream.set_close_callback(self._on_connection_close)
self.stream.read_until(b&\r\n\r\n&, self._header_callback)这里还啰嗦几句,Application的__call__方法首先会调用该请求对应Handler的父类RequestHandler的_execute方法,这里的几个逻辑解释一下。
首先执行self._when_complete(self.prepare(), self._execute_method),会执行self.prepare(),即正式处理请求之前的逻辑,基类中是空实现,开发者可根据需要在自己的Handler中实现,该方法正常返回一个Future对象。
如果未实现self.prepare()则直接调用self._execute_method,反之则通过IOLoop循环执行完self.prepare()后再调用self._execute_method,即调用开发者写的Handler里面的get或post等请求逻辑。
开发者逻辑执行完成后执行self.finish()。def _execute(self, transforms, *args, **kwargs):
&&&Executes this request with the given output transforms.&&&
self._transforms = transforms
if self.request.method not in self.SUPPORTED_METHODS:
raise HTTPError(405)
self.path_args = [self.decode_argument(arg) for arg in args]
self.path_kwargs = dict((k, self.decode_argument(v, name=k))
for (k, v) in kwargs.items())
# If XSRF cookies are turned on, reject form submissions without
# the proper cookie
if self.request.method not in (&GET&, &HEAD&, &OPTIONS&) and \
self.application.settings.get(&xsrf_cookies&):
self.check_xsrf_cookie()
self._when_complete(self.prepare(), self._execute_method)
except Exception as e:
self._handle_request_exception(e)
def _when_complete(self, result, callback):
if result is None:
callback()
elif isinstance(result, Future):
if result.done():
if result.result() is not None:
raise ValueError(&#39;Expected None, got %r&#39; % result)
callback()
# Delayed import of IOLoop because it&#39;s not available
# on app engine
from tornado.ioloop import IOLoop
IOLoop.current().add_future(
result, functools.partial(self._when_complete,
callback=callback))
raise ValueError(&Expected Future or None, got %r& % result)
except Exception as e:
self._handle_request_exception(e)
def _execute_method(self):
if not self._finished:
method = getattr(self, self.request.method.lower())
self._when_complete(method(*self.path_args, **self.path_kwargs),
self._execute_finish)
def _execute_finish(self):
if self._auto_finish and not self._finished:
self.finish()再看IOLoop,这个模块是异步机制的核心,它包含了一系列已经打开的文件描述符和每个描述符的处理器(handlers)。
针对不同的平台,tornado提供了多种IOLoop实现方式,包括select、epoll、kqueue,其实就是IO多路复用的实现,这些都继承PollIOLoop,PollIOLoop是对IOLoop的一个基本封装。
IOLoop的功能是选择那些已经准备好读写的文件描述符,然后调用它们各自的处理器。
可以通过调用add_handler()方法将一个socket加入IOLoop中,上面的HTTPServer监听socket就是通过add_handler添加到IOLoop中去的:
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
来具体看下add_handler这个方法,为fd注册handler来接收event,事件包括READ、WRITE、ERROR三种,默认为ERROR,当注册的事件触发时,将会调用handler(fd, events)函数。
self._impl是前面说的select、epoll、kqueue其中一种的实例,register函数只是根据事件类型将fd放到不同的事件集合中去。
def add_handler(self, fd, handler, events):
self._handlers[fd] = stack_context.wrap(handler)
self._impl.register(fd, events | self.ERROR)接下来IOLoop就要开始工作了,看start()方法(代码比较长,只保留了主要部分):
def start(self):
self._running = True
while True:
poll_timeout = 3600.0
with self._callback_lock:
callbacks = self._callbacks
self._callbacks = []
for callback in callbacks:
self._run_callback(callback)
[...通过_timeouts来优化poll_timeout...]
if self._callbacks:
poll_timeout = 0.0
if not self._running:
event_pairs = self._impl.poll(poll_timeout)#取出数据已准备好的事件,当poll有结果时才会返回,否则一直阻塞,直到poll_timeout
except Exception as e:
if (getattr(e, &#39;errno&#39;, None) == errno.EINTR or
(isinstance(getattr(e, &#39;args&#39;, None), tuple) and
len(e.args) == 2 and e.args[0] == errno.EINTR)):
# 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()
self._handlers[fd](fd, events)#执行handler,即执行netutil中的accept_handler方法,接着会接受socket,调用TCPServer中的_handle_connection方法,该方法会创建一个IOStream实例进行异步读写
except (OSError, IOError) as e:
if e.args[0] == errno.EPIPE:
# Happens when the client closes the connection
app_log.error(&Exception in I/O handler for fd %s&,
fd, exc_info=True)
except Exception:
app_log.error(&Exception in I/O handler for fd %s&,
fd, exc_info=True)
这里看下SelectIOLoop的_impl(即_Select)的poll:
def poll(self, timeout):
readable, writeable, errors = select.select(self.read_fds, self.write_fds, self.error_fds, timeout)#有结果才返回,否则一直阻塞,直到poll_timeout
events = {}
for fd in readable:
events[fd] = events.get(fd, 0) | IOLoop.READ
for fd in writeable:
events[fd] = events.get(fd, 0) | IOLoop.WRITE
for fd in errors:
events[fd] = events.get(fd, 0) | IOLoop.ERROR
return events.items()
至此,可以清晰得看到tornado是如何工作的了!核心就两点:使用epoll模型,保证高并发时接受请求的高效性;将可能阻塞的方法都放到IOLoop里面去循环执行,即程序上的异步,保证CPU的高利用率。这样高并发时,tornado一直在接受请求并一直在努力顺畅的工作,性能自然就上去了。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:7139次
排名:千里之外
原创:19篇浅析tornado协程运行原理 - 推酷
浅析tornado协程运行原理
去年有一段时间一直在研究各种python协程框架,包括gevent, asyncio, tornado。阅读tornado的源码还是两个多月前的事了,一直想写一篇文章出来整理整理,但不知道从何处开始下笔。如果贴上一段段源码,然后通过语言来描述各种流程,这种类型的文章网上也有不少,况且这样子的讲解对于读者来说可能会比较乏味。
我希望我对于源码分析的博文能够通过贴上更容易理解的图(当然也会有一些代码来辅助讲解),这样的分享对读者来说会更加容易读懂,也更有价值。对自己要求高了,反而更难下笔,在试图画图的过程中,发现其实有好多细节自己也没有琢磨透,导致在如何组织这幅流程图的问题上斟酌了好久,不过好在最后终于捯饬出了一张自己觉得还算及格的流程图,作完图的时候我感觉比起之前刚阅读完代码时候的理解又上了一个层次。
tornado执行协程的方式有很多,但协程内部的运行原理没什么区别,这篇文章以 IOLoop 中的 run_sync 函数作为入口进行介绍。在开始进行分析之前,先把流程图贴上,其中的细节后面会通过代码辅助的方式一一讲解。
在理解tornado运行原理的过程中,我是通过写一个demo,然后在源码中到处打断点,然后调试的方式,一遍遍走,到最后慢慢地理解。顺便也把我的demo代码贴上吧(看过我之前的一篇译文的读者可能会发现,这个demo是从那儿仿照过来的)。
import randomimport timefrom tornado import genfrom tornado.ioloop import IOLoop@gen.coroutinedef get_url(url):
wait_time = random.randint(1, 4)
yield gen.sleep(wait_time)
print('URL {} took {}s to get!'.format(url, wait_time))
raise gen.Return((url, wait_time))@gen.coroutinedef outer_coroutine():
before = time.time()
coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']]
result = yield coroutines
after = time.time()
print(result)
print('total time: {} seconds'.format(after - before))if __name__ == '__main__':
IOLoop.current().run_sync(outer_coroutine)
有兴趣的读者可以自己去执行一下玩玩,输出类似于这样:
URL URL1 took 1s to get!
URL URL2 took 2s to get!
URL URL3 took 2s to get!
[('URL1', 1), ('URL2', 2), ('URL3', 2)]
total time: 2. seconds
起初我以为调用协程后,返回的是一个生成器对象,毕竟 gen.coroutine 装饰在一个函数或者生成器上。看了源码发现,其实每次调用一个协程,它在获取了生成器对象之后,同时又对它执行了 next 操作来获取生成器内部yield出来的值,这个可以是一个值,当然也可以是一个由内部协程嵌套调用返回的future对象。
# gen.pydef _make_coroutine_wrapper(func, replace_callback):
@functools.wraps(func)
def wrapper(*args, **kwargs):
future = TracebackFuture()
result = func(*args, **kwargs)
# 省略n个except
if isinstance(result, types.GeneratorType):
orig_stack_contexts = stack_context._state.contexts
yielded = next(result)
# 如果func内部有yield关键字,result是一个生成器
# 如果func内部又调用了其它协程,yielded将会是由嵌套协程返回的future对象
# 省略n个except
Runner(result, future, yielded)
return future
future = None
future.set_result(result)
return future
return wrapper
我觉得 Future 在tornado中是一个很奇妙的对象,它是一个穿梭于协程和调度器之间的信使。提供了回调函数注册(当异步事件完成后,调用注册的回调)、中间结果保存、嵌套协程唤醒父协程(通过Runner实现)等功能。Coroutine和Future是一一对应的,可以从上节gen.coroutine装饰器的实现中看到。每调用一个协程,表达式所返回的就是一个Future对象,它所表达的意义为: 这个协程的内部各种异步逻辑执行完毕后,会把结果保存在这个Future中,同时调用这个Future中指定的回调函数 ,而future中的回调函数是什么时候被注册的呢?那就是当前——你通过调用协程,返回了这个future对象的时候:
我们看看demo代码中run_sync的实现:
# ioloop.py IOLoopdef run_sync(self, func, timeout=None):
future_cell = [None]
def run():
result = func()
except Exception:
future_cell[0] = TracebackFuture()
future_cell[0].set_exc_info(sys.exc_info())
if is_future(result):
future_cell[0] = result
future_cell[0] = TracebackFuture()
future_cell[0].set_result(result)
self.add_future(future_cell[0], lambda future: self.stop())
self.add_callback(run)
if timeout is not None:
timeout_handle = self.add_timeout(self.time() + timeout, self.stop)
self.start()
if timeout is not None:
self.remove_timeout(timeout_handle)
if not future_cell[0].done():
raise TimeoutError('Operation timed out after %s seconds' % timeout)
return future_cell[0].result()
代码中先给 IOLoop 注册一个回调函数,等下个事件循环再执行内部定义的run函数。在run中通过 result = func() 执行协程 outer_coroutine ,result则是该协程对应的future对象。如果这个时候不对future作任何操作,最后这个future完成后也不会执行任何回调。所以在源码中通过 add_future 给这个future添加回调函数,也就是 self.stop() ,表明这个协程执行完毕后触发的操作是退出事件循环。
其实IOLoop::add_future这个函数的命名会有些奇怪,刚读代码还不知道它是干嘛的(给IOLoop添加future是什么鬼?如果说是add_callback那还容易理解),看了add_future的实现就明白了:
# ioloop.py IOLoopdef add_future(self, future, callback):
&&&Schedules a callback on the ``IOLoop`` when the given
`.Future` is finished.
The callback is invoked with one argument, the
`.Future`.
assert is_future(future)
callback = stack_context.wrap(callback)
future.add_done_callback(
lambda future: self.add_callback(callback, future))
它并不会给IOLoop添加future(也没有什么意义),它只是给这个future添加回调函数而已,而这个回调函数是当这个future完成以后给IOLoop添加一个回调函数(有点绕,哈哈~给IOLoop添加的回调函数在这里就是stop)。 因此当一个future完成以后,到最后future的回调函数真正被执行将会隔着一个IOLoop的事件循环,而不是马上会被执行 。
如果说tornado是一辆车,那么Runner对象就是它的发动机,由它来调度各种协程来完成异步事件的操作。Coroutine和Runner也是一一对应的,每个Coroutine都是由一个Runner实例去执行的。协程包装着生成器(当然也有可能是函数,本文考虑比较复杂的协程嵌套调用的情况),在生成器内部,也有可能会调用其它的协程,从而把内部协程的future对象yield出来,这个runner就会通过调用返回的方式( future = next(gen) )接到内部出来的future,并把它纳入执行的loop中,先是 handle_yielded ,再是 run (中间会隔着一个或者多个IOLoop的事件循环,因此图中是用虚线表示的)。
调度器中有两个比较重要的函数: handle_yielded 和 run ,先来看 handle_yielded :
# gen.py Runnerdef handle_yield(self, yielded):
# Lists containing YieldPoints re
# other lists are handled via multi_future in convert_yielded.
if (isinstance(yielded, list) and
any(isinstance(f, YieldPoint) for f in yielded)):
yielded = Multi(yielded)
elif (isinstance(yielded, dict) and
any(isinstance(f, YieldPoint) for f in yielded.values())):
yielded = Multi(yielded)
if isinstance(yielded, YieldPoint):
self.future = convert_yielded(yielded)
except BadYieldError:
self.future = TracebackFuture()
self.future.set_exc_info(sys.exc_info())
if not self.future.done() or self.future is moment:
self.io_loop.add_future(
self.future, lambda f: self.run())
return False
return True
在runner中, handle_yielded 用于处理generator返回的内部协程future对象。因为协程处理的大部分是异步的事件,所以内部协程yield出来的future对象状态多半还是处于未完成。这个时候收到该future的Runner所能做的也仅仅只是注册一个回调函数而已(上面源码的最后几行)。
再来看看 run :
# gen.py Runnerdef run(self):
&&&Starts or resumes the generator, running until it reaches a
yield point that is not ready.
if self.running or self.finished:
self.running = True
while True:
future = self.future
if not future.done():
self.future = None
orig_stack_contexts = stack_context._state.contexts
exc_info = None
value = future.result()
except Exception:
self.had_exception = True
exc_info = sys.exc_info()
if exc_info is not None:
yielded = self.gen.throw(*exc_info)
exc_info = None
yielded = self.gen.send(value)
if stack_context._state.contexts is not orig_stack_contexts:
self.gen.throw(
stack_context.StackContextInconsistentError(
'stack_context inconsistency (probably caused '
'by yield within a &with StackContext& block)'))
except (StopIteration, Return) as e:
self.finished = True
self.future = _null_future
if self.pending_callbacks and not self.had_exception:
# If we ran cleanly without waiting on all callbacks
# raise an error (really more of a warning).
# had an exception then some callbacks may have been
# orphaned, so skip the check in that case.
raise LeakedCallbackError(
&finished without waiting for callbacks %r& %
self.pending_callbacks)
self.result_future.set_result(getattr(e, 'value', None))
self.result_future = None
self._deactivate_stack_context()
except Exception:
self.finished = True
self.future = _null_future
self.result_future.set_exc_info(sys.exc_info())
self.result_future = None
self._deactivate_stack_context()
if not self.handle_yield(yielded):
self.running = False
run函数中的注释很好得诠释了它的作用,它就是不断地给传入Runner的generator执行next或者send操作(next或send都会让生成器继续运行,区别就是send会传一个参数进去),直到generator返回的future对象状态还未完成,需要等待异步响应,这个时候它会调用handle_yielded。
异步响应来了以后,就会调用这个run,为什么呢?因为在 handle_yielded 中给这个future注册了回调函数,回调函数就是 run 函数。然后在run函数中执行send(value),让这个生成器继续运行,如此往复循环,直到generator退出。
generator退出就代表着这个Runner引擎所跑的Coroutine完成了,然后再给这个Coroutine所对应的Future对象执行set_result操作,表示这个协程的Future已完成了,可以执行它的回调函数了。
这个回调函数对于outer_coroutine的future来说就是执行IOLoop的stop操作。对于inner_coroutine的future来说就是outer_coroutine对应的Runner的run操作。这句话很绕,但是要是真读懂了,相信对于它的运行原理也就了解的差不多了。
IOLoop是一个很常见的模块,就是多路复用IO机制,好多项目中都有这一块的封装,原理都差不多。也可以参考
中的loop模块,它也是用python实现的基于多种不同操作系统io多路复用的封装。tornado的ioloop也是类似的,记录了一个个文件描述符和handler的pair,每当有io事件发生,就会调用该文件描述符对应的handler。如果这个handler是对future执行set_result操作,那连锁地就会执行Runner中的run,从而进入Runner的运行循环中,直到需要等待下一个异步事件,然后再向ioloop注册事件。。。如此循环往复。
我讲的可能词不达意,毕竟我自己也是看了好多遍源码,才一步步理解清晰的。读者也不妨运行我的例子,逐步调试看看,说不定会有意想不到的收获。如果我哪些地方讲的欠妥当,也欢迎大家来指正。
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 tornado websocket 的文章

 

随机推荐