什么是流Stream
呢?
流Stream
是一种抽象的数据结构,类似水流当在水管中流动时可以从某个地方源源不断流向另一个地方。可以将数据看成是数据流,当敲键盘时是将每个字符依次连接起来形成字符流,并从键盘输入到应用程序,实际上在计算机中键盘被称为标准输入流stdin
。如果应用程序将字符一个个输出到显示器,这也可以看成是一个流,这个流叫做标准输出流stdout
。
数据流的特点是数据必须是有序的,而且必须依次读取,或者依次写入,不能够想数组那样随机定位。
在计算机中有些数据流是用来读取数据的,比如从文件中读取数据时,首先需要打开一个文件流,然后从文件流中不断地读取数据。而有些数据流是用来写入数据的,比如向文件中写入数据时,只需要将数据不断往文件流中写进去即可。
使用IOStream封装TCP Socket
由于TCP连接是面向流的连接,这一点与IOStream要表达的概念非常吻合,在使用阻塞Socket处理数据时,如果能借助于IOStream强大的字符串流处理功能,可以简化程序设计。比如说需要在夫服务器和客户端之间类的对象中重载IOStream中ostream
输出操作符<<
和istream
输入操作符>>
,这样使用时直观并方便进行序列化。因此,从某种意义上来讲,IOStream提供了一种简单的对象序列化的解决方案。
Tornado的IOStream是什么样的呢?
Tornado的核心代码是由ioloop.py
和iostream.py
这两个文件组成的,ioloop.py
提供了一个循环主要用于处理IO事件,iostream
封装了一个非阻塞的Socket。
IOStream对Tornado的高效性起了非常大的作用,IOStream封装了Socket的非阻塞IO的读写操作。简单来说或,当客户端连接建立后,服务器与客户端的请求响应都是基于IOStream的,也就是说IOStream是用来处理连接的。
Tornado中IOStream是对Socket读写操作进行的封装,分别提供读、写缓冲区实现对Socket的异步读写。当Socket被accpet
之后HTTPServer的_handle_connection
会被回调并初始化IOStream对象,进一步通过IOStream提供的接口完成Socket的读写操作。
IOStream是建立在IOLoop基础之上的,IOStream与IOLoop交互的过程主要从读写数据两方面来分析。
- IOStream读数据
将Socket添加到IOLoop中并设置回调函数,在回调函数中从Socket中读取数据,并检查是否接收到足够的数据,如果没有接收完则需要保存当前的数据,直至读取完毕为止。
- IOStream写数据
将Socket添加至IOLoop中并设置回调函数,在回调函数中向Socket写数据,如果数据比较多则需要分多次去写。
IOStream主要的主要是让各组件无需与IOLoop直接交互,IOStream帮助各组件将Socket添加到IOLoop中,并设置回调函数,读取并保存数据,直到所有数据都接收完毕为止,或是向Socket写入数据,直到写完所有数据,写入完成后则调用其他组件并设置回调函数。
IOStream的事件循环
IOStream是基于IOLoop的,创建IOStream时必须给定一个文件描述符,IOStream将该文件描述符添加到IOLoop的IO事件中并设置回调函数_handle_events
,_handle_events
函数中实现了IOStream的事件循环。IOStream同样可以看作是一个事件循环,它提供两类事件循环:读完成和写完成。
_handle_events
函数中判断文件描述符fd
的事件分为三种情况:
- 当文件内描述符可读时
IOStream从文件描述符fd
中读取数据并保存到自己的数据缓冲中,每次读取到新的数据后,IOStream都会检查是否出发了自己管理的事件。比如是否读取到某一特定的数据(由read_until
、read_until_regex
等接口注册),是否读取到足够多的数据(由read_bytes
注册)等,如果出发了事件则调用对应事件的回调函数(异步事件为future,设置future的result即可调用异步事件的回调函数)。 - 当文件描述符可写时
首先IOStream从写缓冲中读取足够的数据,写入到文件描述符fd
中。其次能进入当前逻辑说明上一次写入文件描述符的数据已经发送储区了,此时需要逐个检查注册的写事件是否已经完成(各写事件中存储了自己关注的写缓冲区的位置,通过检查该位置判断该事件的数据是否已经发送),如果完成则调用事件的回调函数。 - 当文件描述符异常时(
EPOLLERR
、EPOLLHUP
)
将文件描述符从IOLoop中删除并设置自己为关闭状态,上层再向自己读写数据时触发异常。
例如:对于IOStream的整体认识是负责IO读写并回调
$ vim server.py
创建一个继承自TCPServer
类的实例并监听指定地址的端口,然后启动服务器、启动消息循环,服务器开始运行。此时,如果有客户端连接过来,Tornado会创建一个IOStream
,然后调用handle_stream
方法,调用时传入两个参数iostream
和客户端地址。服务器每收到一段20个字符以内的消息,将其反序回传,如果收到exit
字符串则断开连接。需要注意的是断开连接不用yield
调用。无论是谁断开连接,连接双方都会各自出触发一个StreamClosedError
错误。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from tornado.options import define, options
from tornado.ioloop import IOLoop
from tornado.tcpserver import TCPServer
from tornado import gen
define("ip", type=str, default="0.0.0.0")
define("port", type=int, default=8000)
class Connection(object):
def __init__(self, stream, address):
self.stream = stream
self.address = address
@gen.coroutine
def start(self, server):
while True:
# 循环从Stream中读取消息,每个消息以exit结尾。
future = self.stream.read_until("exit".encode())
message = yield future
message = message.decode()
# 如果仅发送exit则说明客户端消息已经发送完毕
if message == "exit":
self.stream.close()
server.close(self)
break
else:
#将接收到的消息以异步的方式写回客户端
future = self.stream.write(message.encod e())
yield future
class Server(TCPServer):
def __init__(self):
super().__init__()
self._conns = set()
# TCPServer已经建立好IOStream,仅需使用。
def handle_stream(self, stream, address):
conn = Connection(stream, address)
self._conns.add(conn)
return conn.start(self)
def close(self, conn):
self._conns.remove(conn)
if __name__ == "__main__":
server = Server()
server.listen(options.port, options.ip)
IOLoop.current().start()
$ vim client.py
使用TCPClient
无需继承,只需要调用connect
连接方法连接到服务器,此时就会返回IOStream
对象。客户端向服务器发送一些字符串,服务器会反序发回。最后发出exit
字符串让服务器断开连接。由于采用的是客户端主动发起连接的行为,因此采用的是主动通过调用IOLoop
的run_sync
。在run_sync
中Tornado会先启动消息循环,执行目标函数之后再结束消息循环。
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
import socket, time
HOST = "127.0.0.1"
PORT = 8000
BUFSIZE = 1024
sdf = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sdf.connect((HOST, PORT))
sdf.send("hello world exit".encode())
data = sdf.recv(BUFSIZE).decode()
print(data)
sdf.send("whats up exit".encode())
data = sdf.recv(BUFSIZE).decode()
print(data)
sdf.send("thanks exit".encode())
time.sleep(3)
sdf.close()
服务器接收请求的流程是什么样的呢?
当客户端与服务器连接建立后,服务器会产生一个对该连接对应的Socket,同时就将该Socket封装至IOStream实例中,这也就是IOStream的初始化过程。
由于Tornado是基于IO多路复用的,因此会将Socket进行注册register
,事件为RERADABLE
。
当该Socket事件发生时,也就 意味着有数据从连接发送到了系统缓冲区中,此时需要将chunk
读取到内存中为其开启的_read_buffer
读缓冲区中,注意在IOStream
中使用deque
作为buffer
。
读缓冲_read_buffer
和写缓冲_write_buffer
本质上都是Tornado进程开启的一段用来存储数据的内存空间。
chunk
是客户端发送过来的请求数据,服务器接收到chunk
接收到之后是需要做进一步的操作的,比如人chunk
中可能包含多个请求,如何将请求分离,由于每个请求的报文首部的结束符是b'\r\n\r\n
,因此可使用read_util
来分离请求并设置回调callback
,同时会将分离的请求数据从读缓冲_read_buffer
中移除。
接着会将回调callback
及其参数(被分离的请求数据)添加到IOLoop.__callbacks
中,等待下一次IOLoop的执行,届时会迭代_callbacks
并执行回调函数。
由于Tornado是水平触发的,如果读取完毕一段chunk
后系统缓冲区中依然还有是数据,那么下一次的epoll.poll()
依然会返回该Socket。
IOStream类
Tornado中的IOStream类封装了Socket的非阻塞IO的读写操作,IOStream是建立在IOLoop基础之上的。
from tornado.iostream import IOStream
Tornado中IOStream初始化过程中主要完成四项操作
- 绑定对应的Socket
- 绑定IOLoop
- 创建读缓冲区
_read_buffer
,一个Python的deque
容器。 - 创建写缓冲区
_write_buffer
,一个Python的deque
容器。
IOStream类__init__
初始化属性
def __init__():
# 封装Socket
self.socket = socket
# 设置Socket为非阻塞
self.setblocking(False)
# 获取当前IOLoop
self.io_loop = io_loop or ioloop.IOLoop.current()
# 读缓存,collections.deque类型
self._read_buffer = deque()
# 写缓冲,collections.deque类型
self._write_buffer = deque()
# 读取到指定字节数据时或指定指定标志字符串时执行回调函数
self._read_callback = None
# 发送完写缓冲_write_buffer的数据时执行的回调函数
self._write_callback = None
IOStream提供的主要功能接口也包括四个
class IOStream(object):
def read_until(self, delimiter, callback):
def read_bytes(self, num_bytes, callback, streaming_callback=None):
def read_until_regex(self, regex, callback):
def read_until_close(self, callback, streaming_callback=None):
def write(self, data, callback=None):
读接口
IOStream的读数据接口主要分为read_until
、read_bytes
、read_until_regex
等,IOStream在同一时间内只能存在一个读事件。
- read_bytes(bytes, callback)
read_bytes
是在有固定的字节的数据到来的时候回调函数
- read_until(delimiter, callback)
read_until
用于在读取到固定的字符序列结尾后调用回调函数
read_bytes
与read_until
之间的异同点
read_until
和read_bytes
是最常见的读接口,它们的工作过程都是先注册读事件结束时调用的回调函数,然后调用_try_inline_read
方法。_try_inline_read
首先尝试_read_from_buffer
即从上一次的读缓冲区中获取数据,如果有数据则直接调用self._run_callback(callback, self._consume(data_length))
执行回调函数,_consume
消耗掉了_read_buffer
中的数据。否则_read_buffer
之前没有未读数据,则先通过_read_to_buffer
将数据从Socket读入到_read_buffer
中,然后再执行_read_from_buffer
操作。
read_until
和read_bytes
的区别在于_read_from_buffer
过程中截取数据的方法不同,read_until
读取到delimiter
终止,而read_bytes
则读取到num_bytes
个字节终止。
read_until_regex
相当于delimiter
为某一正则表达式的read_until
。
read_until_close
主要用于IOStream流关闭前后的读取,如果调用read_until_close
时stream
已经关闭,那么将会_consume
掉_read_buffer
中的所有是数据。否则_read_until_close
标志位设置为True
,注册_streaming_callback
回调函数调用_add_io_state
添加io_loop.READ
状态。
写接口
写数据接口主要是write
,可同时存在多个,只要IOStream的写缓冲区足够。每次添加时创建一个future
并记录该事件要写入的数据在写缓冲区中的位置,也就时加入数据后IOStream的_total_write_index
。当每次有数据发送后根据该位置信息判断是否写入结束,判断IOStream的_total_write_done_index
是否超过了事件的数据位置。
- write(data)
write
主要用于异步写,也就是将数据拷贝到应用层的缓冲区,再由IOLoop
下层统一调度。
write
首先将data
按照数据块大小WRITE_BUFFER_CHUNK_SIZE
分块写入write_buffer
,然后调用handle_write
向Socket中发送数据。
def _handle_events(self, fd, events)
通常为IOLoop对象add_handler
方法传入的回调函数,由IOLoop的事件机制来进行调度。
def _add_io_state(self, state)
IOLoop对象的handler
注册IOLoop.READ
或IOLoop.WRITE
状态,handle
为IOStream
对象的_handle_events
方法。
def _consume(self, loc)
合并读缓冲区loc
个字节,从缓冲区删除并返回这些数据。
IOStream与TCP有什么关系呢?
Tornado中TCPServer和TCPClient可用于实现TCP的服务器和客户端,实际上它们都是对IOStream的简单包装。IOStream是服务器与客户端之间的TCP通道,被动等待创建IOStream的一方是服务器,主动寻找对方创建IOStream的一方则是客户端。在IOStream创建之后,服务器与客户端的操作再无分别,在任何时候都可以通过iostream.write
向对方传送内容,或者是通过iostream.read_xx
即read_
开头的方法来接收对方传输来的内容,或者以iostream.close
关闭连接。
例如:
$ vim server.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from tornado.options import define, options
from tornado.ioloop import IOLoop
from tornado.tcpserver import TCPServer
from tornado import gen, iostream
define("port", type=int, default=8000)
class Server(TCPServer):
@gen.coroutine
def handle_stream(self, stream, address):
try:
while True:
msg = yield stream.read_bytes(20, partial=True)
stream.write(str(msg).encode())
yield stream.write(msg[::-1])
if msg == "exit":
stream.close()
except iostream.StreamClosedError:
pass
if __name__ == "__main__":
server = Server()
server.listen(options.port)
server.start()
IOLoop.current().start()
$ vim client.py
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
from tornado.ioloop import IOLoop
from tornado.tcpclient import TCPClient
from tornado.options import define, options
from tornado import gen, iostream
define("ip", type=str, default="127.0.0.1")
define("port", type=int, default=8000)
@gen.coroutine
def Trans():
stream = yield TCPClient().connect(options.ip, options.port)
try:
while True:
data = input("enter: ")
yield stream.write(str(data).encode())
back = yield stream.read_bytes(20, partial=True)
print("back: %s" % back)
msg = yield stream.read_bytes(20, partial=True)
print("msg: %s" % msg)
if data=="exit":
break
except iostream.StreamClosedError:
pass
if __name__ == "__main__":
IOLoop.current().run_sync(Trans)