Tornado-6.0.3源码分析之IOStream类

一、前述

前面讲到的IOLoop类是可以实现对套接字相关的网络读写状态的监听和回调处理。在Tornado的实现里面,对于网络的数据的读写操作,进行了一层封装,以IOStream类,对上提供相应的操作接口。IOStream类并不是实时进行网络数据的读写操作,而是维持有一个内存缓冲区的操作,当需要读取数据时,先在内存缓冲区内查找是否满足读取条件,若满足,则直接从内存缓冲区中将数据读取提交给上层处理;若内存缓冲区无法满足读取条件,而从网络缓冲数据中进行数据读取,至到满足条件或者连接被关闭。若是写操作,则是先写入写缓冲区,当需要写入到网络时,由上层调用fresh接口,则数据被真实的写入到网络中。
这里先看下IOStream相关的类图关系
在这里插入图片描述
这里看到,其实类间关系也比较简单。就是把一个IO相关的共同操作或者接口,定义在BaseIOStream类,然后对于不同的IO操作,各自实现相应的子类,如socket IO相关的就是IOStream类,而管道相关的就实现PipeIOStream类,本人主要看的是socket IO相关的IOStream类;同时BaseIOStream类与IOLoop类是相关联的,因为对于特定的连接的读写状态的监控,是提交给IOLoop类,由IOLoop类在特定的读事件或者写事件被触发时,进行回调相应的处理函数的。下面就来看下这些类的基本功能。

二、BaseIOStream类

对于这个公共基础类,它的实现,体现上可以划分为4部分:

  • 规定子类需要实现的接口;这里就是说具体的怎么读、怎么写,是由子类实现。
  • 读写相关的功能接口;这里主要是一系列的读操作接口read_*()和一个写操作接口write()。
  • 关闭连接相关的接口;这里主要是close()和set_close_callback()
  • 读写状态相关的处理接口;这个主要是在类内部被调用的实现,以实现与IOLoop类的交互。
2.1 规定子类需要实现的接口

对于子类必须需要实现的接口有:

  • fileno():返回文件描述符
  • close_fd():关闭这个数据流,这个接口只能在BaseIOStream类内部被调用,其它地方应该调用的是close接口。
  • write_to_fd():尝试写数据到该文件句柄所代表的流中。
  • read_from_fd():尝试从该文件句柄的数据流中读取数据。
  • get_fd_error():如果该文件句柄出错时,这里返回出错的相关信息。
  • _handle_connect(): 处理连接操作,这个一般是对于网络fd来说,当初始化一个IOStream类时,有可能这个网络相关的文件句柄fd,还没有和服务器或者客户端进行真实的网络连接上,那么在连接成功时,就需要先内部调用_handle_connect处理连接操作,后续才真正的操作读写。

这里只是约定了子类需要实现这些接口,以完成具体的读取操作。比如对于socket数据流,就是对网络数据的读写,而如果是管道数据流,就是管道的读写操作。

2.2 读写相关的功能接口

首先要知道的这是一个异步读写操作,因此是返回一个Future对象。
写操作就一个write()方法,当上层需要等数据被写完才能继续往下操作时,可以await 这个Future,来等待写操作完成。接下来先看下这个方法。

	def write(self, data: Union[bytes, memoryview]) -> "Future[None]":
		# 这里检查一下是否已经被关闭,若是,则抛一个StreamClosedError异常出来。
        self._check_closed()
        if data:
            if (
                self.max_write_buffer_size is not None
                and len(self._write_buffer) + len(data) > self.max_write_buffer_size
            ):
            	# 如果超过了最大的写缓冲,就抛一个写缓冲满了的异常
                raise StreamBufferFullError("Reached maximum write buffer size")
            # 把数据保存到写缓冲区,注意这里的_write_buffer是一个_StreamBuffer()类的实例,
            # 这个类是实现一个特殊功能的缓冲,主要是为了避免大数据量的复制,在保存数据时,
            # 区分以bytearray形式或者以memoryview形式存储。
            self._write_buffer.append(data)
            self._total_write_index += len(data)
        # 创建一个Future对象,并被保存在_write_futures中,这里的_write_futures是一个双边队列
        # 以便于快速进行先进先出的操作。
        future = Future()  # type: Future[None]
        future.add_done_callback(lambda f: f.exception())
        self._write_futures.append((self._total_write_index, future))
        # 这里判断如果当前不是处于连接中的状态就写操作
        # 这是因为对于实例化出BaseIOStream时,相应的socket fd可能还没有完成连接。
        if not self._connecting:
        	# 这里执行写操作,在这里面关键的就是调用了self.write_to_fd方法,还记得刚才说过,
        	# 这个self.write_to_fd方法是必须由子类实现的,即子类才知道要怎么写
            self._handle_write()
            if self._write_buffer:
            	# 可能走到这里主要是因为前面在写操作时,有可能出现暂时写不出去,则在这里设置监听写状态
            	# 在_add_io_state里面self.io_loop.add_handler(self.fileno(), self._handle_events, self._state)
            	# 状态交由io_loop来监听,在可写时,就会回调self._handle_events,然后又调用到self._handle_write()方法,继续写
                self._add_io_state(self.io_loop.WRITE)
            # 持续读状态的监听,以及时发现连接是否被关闭了。
            self._maybe_add_error_listener()
        return future

稍微总结一下写操作,即当上层调用write方法时,先异步返回一个future对象给上层,同时这里去异步执行写数据(_handle_write方法),数据可能被立即写到网络中,也可能暂时写不出去,对于写不出去时,设置一个写状态监听,当可以继续写时,就回调_handle_events方法,这个方法里面就判断是不是写状态被触发了,从而继续调用_handle_write进行数据的写操作。
而读相关的功能,依据不同的读条件,分别实现了不同的读操作:

  • read_until_regex:读取直到满足正则表达式,或者达到最大读取数量
  • read_until:读取直到满足特定的字符,如换行符,或者达到最大读取数量
  • read_bytes:读取指定数量的数据,或者一旦读取到数据就返回,不一定是参数指定的数量。
  • read_into:功能同read_bytes,只是这个方法是把数据直接读到参数指定的buf内存中。
  • read_until_close:读取直到连接被关闭。

不管是哪个读操作实现,本质上都是依赖于内部的_try_inline_read方法,改变的只是满足读取操作的条件不同而已。因此重点是看懂这个_try_inline_read方法即可。

    def _try_inline_read(self) -> None:
        """
        尝试从内存缓冲中完成读取操作;如果满足本次读取,则读取数据给上层调用者,
        并设置在下一次的IOLoop中回调读取网络数据到内存缓冲区中;
        如果不能满足本次读取,则在该sockets上监听读取数据。
        """
        # 依据不同的读取条件,在内存缓冲区中查找是否满足读取
        pos = self._find_read_pos()
        if pos is not None:
        	# 1、内存缓冲区中的数据已经满足本次读取操作,则直接从内存中读取。
        	# 这里面调用到_finish_read方法,故名思义,就是完成读操作。
        	# 有一个区分就是读取的数据是直接复制到上层提供的buf里面(即上层调用的是read_info方法),
        	# 还是把读取的数据放置到future对象的结果里面,返回给上层。
        	# 因此在代码里面可以看到对self._user_read_buffer的判断,走不同的处理。
        	# 通过future_set_result_unless_cancelled(future, result),设置相应的读操作的Future对象已经结束,则上层如果有await 这个Future就可以往下走了。
        	# 在_finish_read的最后调用了_maybe_add_error_listener,这里会继续判断添加读监听,
        	# 以便于下次在上层调用前,先异步读数据到内存缓冲区里面
            self._read_from_buffer(pos)
            return
        self._check_closed()
        # 2、内存缓冲区的数据不足以完成本次读取操作,则读取网络缓冲区的数据。
        # 这里面通过_read_to_buffer方法,调用到bytes_read = self.read_from_fd(buf),进而真正的读取数据数据;
        # 对于网络缓冲区的数据,有两种可能的结果;
        # 第一、在不需要全部读取前(或刚好全部读取时)就已经满足了本次读取操作或者达到读取数量的上限,则结束读取操作;
        # 第二、本次网络缓冲区的数据全部都读取出来了,还是不能满足读取条件,也没达到读取数量的上限;
        pos = self._read_to_buffer_loop()
        if pos is not None:
        	# 3、走这里就代表着,通过读取网络缓冲区的数据,已经满足了本次读操作,从而进一步读取给上层即可。
            self._read_from_buffer(pos)
            return
        # 4、走到这里,代表着原先的内存缓冲区+网络缓冲区的数据都无法满足本次的读取操作,
        # 则设置读状态监听,以便于在有数据到达网络缓冲区时,及时读取。
        # 此时如果在上层调用者中,是await 当前这个读操作的Future对象的话,那么上层被阻塞在读取操作的。
        if not self.closed():
            self._add_io_state(ioloop.IOLoop.READ)
2.3 关闭连接相关的接口

对于关闭连接的操作,相关的接口主要是有两个:

  • set_close_callback:设置关闭时的回调函数
  • close:关闭连接流
    def close(
        self,
        exc_info: Union[
            None,
            bool,
            BaseException,
            Tuple[
                "Optional[Type[BaseException]]",
                Optional[BaseException],
                Optional[TracebackType],
            ],
        ] = False,
    ) -> None:
        if not self.closed():
        	# 1、根据参数类型或者值的不同,保存相应的异常信息
            if exc_info:
                if isinstance(exc_info, tuple):
                    self.error = exc_info[1]
                elif isinstance(exc_info, BaseException):
                    self.error = exc_info
                else:
                    exc_info = sys.exc_info()
                    if any(exc_info):
                        self.error = exc_info[1]
            # 2、如果是读取直到关闭的读操作,则完成本次读取操作
            if self._read_until_close:
                self._read_until_close = False
                self._finish_read(self._read_buffer_size, False)
            elif self._read_future is not None:
                # 3、如果是其它读取条件,且当前还在读状态,则尝试把已有的数据全部返回给上层
                try:
                    pos = self._find_read_pos()
                except UnsatisfiableReadError:
                    pass
                else:
                    if pos is not None:
                        self._read_from_buffer(pos)
            # 4、去掉本句柄的所有网络监听
            if self._state is not None:
                self.io_loop.remove_handler(self.fileno())
                self._state = None
            # 5、调用close_fd真正的关闭本次连接。
            self.close_fd()
            self._closed = True
        # 6、在_signal_closed里面,结束所有的异步操作对象Future, 主要可能包括读取的Future,多个写操作的Future;
        # 也可能是还没有读写操作,还处在连接中的状态的connect的Future。
        # 如果这些Future还没有结束的,通过future.set_exception(StreamClosedError(real_error=self.error))结束掉。
        # 最后设置关闭的回调函数,即之前通过set_close_callback设置的函数。
        self._signal_closed()
2.4 读写状态相关的处理接口

读写状态相关的接口,主要是在内部使用,用于对相应数据流的监听和处理功能。
所涉及到的函数成员有:

  • _handle_events:状态触发后的回调处理函数
  • _add_io_state:添加相应的监听状态

handle_events主要是在相关的读写状态或者其它error状态时,调用相应的self._handle_connect、
self._handle_read、self._handle_write去完成相应的读写或者异常的处理

而_add_io_state主要是如果当前还没有任何的状态监听,设置需要监听的状态(IOLoop.{READ,WRITE})和IOLoop.ERROR;如果当前已有监听的状态,且还没有监听state所对应的状态,就补充添加监听state所表示的状态

三、IOStream类

IOStream类是一个以Socket连接为基础的BaseIOStream的实现类,它支持BaseIOStream的所有读写操作相关的方法,且另外实现了一个connect方法、一个start_tls方法。
先看下它的初始化函数:

	# 必须接受一个socket实例,及其它可能的BaseIOStream的实例化参数。
    def __init__(self, socket: socket.socket, *args: Any, **kwargs: Any) -> None:
        self.socket = socket
        self.socket.setblocking(False)
        super(IOStream, self).__init__(*args, **kwargs)
    # 这个socket实例,在传递给IOStream初始化时,可能已经完成了网络连接操作,也可能还没有进行网络连接;
    # 对于服务端程序来说,这个socket实例是通过socket.accept <socket.socket.accept>返回的客户端的连接实例;
    # 对于客户端程序来说,这个socket实例是通过socket.socket创建的实例,且在传递给IOStream之前,
    # 可能已经完成了与服务端的网络连接,也可能是在后续通过IOStream.connect方法去连接服务端。

既然继承自BaseIOStream类,则必须要完成相应的读写约定的接口。
即完成对下面接口的实现:

  • fileno
  • close_fd
  • get_fd_error
  • read_from_fd
  • write_to_fd
  • _handle_connect

如前面所说,IOStream是以socket连接为基础的,因此这些操作也就是对socket实例的相对应的操作的再次包装而已。

对于IOStream.connect方法的过程主要是:
1、设置当前正在连接的状态。self._connecting = True;生成一个self._connect_future。
2、调用self.socket.connect(address)去进行网络上的连接请求。
3、添加self._add_io_state(self.io_loop.WRITE)可写状态监听。在之前的分析中可以知道,这里在完成连接后,会解发可写,这时io loop会回调相应的处理函数_handle_events方法,在这个方法里面,会判断self._connecting值,并调用_handle_connect方法去处理连接后的事情,在IOStream中,_handle_connect只是去获取判断是否连接出错,去掉相应的连接中的状态。

而start_tls方法主要是能把普通的socket连接转化为ssl相关的连接,从而把IOStream实例转化为SSLIOStream实例而已。但是这个方法的调用时机有一些限制,不能在当前正在读写操作时,进行这样的转化,也不能在IOStream自身的内存缓冲中有数据时进行转化,但是如果是socke连接的网络缓冲区有数据则没有关系。这意味着,操作这样的转化一般是在全部读干净或者全部写干净、或者还没有进行读写操作之前来完成。

其它的SSLIOStream和PipeIOStream只是对于特定的SSL连接和管道连接的约定实现而已。基本上同IOStream,这里不再赘述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章