在現代網路應用中,非同步程式設計已成為提升效能的關鍵技術。本文將深入探討如何利用 Python 的 Asyncio 函式函式庫,結合 Telnet 建立非同步通訊通道,並示範如何建立迴音伺服器。同時,我們將介紹 aiohttp 函式函式庫,用於非同步 HTTP 請求,以及 aiofiles 函式函式庫,用於非同步檔案寫入。最後,我們將比較非同步與同步網頁下載的效能差異,並簡要討論平行程式設計中的死結問題。

使用Asyncio和Telnet建立通訊通道

在本節中,我們將使用Asyncio和Telnet建立一個簡單的通訊通道。首先,我們需要建立一個Asyncio伺服器,然後使用Telnet連線到伺服器。

建立Asyncio伺服器

以下是建立Asyncio伺服器的程式碼:

import asyncio

class MyProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        print("Connection from {}".format(transport.get_extra_info("peername")))

    def data_received(self, data):
        print("Received data: {}".format(data.decode()))

    def connection_lost(self, exc):
        print("Connection lost")

loop = asyncio.get_event_loop()
coro = loop.create_server(MyProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

print("Serving on {}".format(server.sockets[0].getsockname()))

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

server.close()
loop.run_until_complete(server.wait_closed())
loop.close()

這段程式碼建立了一個Asyncio伺服器,監聽127.0.0.1:8888埠。當有客戶端連線到伺服器時,伺服器會列印預出連線資訊。

安裝Telnet

在Windows系統中,Telnet已經安裝,但可能未啟用。要啟用Telnet,可以使用以下命令:

dism /online /Enable-Feature /FeatureName:TelnetClient

在Linux系統中,Telnet通常已經預裝,所以可以直接跳過此步驟。

在macOS系統中,如果Telnet未安裝,可以使用Homebrew安裝:

brew install telnet

模擬通訊通道

要模擬通訊通道,需要執行以下步驟:

  1. 執行伺服器指令碼:python example1.py
  2. 開啟另一個終端,連線到伺服器:telnet 127.0.0.1 8888

伺服器終端會列印預出連線資訊,客戶端終端會顯示Telnet介面。客戶端可以向伺服器傳送資料,伺服器會接收並處理資料。

伺服器輸出

Serving on ('127.0.0.1', 8888)
Connection from ('127.0.0.1', 60332)

客戶端輸出

Trying 127.0.0.1...
Connected to localhost.

這樣就建立了一個簡單的通訊通道,使用Asyncio和Telnet。客戶端可以向伺服器傳送資料,伺服器會接收並處理資料。

非同步伺服器與客戶端之間的雙向通訊

在上一節中,我們實作了一個基本的非同步伺服器,該伺服器可以接收客戶端的連線並處理其傳送的訊息。然而,為了建立一個真正的通訊管道,我們需要讓伺服器也能夠向客戶端傳送訊息。

更新伺服器以實作迴音功能

為了實作這一功能,我們需要更新我們的 EchoServerClientProtocol 類別,讓它可以將收到的訊息原樣發回給客戶端。這可以透過使用 asyncioWriteTransport 類別中的 write() 方法來實作。

更新 data_received() 方法

以下是更新後的 data_received() 方法:

def data_received(self, data):
    message = data.decode()
    print(f"收到訊息:{message}")
    self.transport.write(data)  # 將收到的訊息發回給客戶端

在這裡,我們使用 decode() 方法將收到的二進位制資料轉換為字串,然後列印預出收到的訊息。接著,我們使用 write() 方法將收到的訊息原樣發回給客戶端。

測試更新後的伺服器

現在,我們可以測試更新後的伺服器了。啟動伺服器後,使用客戶端連線到伺服器,並傳送一些訊息。伺服器應該會將收到的訊息原樣發回給客戶端。

多個客戶端之間的通訊

使用非同步程式設計,我們可以讓多個客戶端同時與伺服器進行通訊,而不需要使用多執行緒或多程式。這是因為非同步程式設計允許我們在單一執行緒中處理多個任務。

多個客戶端的連線

當多個客戶端連線到伺服器時,伺服器會為每個客戶端建立一個新的 EchoServerClientProtocol 例項。這些例項會處理各自的客戶端連線,並將收到的訊息發回給對應的客戶端。

通訊的可靠性

非同步程式設計可以讓我們建立可靠的通訊管道。即使在多個客戶端同時傳送訊息的情況下,伺服器也能夠正確地處理和發回訊息。

建立根據Asyncio的迴音伺服器

在這個範例中,我們將使用Python的Asyncio函式庫建立一個簡單的迴音伺服器。這個伺服器會接收來自使用者的訊息,並將其原樣回傳給使用者。

伺服器端實作

首先,我們需要定義一個繼承自asyncio.Protocol的類別,代表我們的伺服器協定。這個類別需要實作兩個方法:connection_madedata_received

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))
        self.transport.write(('Echoed back: {}'.format(message)).encode())

啟動伺服器

接下來,我們需要使用asyncio.get_event_loop()取得目前的事件迴圈,然後使用loop.create_server()建立一個伺服器。這個伺服器會在localhost的8888埠口上監聽。

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass
finally:
    server.close()
    loop.close()

執行伺服器

執行伺服器後,就可以使用telnet或其他工具連線到這個伺服器,並傳送訊息給伺服器。伺服器會將收到的訊息原樣回傳給使用者。

客戶端連線

你可以使用telnet工具連線到這個伺服器:

telnet 127.0.0.1 8888

然後,你就可以傳送訊息給伺服器:

Hello, server!

伺服器會將收到的訊息原樣回傳給你:

Echoed back: Hello, server!

圖表翻譯

以下是伺服器的工作流程圖表:

  flowchart TD
    A[使用者連線] --> B[伺服器接收訊息]
    B --> C[伺服器處理訊息]
    C --> D[伺服器回傳訊息]
    D --> E[使用者接收訊息]

圖表翻譯

這個圖表展示了伺服器的工作流程。首先,使用者連線到伺服器。然後,伺服器接收使用者傳送的訊息。接下來,伺服器處理這個訊息,並將其原樣回傳給使用者。最後,使用者接收到伺服器回傳的訊息。

建立基礎的Telnet伺服器

Telnet是一種基礎的網路協定,允許使用者連線到遠端伺服器並執行命令。下面是使用Python和asyncio建立一個簡單的Telnet伺服器的例子。

伺服器程式碼

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        print('Received:', data.decode())
        self.transport.write(data)
        self.transport.close()

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

print('Server started. Listening on port 8888...')

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

server.close()
loop.close()

客戶端連線

使用者可以使用Telnet客戶端連線到伺服器。例如,在Linux或Mac上,可以使用以下命令:

telnet 127.0.0.1 8888

在Windows上,可以使用內建的Telnet客戶端或下載第三方Telnet客戶端。

伺服器處理

當客戶端連線到伺服器時,伺服器會列印預出連線資訊並建立一個新的協定例項。當客戶端傳送資料時,伺服器會列印預出接收到的資料並將其發回給客戶端。然後,伺服器會關閉連線。

關閉傳輸

在某些情況下,伺服器可能需要強制關閉傳輸。例如,當伺服器過載時,可以關閉連線以防止進一步的請求。可以使用transport.close()方法來關閉傳輸。

修改協定

可以修改協定來實作自定義的行為。例如,可以修改data_received()方法來處理接收到的資料並傳送自定義的回應。

非同步網路請求與 aiohttp

在上一節中,我們探討了使用 asyncio 模組實作伺服器端的非同步通訊。現在,我們將關注客戶端的非同步通訊,使用 aiohttp 模組進行 HTTP 請求。

aiohttp 模組介紹

aiohttp 是一個支援非同步程式設計的 Python 模組,提供了高階別的 HTTP 通訊功能。它可以與 asyncio 模組無縫整合,方便地實作非同步 HTTP 請求。

客戶端通訊例項

以下是一個使用 aiohttp 進行客戶端通訊的例子:

import aiohttp
import asyncio

async def fetch_page(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    url = "https://example.com"
    async with aiohttp.ClientSession() as session:
        html = await fetch_page(session, url)
        print(html)

asyncio.run(main())

在這個例子中,我們使用 aiohttpClientSession 類別建立了一個客戶端會話,然後使用 fetch_page 函式向指定的 URL 傳送 GET 請求,並取得 HTML 回應。

非同步網路請求

使用 aiohttp 可以輕鬆地實作非同步網路請求。以下是一個示例:

import aiohttp
import asyncio

async def fetch_pages(session, urls):
    tasks = []
    for url in urls:
        task = asyncio.create_task(fetch_page(session, url))
        tasks.append(task)
    results = await asyncio.gather(*tasks)
    return results

async def fetch_page(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = ["https://example.com", "https://example.org", "https://example.net"]
    async with aiohttp.ClientSession() as session:
        pages = await fetch_pages(session, urls)
        for page in pages:
            print(page)

asyncio.run(main())

在這個例子中,我們使用 fetch_pages 函式向多個 URL 傳送 GET 請求,並使用 asyncio.gather 函式等待所有請求完成。

安裝 aiohttp 和 aiofiles

在開始使用 aiohttp 進行非同步網路請求之前,您需要先安裝這個模組。您可以使用 pip 或 Anaconda 來安裝 aiohttp 和 aiofiles,後者是一個用於非同步檔案寫入的模組。

使用 pip 安裝

如果您使用 pip 作為套件管理工具,可以執行以下命令:

pip install aiohttp
pip install aiofiles

使用 Anaconda 安裝

如果您使用 Anaconda,可以執行以下命令:

conda install aiohttp
conda install aiofiles

驗證安裝

安裝完成後,您可以在 Python 解譯器中驗證是否安裝成功。嘗試匯入 aiohttp 和 aiofiles 模組:

>>> import aiohttp
>>> import aiofiles

如果沒有出現錯誤訊息,則表示安裝成功。

非同步抓取網站 HTML 程式碼

現在,讓我們看看如何使用 aiohttp 非同步抓取網站的 HTML 程式碼。即使只有一個任務(網站),您的應用程式仍然需要是非同步的,且需要實作非同步程式的結構。

範例程式碼

import aiohttp
import asyncio

async def get_html(session, url):
    async with session.get(url, ssl=False) as res:
        return await res.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await get_html(session, 'https://example.com')
        print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

這個範例程式碼使用 aiohttp 非同步抓取 https://example.com 的 HTML 程式碼,並將結果印出到主控臺。

執行程式碼

您可以將這個程式碼儲存到一個檔案中(例如 fetch_html.py),然後使用 Python 解譯器執行:

python fetch_html.py

這將會非同步抓取 https://example.com 的 HTML 程式碼,並將結果印出到主控臺。

非同步下載與檔案寫入

在上一節中,我們已經學習瞭如何使用 aiohttp 進行非同步的 HTTP 請求。現在,我們將進一步探討如何將下載的 HTML 內容寫入檔案中。

使用 aiofiles 寫入檔案

要實作非同步檔案寫入,我們將使用 aiofiles 模組。aiofiles 提供了一種非同步的方式來讀寫檔案,與 aiohttp 配合使用,可以實作高效的非同步下載和檔案儲存。

實作 download_html 協程

以下是 download_html 協程的實作:

import os
import aiofiles

async def download_html(session, url):
    async with session.get(url, ssl=False) as res:
        filename = f'output/{os.path.basename(url)}.html'
        async with aiofiles.open(filename, 'wb') as f:
            while True:
                chunk = await res.content.read(1024)
                if not chunk:
                    break
                await f.write(chunk)

在這個協程中,我們使用 aiofiles 開啟一個檔案,並將下載的 HTML 內容寫入其中。注意,我們使用 wb 模式開啟檔案,以便寫入二進位制資料。

使用 asyncio 執行下載任務

現在,我們可以使用 asyncio 執行下載任務:

import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        await download_html(session, 'https://www.packt.com')

asyncio.run(main())

這將下載 Packt 網站的 HTML 內容,並將其儲存到 output 目錄中。

內容解密:

在這個例子中,我們使用 aiohttp 進行非同步 HTTP 請求,並使用 aiofiles 寫入檔案。aiofiles 提供了一種非同步的方式來讀寫檔案,與 aiohttp 配合使用,可以實作高效的非同步下載和檔案儲存。

圖表翻譯:

  flowchart TD
    A[開始] --> B[下載 HTML]
    B --> C[寫入檔案]
    C --> D[完成]

這個圖表展示了下載和寫入檔案的過程。首先,我們下載 HTML 內容,然後將其寫入檔案中。最終,下載和寫入任務完成。

非同步網頁下載與同步網頁下載的比較

在前面的例子中,我們使用了 aiohttpaiofiles 來實作非同步的網頁下載和檔案寫入。現在,我們將比較這個非同步版本和其對應的同步版本。

非同步版本

非同步版本的程式碼如下:

import aiohttp
import aiofiles
import asyncio

async def download_html(session, url):
    async with session.get(url) as response:
        async with aiofiles.open(f"output/{url.split('/')[-1]}.html", mode="w") as f:
            while True:
                chunk = await response.content.read(1024)
                if not chunk:
                    break
                await f.write(chunk)

async def main(url):
    async with aiohttp.ClientSession() as session:
        await download_html(session, url)

urls = [
    # ...
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*(main(url) for url in urls)))

這個版本使用 aiohttp 來傳送 HTTP GET 請求,並使用 aiofiles 來非同步地寫入檔案。

同步版本

同步版本的程式碼如下:

import requests

def download_html(url):
    response = requests.get(url)
    with open(f"output/{url.split('/')[-1]}.html", mode="w") as f:
        f.write(response.text)

def main():
    urls = [
        # ...
    ]
    for url in urls:
        download_html(url)

if __name__ == "__main__":
    main()

這個版本使用 requests 來傳送 HTTP GET 請求,並使用 open 來同步地寫入檔案。

比較結果

非同步版本的輸出如下:

> python3 example5.py
Took 0.72 seconds.

同步版本的輸出如下:

> python3 example6.py
Took 2.34 seconds.

由於非同步版本可以同時傳送多個 HTTP 請求和寫入檔案,因此其執行時間遠少於同步版本。

平行程式設計中的死結問題

在平行程式設計中,死結(Deadlock)是一種常見的問題,它發生在多個程式或執行緒因為競爭資源而無法繼續執行的情況下。這章節將討論死結的理論原因、實際案例和解決方法。

死結的定義

死結是一種狀況,其中多個程式或執行緒因為競爭資源而無法繼續執行。這種情況通常發生在多個程式或執行緒需要分享相同的資源,但由於某些原因,無法獲得所需的資源。

從效能評估視角來看,本文深入探討瞭如何利用 Asyncio 和 Telnet 建立高效的非同步通訊通道,並分析了非同步網路請求的優勢。藉由實際程式碼範例,我們逐步展示瞭如何建立 Asyncio 伺服器、處理客戶端連線、傳送和接收資料,以及使用 aiohttp 進行非同步 HTTP 請求和檔案寫入。此外,文章還比較了非同步和同步網頁下載的效能差異,結果顯示非同步方法在處理多個網路請求時效率顯著提升。然而,非同步程式設計的複雜性也伴隨著潛在的死結風險。對於追求極致效能的網路應用而言,掌握 Asyncio 和 aiohttp 的使用技巧至關重要,但同時也需注意程式碼設計的合理性,避免非同步程式設計的陷阱。未來,隨著網路應用對高併發和低延遲的需求日益增長, Asyncio 等非同步程式設計技術將扮演更重要的角色,並可能與其他技術如 WebSocket 等深度融合,創造更多可能性。玄貓認為,熟練運用非同步程式設計技術將成為開發者不可或缺的核心技能。