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,這裏不再贅述。

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