一、前述
前面講到的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,這裏不再贅述。