在現代網路應用中,非同步程式設計已成為提升效能的關鍵技術。本文將深入探討如何利用 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
模擬通訊通道
要模擬通訊通道,需要執行以下步驟:
- 執行伺服器指令碼:
python example1.py
- 開啟另一個終端,連線到伺服器:
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
類別,讓它可以將收到的訊息原樣發回給客戶端。這可以透過使用 asyncio
的 WriteTransport
類別中的 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_made
和data_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())
在這個例子中,我們使用 aiohttp
的 ClientSession
類別建立了一個客戶端會話,然後使用 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 內容,然後將其寫入檔案中。最終,下載和寫入任務完成。
非同步網頁下載與同步網頁下載的比較
在前面的例子中,我們使用了 aiohttp
和 aiofiles
來實作非同步的網頁下載和檔案寫入。現在,我們將比較這個非同步版本和其對應的同步版本。
非同步版本
非同步版本的程式碼如下:
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 等深度融合,創造更多可能性。玄貓認為,熟練運用非同步程式設計技術將成為開發者不可或缺的核心技能。