Tornado IOStream

什么是流Stream呢?

Stream是一种抽象的数据结构,类似水流当在水管中流动时可以从某个地方源源不断流向另一个地方。可以将数据看成是数据流,当敲键盘时是将每个字符依次连接起来形成字符流,并从键盘输入到应用程序,实际上在计算机中键盘被称为标准输入流stdin。如果应用程序将字符一个个输出到显示器,这也可以看成是一个流,这个流叫做标准输出流stdout

Tornado IOStream_第1张图片
标准输入输出流

数据流的特点是数据必须是有序的,而且必须依次读取,或者依次写入,不能够想数组那样随机定位。

在计算机中有些数据流是用来读取数据的,比如从文件中读取数据时,首先需要打开一个文件流,然后从文件流中不断地读取数据。而有些数据流是用来写入数据的,比如向文件中写入数据时,只需要将数据不断往文件流中写进去即可。

使用IOStream封装TCP Socket

由于TCP连接是面向流的连接,这一点与IOStream要表达的概念非常吻合,在使用阻塞Socket处理数据时,如果能借助于IOStream强大的字符串流处理功能,可以简化程序设计。比如说需要在夫服务器和客户端之间类的对象中重载IOStream中ostream输出操作符<<istream输入操作符>>,这样使用时直观并方便进行序列化。因此,从某种意义上来讲,IOStream提供了一种简单的对象序列化的解决方案。


Tornado的IOStream是什么样的呢?

Tornado的核心代码是由ioloop.pyiostream.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_untilread_until_regex等接口注册),是否读取到足够多的数据(由read_bytes注册)等,如果出发了事件则调用对应事件的回调函数(异步事件为future,设置future的result即可调用异步事件的回调函数)。
  • 当文件描述符可写时
    首先IOStream从写缓冲中读取足够的数据,写入到文件描述符fd中。其次能进入当前逻辑说明上一次写入文件描述符的数据已经发送储区了,此时需要逐个检查注册的写事件是否已经完成(各写事件中存储了自己关注的写缓冲区的位置,通过检查该位置判断该事件的数据是否已经发送),如果完成则调用事件的回调函数。
  • 当文件描述符异常时(EPOLLERREPOLLHUP
    将文件描述符从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字符串让服务器断开连接。由于采用的是客户端主动发起连接的行为,因此采用的是主动通过调用IOLooprun_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初始化过程中主要完成四项操作

  1. 绑定对应的Socket
  2. 绑定IOLoop
  3. 创建读缓冲区_read_buffer,一个Python的deque容器。
  4. 创建写缓冲区_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_untilread_bytesread_until_regex等,IOStream在同一时间内只能存在一个读事件。

  • read_bytes(bytes, callback)

read_bytes是在有固定的字节的数据到来的时候回调函数

  • read_until(delimiter, callback)

read_until用于在读取到固定的字符序列结尾后调用回调函数

read_bytesread_until之间的异同点

read_untilread_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操作。

Tornado IOStream_第2张图片
read_until与read_bytes

read_untilread_bytes的区别在于_read_from_buffer过程中截取数据的方法不同,read_until读取到delimiter终止,而read_bytes则读取到num_bytes个字节终止。

read_until_regex相当于delimiter为某一正则表达式的read_until

read_until_close主要用于IOStream流关闭前后的读取,如果调用read_until_closestream已经关闭,那么将会_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.READIOLoop.WRITE状态,handleIOStream对象的_handle_events方法。

def _consume(self, loc)

合并读缓冲区loc个字节,从缓冲区删除并返回这些数据。


IOStream与TCP有什么关系呢?

Tornado中TCPServer和TCPClient可用于实现TCP的服务器和客户端,实际上它们都是对IOStream的简单包装。IOStream是服务器与客户端之间的TCP通道,被动等待创建IOStream的一方是服务器,主动寻找对方创建IOStream的一方则是客户端。在IOStream创建之后,服务器与客户端的操作再无分别,在任何时候都可以通过iostream.write向对方传送内容,或者是通过iostream.read_xxread_开头的方法来接收对方传输来的内容,或者以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)

你可能感兴趣的:(Tornado IOStream)