python tornado TCPserver异步协程实例
项目所用知识点
- tornado
- socket
- tcpserver
- 协程
- 异步
tornado tcpserver源码抛析
在tornado的tcpserver文件中,实现了TCPServer这个类,他是一个单线程的,非阻塞的tcp 服务。
为了与上层协议(在tornado中就是HTTPServer)交互,TCPServer提供了一个接口:handle_stream, 要求其子类必需实现该方法,该方法就是主要用来处理应用层逻辑的。
我们可以通过下面代码倒入模块查看源码
from tornado.tcpserver import TCPServer
源码中给了好多解释,先把源码注释贴进来
class TCPServer(object):
‘’‘
1.要想用TCPserver,我给你提供你一个接口handle_stream,子类中必须要有这个方法
2.他已经给我们举出了例子
3.往下他给咱们介绍了几种启动方法,而我用的listen()方法启动看起来简单明了
’‘’
r"""A non-blocking, single-threaded TCP server. To use `TCPServer`, define a subclass which overrides the `handle_stream`
method. For example, a simple echo server could be defined like this:: from tornado.tcpserver import TCPServer
from tornado.iostream import StreamClosedError
from tornado import gen class EchoServer(TCPServer):
@gen.coroutine
def handle_stream(self, stream, address):
while True:
try:
data = yield stream.read_until(b"\n")
yield stream.write(data)
except StreamClosedError:
break To make this server serve SSL traffic, send the ``ssl_options`` keyword
argument with an `ssl.SSLContext` object. For compatibility with older
versions of Python ``ssl_options`` may also be a dictionary of keyword
arguments for the `ssl.wrap_socket` method.:: ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"),
os.path.join(data_dir, "mydomain.key"))
TCPServer(ssl_options=ssl_ctx) `TCPServer` initialization follows one of three patterns: 1. `listen`: simple single-process:: server = TCPServer()
server.listen(8888)
IOLoop.current().start() 2. `bind`/`start`: simple multi-process:: server = TCPServer()
server.bind(8888)
server.start(0) # Forks multiple sub-processes
IOLoop.current().start() When using this interface, an `.IOLoop` must *not* be passed
to the `TCPServer` constructor. `start` will always start
the server on the default singleton `.IOLoop`. 3. `add_sockets`: advanced multi-process:: sockets = bind_sockets(8888)
tornado.process.fork_processes(0)
server = TCPServer()
server.add_sockets(sockets)
IOLoop.current().start() The `add_sockets` interface is more complicated, but it can be
used with `tornado.process.fork_processes` to give you more
flexibility in when the fork happens. `add_sockets` can
also be used in single-process servers if you want to create
your listening sockets in some way other than
`~tornado.netutil.bind_sockets`. .. versionadded:: 3.1
The ``max_buffer_size`` argument. .. versionchanged:: 5.0
The ``io_loop`` argument has been removed.
"""
自己仔细看该类的其他方法
def listen(self, port, address=""):
"""Starts accepting connections on the given port. This method may be called more than once to listen on multiple ports.
`listen` takes effect immediately; it is not necessary to call
`TCPServer.start` afterwards. It is, however, necessary to start
the `.IOLoop`.
"""
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets) def add_sockets(self, sockets):
"""Makes this server start accepting connections on the given sockets. The ``sockets`` parameter is a list of socket objects such as
those returned by `~tornado.netutil.bind_sockets`.
`add_sockets` is typically used in combination with that
method and `tornado.process.fork_processes` to provide greater
control over the initialization of a multi-process server.
"""
for sock in sockets:
self._sockets[sock.fileno()] = sock
self._handlers[sock.fileno()] = add_accept_handler(
sock, self._handle_connection) def add_socket(self, socket):
"""Singular version of `add_sockets`. Takes a single socket object."""
self.add_sockets([socket]) def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128,
reuse_port=False):
"""Binds this server to the given port on the given address. To start the server, call `start`. If you want to run this server
in a single process, you can call `listen` as a shortcut to the
sequence of `bind` and `start` calls. Address may be either an IP address or hostname. If it's a hostname,
the server will listen on all IP addresses associated with the
name. Address may be an empty string or None to listen on all
available interfaces. Family may be set to either `socket.AF_INET`
or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise
both will be used if available. The ``backlog`` argument has the same meaning as for
`socket.listen <socket.socket.listen>`. The ``reuse_port`` argument
has the same meaning as for `.bind_sockets`. This method may be called multiple times prior to `start` to listen
on multiple ports or interfaces. .. versionchanged:: 4.4
Added the ``reuse_port`` argument.
"""
sockets = bind_sockets(port, address=address, family=family,
backlog=backlog, reuse_port=reuse_port)
if self._started:
self.add_sockets(sockets)
else:
self._pending_sockets.extend(sockets) def start(self, num_processes=1):
"""Starts this server in the `.IOLoop`. By default, we run the server in this process and do not fork any
additional child process. If num_processes is ``None`` or <= 0, we detect the number of cores
available on this machine and fork that number of child
processes. If num_processes is given and > 1, we fork that
specific number of sub-processes. Since we use processes and not threads, there is no shared memory
between any server code. Note that multiple processes are not compatible with the autoreload
module (or the ``autoreload=True`` option to `tornado.web.Application`
which defaults to True when ``debug=True``).
When using multiple processes, no IOLoops can be created or
referenced until after the call to ``TCPServer.start(n)``.
"""
assert not self._started
self._started = True
if num_processes != 1:
process.fork_processes(num_processes)
sockets = self._pending_sockets
self._pending_sockets = []
self.add_sockets(sockets) def stop(self):
"""Stops listening for new connections. Requests currently in progress may still continue after the
server is stopped.
"""
if self._stopped:
return
self._stopped = True
for fd, sock in self._sockets.items():
assert sock.fileno() == fd
# Unregister socket from IOLoop
self._handlers.pop(fd)()
sock.close() def handle_stream(self, stream, address):
"""Override to handle a new `.IOStream` from an incoming connection. This method may be a coroutine; if so any exceptions it raises
asynchronously will be logged. Accepting of incoming connections
will not be blocked by this coroutine. If this `TCPServer` is configured for SSL, ``handle_stream``
may be called before the SSL handshake has completed. Use
`.SSLIOStream.wait_for_handshake` if you need to verify the client's
certificate or use NPN/ALPN. .. versionchanged:: 4.2
Added the option for this method to be a coroutine.
"""
raise NotImplementedError() def _handle_connection(self, connection, address):
if self.ssl_options is not None:
assert ssl, "Python 2.6+ and OpenSSL required for SSL"
try:
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()
else:
raise
except socket.error as err:
# If the connection is closed immediately after it is created
# (as in a port scan), we can get one of several errors.
# wrap_socket makes an internal call to getpeername,
# which may return either EINVAL (Mac OS X) or ENOTCONN
# (Linux). If it returns ENOTCONN, this error is
# silently swallowed by the ssl module, so we need to
# catch another error later on (AttributeError in
# SSLIOStream._do_ssl_handshake).
# To test this behavior, try nmap with the -sT flag.
# https://github.com/tornadoweb/tornado/pull/750
if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL):
return connection.close()
else:
raise
try:
if self.ssl_options is not None:
stream = SSLIOStream(connection,
max_buffer_size=self.max_buffer_size,
read_chunk_size=self.read_chunk_size)
else:
stream = IOStream(connection,
max_buffer_size=self.max_buffer_size,
read_chunk_size=self.read_chunk_size) future = self.handle_stream(stream, address)
if future is not None:
IOLoop.current().add_future(gen.convert_yielded(future),
lambda f: f.result())
except Exception:
app_log.error("Error in connection callback", exc_info=True)
通过方法名就能看出来,而且开头已经给出实例怎么去用,所以这个就不一一解释了,我自己的用法如下
from tornado.tcpserver import TCPServer
from tornado.iostream import IOStream, StreamClosedError
from tornado import gen
from tornado.ioloop import IOLoop
import struct class ProxyServer(TCPServer):
def __init__(self, *args, **kwargs):
super(ProxyServer, self).__init__(*args, **kwargs)
self.devices = dict() @gen.coroutine
def handle_stream(self, stream, address):
pass if __name__ == "__main__":
server = ProxyServer()
server.listen(1234)
IOLoop.current().start()
具体步骤来分析 一下
TCPServer执行过程
1.server = ProxyServer()创建tcpserver对象,该步骤仅仅做了一个初始化操作
def __init__(self, io_loop=None, ssl_options=None, max_buffer_size=None, read_chunk_size=None):
self.io_loop = io_loop
self.ssl_options = ssl_options
self._sockets = {} # fd -> socket object 用来存储文件描述符与socket对象的映射关系
self._pending_sockets = []
self._started = False
self.max_buffer_size = max_buffer_size # 最大缓冲长度
self.read_chunk_size = read_chunk_size # 每次读的chunk大小 # 校验ssl选项.
if self.ssl_options is not None and isinstance(self.ssl_options, dict):
if 'certfile' not in self.ssl_options:
raise KeyError('missing key "certfile" in ssl_options') if not os.path.exists(self.ssl_options['certfile']):
raise ValueError('certfile "%s" does not exist' % self.ssl_options['certfile'])
if ('keyfile' in self.ssl_options and not os.path.exists(self.ssl_options['keyfile'])):
raise ValueError('keyfile "%s" does not exist' % self.ssl_options['keyfile'])
2.想都不要想肯定是开启socket
步骤是执行server.listen(1234)的时候,
def listen(self, port, address=""):
"""Starts accepting connections on the given port. This method may be called more than once to listen on multiple ports.
`listen` takes effect immediately; it is not necessary to call
`TCPServer.start` afterwards. It is, however, necessary to start
the `.IOLoop`.
"""
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets)
3.看下listen里面有调用bind_sockets()方法,来看下该方法
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=_DEFAULT_BACKLOG, flags=None, reuse_port=False):
if reuse_port and not hasattr(socket, "SO_REUSEPORT"):
raise ValueError("the platform doesn't support SO_REUSEPORT") sockets = []
if address == "":
address = None
# address family参数指定调用者期待返回的套接口地址结构的类型。它的值包括四种:AF_UNIX,AF_INET,AF_INET6和AF_UNSPEC。
# AF_UNIX用于同一台机器上的进程间通信
# 如果指定AF_INET,那么函数就不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。
# AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。
# 如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回
if not socket.has_ipv6 and family == socket.AF_UNSPEC: # 如果系统不支持ipv6
family = socket.AF_INET
if flags is None:
flags = socket.AI_PASSIVE
bound_port = None
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)):
af, socktype, proto, canonname, sockaddr = res
if (sys.platform == 'darwin' and address == 'localhost' and af == socket.AF_INET6 and sockaddr[3] != 0):
# Mac OS X在“localhost”的getaddrinfo结果中包含一个链接本地地址fe80 :: 1%lo0。
# 但是,防火墙不了解这是一个本地地址,并且会提示访问。 所以跳过这些地址。
continue
try:
sock = socket.socket(af, socktype, proto)
except socket.error as e:
# 如果协议不支持该地址
if errno_from_exception(e) == errno.EAFNOSUPPORT:
continue
raise
# 为 fd 设置 FD_CLOEXEC 标识
set_close_exec(sock.fileno())
if os.name != 'nt': # 非windows
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if reuse_port:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
if af == socket.AF_INET6:
# On linux, ipv6 sockets accept ipv4 too by default,
# but this makes it impossible to bind to both
# 0.0.0.0 in ipv4 and :: in ipv6. On other systems,
# separate sockets *must* be used to listen for both ipv4
# and ipv6. For consistency, always disable ipv4 on our
# ipv6 sockets and use a separate ipv4 socket when needed.
#
# Python 2.x on windows doesn't have IPPROTO_IPV6.
if hasattr(socket, "IPPROTO_IPV6"):
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) # 自动端口分配,端口=None
# 应该绑定在IPv4和IPv6地址上的同一个端口上
host, requested_port = sockaddr[:2]
if requested_port == 0 and bound_port is not None:
sockaddr = tuple([host, bound_port] + list(sockaddr[2:]))
# 设置socket为非阻塞
sock.setblocking(0)
sock.bind(sockaddr)
bound_port = sock.getsockname()[1]
sock.listen(backlog)
sockets.append(sock)
return sockets
4.接下来执行的是add_sockets()方法
def add_sockets(self, sockets):
if self.io_loop is None:
self.io_loop = IOLoop.current() # 获取IOLoop实例对象 for sock in sockets:
self._sockets[sock.fileno()] = sock
add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop)
可以看到里面调用了add_accept_handler方法,来我们进去看看该方法干啥了
5.探析add_accept_handler方法
def add_accept_handler(sock, callback, io_loop=None):
if io_loop is None: # 获取IOLoop实例对象
io_loop = IOLoop.current() def accept_handler(fd, events):
# 我们处理回调时可能会有许多的连接等待建立; 为了防止其他任务的饥饿,我们必须限制我们一次接受的连接数。
# 理想情况下,我们接受在处理回调过程中等待的连接数,但此可能会对负载产生不利影响。
# 相反,我们使用listen backlog作为我们可以合理接受的连接数的。
for i in xrange(_DEFAULT_BACKLOG): # _DEFAULT_BACKLOG默认为128
try:
connection, address = sock.accept()
except socket.error as e:
# _ERRNO_WOULDBLOCK 与EAGAIN相同,表示再尝试一下,很多情况下是因为资源不足,或者条件未达成
# 当某个子进程与客户端建立了连接,其他子进程再次尝试与该客户端建立连接时就会产生该错误
if errno_from_exception(e) in _ERRNO_WOULDBLOCK:
return
# ECONNABORTED表示有一个连接,在他处于等待被服务端accept的时候主动关闭了。
if errno_from_exception(e) == errno.ECONNABORTED:
continue
raise
callback(connection, address)
io_loop.add_handler(sock, accept_handler, IOLoop.READ) # 为socket注册handler:当发生READ事件时运行accept_handler函数。
欣欣然我们来到了最后一步
6.IOLoop.current().start(),然我们看下源码
def start(self):
try:
while True:
callbacks = self._callbacks
self._callbacks = []
due_timeouts = []
# 将时间已到的定时任务放置到due_timeouts中,过程省略
for callback in callbacks: # 执行callback
self._run_callback(callback)
for timeout in due_timeouts: # 执行定时任务
if timeout.callback is not None:
self._run_callback(timeout.callback)
callbacks = callback = due_timeouts = timeout = None # 释放内存
# 根据情况设置poll_timeout的值,过程省略
if not self._running: # 终止ioloop运行时,在执行完了callback后结束循环
breaktry:
event_pairs = self._impl.poll(poll_timeout)
except Exception as e:
if errno_from_exception(e) == errno.EINTR: # 系统调用被信号处理函数中断,进行下一次循环
continue
else:
raise
self._events.update(event_pairs)
while self._events:
fd, events = self._events.popitem() # 获取一个fd以及对应事件
try:
fd_obj, handler_func = self._handlers[fd] # 获取该fd对应的事件处理函数
handler_func(fd_obj, events) # 运行该事件处理函数
except (OSError, IOError) as e:
if errno_from_exception(e) == errno.EPIPE: # 当客户端关闭连接时会产生EPIPE错误
pass
# 其他异常处理已经省略
fd_obj = handler_func = None # 释放内存空间
这一步想了解更多去参考这篇文章http://www.cnblogs.com/MnCu8261/p/6730691.html
代码实例
目前公司有这么一个需求,iphonex--server--ue4,面对两个客户端,达到iphonex把数据给ue4,server起一个代理作用,需求大概就是这样,具体实现代码如下
from tornado.tcpserver import TCPServer
from tornado.iostream import IOStream, StreamClosedError
from tornado import gen
from tornado.ioloop import IOLoop
import struct class ProxyServer(TCPServer):
def __init__(self, *args, **kwargs):
super(ProxyServer, self).__init__(*args, **kwargs)
self.devices = dict() @gen.coroutine
def handle_stream(self, stream, address):
device = yield stream.read_bytes(1)
if device == b"\x0a":
self.handle_iphonex_stream(stream, address)
elif device == b"\x0b":
self.handle_ue4_stream(stream, address)
else:
print("protocol error.") @gen.coroutine
def handle_iphonex_stream(self, stream, address):
yield stream.write(b"\x00")
print("iphonex") # uuid
rlen = yield stream.read_bytes(4)
rlen = struct.unpack(">I", rlen)[0]
uuid = yield stream.read_bytes(rlen)
uuid = uuid.decode()
yield stream.write(b"\x00")
print(uuid) # keys
rlen = yield stream.read_bytes(4)
rlen = struct.unpack(">I", rlen)[0]
keys = yield stream.read_bytes(rlen)
keys = keys.decode()
yield stream.write(b"\x00")
print(keys) # save
self.devices[uuid] = {'keys': keys} # data
keys = keys.split(',')
fmt = "%df" % len(keys) while True:
try:
data = yield stream.read_bytes(struct.calcsize(fmt))
except StreamClosedError:
print 'iphonex is closed'
break
pdata = struct.unpack(fmt, data)
print(pdata) ue4stream = self.devices[uuid].get('ue4')
if ue4stream:
try:
yield ue4stream.write(data)
except Exception as e:
self.devices[uuid]['ue4'] = None
print('request for %s closed' % uuid) @gen.coroutine
def handle_ue4_stream(self, stream, address):
yield stream.write(b"\x00")
print("ue4") # uuid
rlen = yield stream.read_bytes(4)
rlen = struct.unpack(">I", rlen)[0]
uuid = yield stream.read_bytes(rlen)
uuid = uuid.decode()
print(uuid) if self.devices.get(uuid):
yield stream.write(b"\x00")
else:
yield stream.write(b"\x01")
raise Exception # send keys
keys = self.devices[uuid].get('keys')
stream.write(struct.pack(">I", len(keys)))
stream.write(keys.encode()) valid = yield stream.read_bytes(1)
if valid == b'x\01':
print('keys not support.')
raise Exception self.devices[uuid]['ue4'] = stream if __name__ == "__main__":
server = ProxyServer()
server.listen(1234)
IOLoop.current().start()
请点赞转发帮助身边更多的人
python tornado TCPserver异步协程实例的更多相关文章
- Python爬虫进阶 | 异步协程
一.背景 之前爬虫使用的是requests+多线程/多进程,后来随着前几天的深入了解,才发现,对于爬虫来说,真正的瓶颈并不是CPU的处理速度,而是对于网页抓取时候的往返时间,因为如果采用request ...
- python爬虫--多任务异步协程, 快点,在快点......
多任务异步协程asyncio 特殊函数: - 就是async关键字修饰的一个函数的定义 - 特殊之处: - 特殊函数被调用后会返回一个协程对象 - 特殊函数调用后内部的程序语句没有被立即执行 - 协程 ...
- Python核心框架tornado的异步协程的2种方式
什么是异步? 含义 :双方不需要共同的时钟,也就是接收方不知道发送方什么时候发送,所以在发送的信息中就要有提示接收方开始接收的信息,如开始位,同时在结束时有停止位 现象:没有共同的时钟,不考虑顺序来了 ...
- Python 简易的异步协程使用方法
代码 import asyncio async def ex(id, n): print(id+" start") await asyncio.sleep(n/2) print(i ...
- python——asyncio模块实现协程、异步编程
我们都知道,现在的服务器开发对于IO调度的优先级控制权已经不再依靠系统,都希望采用协程的方式实现高效的并发任务,如js.lua等在异步协程方面都做的很强大. Python在3.4版本也加入了协程的概念 ...
- 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习
我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...
- python协程与异步协程
在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...
- python爬虫---单线程+多任务的异步协程,selenium爬虫模块的使用
python爬虫---单线程+多任务的异步协程,selenium爬虫模块的使用 一丶单线程+多任务的异步协程 特殊函数 # 如果一个函数的定义被async修饰后,则该函数就是一个特殊的函数 async ...
- Python中异步协程的使用方法介绍
1. 前言 在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞.比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序一直在等待网站响应,最后 ...
随机推荐
- install-scp
centos6 minilize system will not scp command install: yum -y install openssh-clients and another mac ...
- centos7服务的管理
centos7上服务管理 author:headsen chen 2017-10-16 16:03:53 1,启动服务(每条都可以) systemctl start httpd ...
- JavaSE中常见的工具类
Arrays 用来操作数组, 常用方法是 sort()和toString()方法 Iterator 我们常说的迭代器就是这哥们,专门用来操作集合元素的工具类 常用方法是: hasNex()t和next ...
- 目标检测网络之 YOLOv2
YOLOv1基本思想 YOLO将输入图像分成SxS个格子,若某个物体 Ground truth 的中心位置的坐标落入到某个格子,那么这个格子就负责检测出这个物体. 每个格子预测B个bounding b ...
- poj 2503 查字典
Description You have just moved from Waterloo to a big city. The people here speak an incomprehensib ...
- 【Darwin】 越狱后玩耍IPhone系统
玩耍IOS系统 大家都知道IOS是自Mac OS修改而来的.而Mac OS和IOS的共同核心是Darwin,其基于FreeBSD发展而来,整体而言也是个类Unix系统.之前把自己的手机越狱之后正好开始 ...
- Django之ORM模型
ORM介绍 对象关系映射(Object Relational Mapping,简称ORM)模式的作用是在关系型数据库与业务实体对象之间进行映射,这使得我们不需要再去和复杂的SQL语句打交道,只需要简单 ...
- [react 基础篇]——React.createClass()方法同时创建多个组件类
react 组件 React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件.React.createClass 方法就用于生成一个组件类 一个组 ...
- 从零部署Spring boot项目到云服务器(准备工作)
自己的博客终于成功部署上线了,回过头来总结记录一下整个项目的部署过程! 测试地址:47.94.154.205:8084 注:文末有福利! 一.Linux下应用Shell通过SSH连接云服务器 //ss ...
- Java基础学习笔记三 Java基础语法
Scanner类 Scanner类属于引用数据类型,先了解下引用数据类型. 引用数据类型的使用 与定义基本数据类型变量不同,引用数据类型的变量定义及赋值有一个相对固定的步骤或格式. 数据类型 变量名 ...