[連載 1] 如何將協議規範變成開源庫系列文章之 WebSocket

這是系列文章的第一篇,也是很是重要的一篇,但願你們能讀懂我想要表達的意思。html

系列文章開篇概述

相對於其餘編程語言來講,Python 生態中最突出的就是第三方庫。任何一個及格的 Python 開發者都使用過至少 5 款第三方庫。前端

就爬蟲領域而言,必將用到的例如網絡請求庫 Requests、網頁解析庫 Parsel 或 BeautifulSoup、數據庫對象關係映射 Motor 或 SQLAlchemy、定時任務 Apscheduler、爬蟲框架 Scrapy 等。python

這些開源庫的使用方法想必你們已經很是熟練了,甚至還修煉出了本身的一套技巧,平常工做中敲起鍵盤確定也是噠噠噠的響。git

可是你有沒有想過:github

  • 那個神奇的功能是如何實現的?
  • 這個功能背後的邏輯是什麼?
  • 爲何要這樣作而不是選擇另外一種寫法?
  • 編寫這樣的庫須要用到哪些知識?
  • 這個論點是否有明確的依據?


若是你從未這樣想過,那說明你還沒到達應該「渡劫」的時機;若是你曾提出過 3 個以上的疑問,那說明你即將到達那個重要的關口;若是你經常這麼想,並且也嘗試着尋找對應的答案,那麼恭喜你,你如今正處於「渡劫」的關口之上。web


偶有羣友會拋出這樣的問題:初級工程師、中級工程師、高級工程師如何界定?數據庫

這個問題有兩種不一樣的觀點,第一個是看工做職級,第二個則是看我的能力。工做職級是一個浮動很大的參照物,例如阿里巴巴的高級研發和我司的高級研發,職級名稱都是「高級研發」,但能力可能會有很大的差距。編程

我的能力又如何評定呢?後端

難不成看代碼寫的快仍是寫的慢嗎?瀏覽器

固然不是!

我的能力應當從廣度和深度兩個方面進行考量,這並無一個明確的標準。當兩人能力差別很大的時候,外人能夠輕鬆的分辨孰強孰弱。

本身怎樣分辨我的能力的進與退呢?

這就回到了上面提到的那些問題:WHO WHAT WHERE WHY WHEN HOW?

我想經過這篇文章告訴你,不要作那個用庫用得很熟練的人,要作那個創造庫的人。計算機世界如此吸引人,就是由於咱們能夠在這個世界裏盡情創造。

你想作一個創造者嗎?

若是不想,那如今你就能夠關掉瀏覽器窗口,回到 Hub 的世界裏。

內容介紹

這是一套系列文章,這個系列將爲你們解讀常見庫(例如 WebSocket、HTTP、ASCII、Base6四、MD五、AES、RSA)的協議規範和對應的代碼實現,幫助你們「知其然,知其因此然」。

目標

此次咱們要學習的是 WebSocket 協議規範和代碼實現,也能夠理解爲從 0 開始編寫 aiowebsocket 庫。至於爲何選擇它,那大概是由於全世界沒有比我更熟悉的它的人了。

我是 aiowebsocket 庫的做者,我花了 7 天編寫這個庫。寫庫的過程,讓我深入體會到造輪子和駕駛的區別,也讓我有了飛速的進步。我但願用連載系列文章的形式幫助你們從駕駛者轉換到創造者,擁有「編程思考」。

前置條件

WebSocket 是一種在單個 TCP 鏈接上進行全雙工通訊的協議,它的出現使客戶端和服務器之間的數據交換變得更加簡單。下圖描述了雙端交互的流程:

WebSocket 一般被應用在實時性要求較高的場景,例如賽事數據、股票證券、網頁聊天和在線繪圖等。WebSocket 與 HTTP 協議徹底不一樣,但一樣被普遍應用。

不管是後端開發者、前端開發者、爬蟲工程師或者信息安全工做者,都應該掌握 WebSocket 協議的知識。

我曾經發表過幾篇關於 WebSocket 的文章:

其中,《【嚴選-高質量文章】開發者必知必會的 WebSocket 協議》介紹了協議規範的相關知識。這篇文章的內容大致以下:

  • WebSocket 協議來源
  • WebSocket 協議的優勢
  • WebSocket 協議規範
  • 一些實際代碼演示

若是沒有掌握 WebSocket 協議的朋友,我建議先去閱讀這篇文章,尤爲是對 WebSocket 協議規範介紹的那部分。

要想將協議規範 RFC6455 變成開源庫,第一步就是要熟悉整個協議規範,因此你須要閱讀【嚴選-高質量文章】開發者必知必會的 WebSocket 協議。固然,有能力的同窗直接閱讀 RFC6455 也何嘗不可。

接着還須要瞭解編程語言中內置庫 Socket 的基礎用法,例如 Python 中的 socket 或者更高級更潮的 StreamsTransports and Protocols。若是你是 Go 開發者、Rust 開發者,請查找對應語言的內置庫。

假設你已經熟悉了 RFC6455,你應該知道 Frame 打包和解包的時候須要用到位運算,正好我以前寫過位運算相關的文章 7分鐘全面瞭解位運算

至於其它的,現用現學吧!

Python 網絡通訊之 Streams

WebSocket,也能夠理解爲在 WEB 應用中使用的 Socket,這意味着本篇將會涉及到 Socket 編程。上面提到,Python 中與 Socket 相關的有 socket、Streams、Transports and Protocols。其中 socket 是同步的,而另外兩個是異步的,這倆屬於你常聽到的 asyncio。

Socket 通訊過程

Socket 是端到端的通訊,因此咱們要搞清楚消息是怎麼從一臺機器發送到另外一臺機器的,這很重要。假設通訊的兩臺機器爲 Client 和 Server,Client 向 Server 發送消息的過程以下圖所示:

Client 經過文件描述符的讀寫 API read & write 來訪問操做系統內核中的網絡模塊爲當前套接字分配的發送 send buffer 和接收 recv buffer 緩存。

Client 進程寫消息到內核的發送緩存中,內核將發送緩存中的數據傳送到物理硬件 NIC,也就是網絡接口芯片 (Network Interface Circuit)。

NIC 負責將翻譯出來的模擬信號經過網絡硬件傳遞到服務器硬件的 NIC。

服務器的 NIC 再將模擬信號轉成字節數據存放到內核爲套接字分配的接收緩存中,最終服務器進程從接收緩存中讀取數據即爲源客戶端進程傳遞過來的 消息。

上述通訊過程的描述和圖片均出自錢文品的深刻理解 RPC 交互流程。

我嘗試尋找通訊過程當中每一個步驟的依據(尤爲是 send buffer to NIC to recv buffer),(我翻閱了 TCP 的 RFC 和 Kernel.org)但遺憾的是並未找到有力的證實(必定是我太菜了),若是有朋友知道,能夠評論告訴我或發郵件 zenrusts@sina.com 告訴我,我能夠擴展出另外一篇文章。

建立 Streams

那麼問題來了:在 Python 中,咱們如何實現端到端的消息發送呢?

答:Python 提供了一些對象幫助咱們實現這個需求,其中相對簡單易用的是 Streams。

Streams 是 Python Asynchronous I/O 中提供的 High-level APIs。Python 官方文檔對 Streams 的介紹以下:

Streams are high-level async/await-ready primitives to work with network connections. Streams allow sending and receiving data without using callbacks or low-level protocols and transports.

我尬譯一下:Streams 是用於網絡鏈接的 high-level async/await-ready 原語。Streams 容許在不使用回調或 low-level protocols and transports 的狀況下發送和接收數據。

Python 提供了 asyncio.open_connection() 讓開發者建立 Streams,asyncio.open_connection() 將創建網絡鏈接並返回 reader 和 writer 對象,這兩個對象實際上是 StreamReader 和 StreamWriter 類的實例。

開發者能夠經過 StreamReader 從 IO 流中讀取數據,經過 StreamWriter 將數據寫入 IO 流。雖然文檔並無給出 IO 流的明肯定義,但我猜它跟 buffer (也就是 send buffer to NIC to recv buffer 中的 buffer)有關,你也能夠抽象的認爲它就是 buffer。

有了 Streams,就有了端到端消息發送的完整實現。下面將經過一個例子來熟悉 Streams 的用法和用途。這是 Python 官方文檔給出的雙端示例,首先是 Server 端:

# TCP echo server using streams
# 本文出自「夜幕團隊 NightTeam」 轉載請聯繫並取得受權
import asyncio

async def handle_echo(reader, writer):
    data = await reader.read(100)
    message = data.decode()
    addr = writer.get_extra_info('peername')

    print(f"Received {message!r} from {addr!r}")

    print(f"Send: {message!r}")
    writer.write(data)
    await writer.drain()

    print("Close the connection")
    writer.close()

async def main():
    server = await asyncio.start_server(
        handle_echo, '127.0.0.1', 8888)

    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()

asyncio.run(main())

接着是 Client 端:

# TCP echo client using streams
# 本文出自「夜幕團隊 NightTeam」 轉載請聯繫並取得受權
import asyncio

async def tcp_echo_client(message):
    reader, writer = await asyncio.open_connection(
        '127.0.0.1', 8888)

    print(f'Send: {message!r}')
    writer.write(message.encode())

    data = await reader.read(100)
    print(f'Received: {data.decode()!r}')

    print('Close the connection')
    writer.close()

asyncio.run(tcp_echo_client('Hello World!'))

將示例分別寫入到 server.py 和 client.py 中,而後按序運行。此時 server.py 的窗口會輸出以下內容:

Serving on ('127.0.0.1', 8888)
Received 'Hello World!' from ('127.0.0.1', 59534)
Send: 'Hello World!'
Close the connection

從輸出中得知,服務啓動的 address 和 port 爲 ('127.0.0.1', 8888),從 ('127.0.0.1', 59534) 讀取到內容爲 Hello World! 的消息,接着將 Hello World! 返回給 ('127.0.0.1', 59534) ,最後關閉鏈接。

client.py 的窗口輸出內容以下:

Send: 'Hello World!'
Received: 'Hello World!'
Close the connection

在建立鏈接後,Client 向指定的端發送了內容爲 Hello World! 的消息,接着從指定的端接收到內容爲 Hello World! 的消息,最後關閉鏈接。

有些讀者可能不太理解,爲何 Client Send Hello World! ,而 Server 接收到以後也向 Client Send Hello World! 。雙端的 Send 和 Received 都是 Hello World! ,這很容易讓新手懵逼。實際上這就是一個普通的回顯服務器示例,也就是說當 Server 收到消息時,將消息內容原封不動的返回給 Client。

這樣只是爲了演示,並沒有它意,但這樣的示例卻會給新手帶來困擾。

以上是一個簡單的 Socket 編程示例,總體思路理解起來仍是很輕鬆的,接下來咱們將逐步解讀示例中的代碼:

* client.py 中用 `asyncio.open_connection()` 鏈接指定的端,並得到 reader 和 writer 這兩個對象。
* 而後使用 writer 對象中的 `write()` 方法將 `Hello World!` 寫入到 IO 流中,該消息會被髮送到 Server。
* 接着使用 reader 對象中的 `read()` 方法從 IO 流中讀取消息,並將消息打印到終端。

看到這裏,你或許會有另外一個疑問:write() 只是將消息寫入到 IO 流,並無發送行爲,那消息是如何傳輸到 Server 的呢?

因爲沒法直接跟進 CPython 源代碼,因此咱們沒法獲得確切的結果。但咱們能夠跟進 Python 代碼,得知消息最後傳輸到 transport.write() ,若是你想知道更多,能夠去看 Transports and Protocols 的介紹。你能夠將這個過程抽象爲上圖的 Client to send buffer to NIC to recv buffer to Server。

功能模塊設計

經過上面的學習,如今你已經掌握了 WebSocket 協議規範和 Python Streams 的基本用法,接下來就能夠設計一個 WebSocket 客戶端庫了。

根據 RFC6455 的約定,WebSocket 以前是 HTTP,經過「握手」來升級協議。協議升級後進入真正的 WebSocket 通訊,通訊包含發送(Send)和接收(Recv)。文本消息要在傳輸過程前轉換爲 Frames,而接受端讀取到消息後要將 Frames 轉換成文本。固然,期間會有一些異常產生,咱們可能須要自定義異常,以快速定位問題所在。如今咱們得出了幾個模塊:

* 握手 - ShakeHands

* 傳輸 - Transports

* 幀處理 - Frames

* 異常 - Exceptions

一切準備就緒後,就能夠進入真正的編碼環節了。

因爲實戰編碼篇幅太長,我決定放到下一期,這期的內容,讀者們可能須要花費一些時間吸取。

小結

開篇我強調了「創造能力」有多麼重要,甚至拋出了一些不是很貼切的例子,但我就是想告訴你,不要作調參🐶。

而後我告訴你,本篇文章要講解的是 WebSocket。

接着又跟你說,要掌握 WebSocket 協議,若是你沒法獨立啃完 RFC6455,還能夠看我寫過的幾篇關於 WebSocket 文章和位運算文章。

過了幾分鐘,給你展現了 Socket 的通訊過程,雖然沒有強有力的依據,但你能夠假設這是對的。

喝了一杯白開水以後,我向你展現了 Streams 的具體用法併爲你解讀代碼的做用,重要的是將 Streams 與 Socket 通訊過程進行了抽象。

這些前置條件都肯定後,我又帶着你草草地設計了 WebSocket 客戶端的功能模塊。

下一篇文章將進入代碼實戰環節,請作好環境(Python 3.6+)準備。

總之,要想越過前面這座山,就請跟我來!


文章做者:「夜幕團隊 NightTeam 」- 韋世東

夜幕團隊成立於 2019 年,團隊成員包括崔慶才、周子淇、陳祥安、唐軼飛、馮威、蔡晉、戴煌金、張冶青和韋世東。

涉獵的主要編程語言爲 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發和對象存儲等。團隊非正亦非邪,只作認爲對的事情,請你們當心。

相關文章
相關標籤/搜索