PyPtt - 如何利用 NAWS 動態調整視窗高度提升資料獲取效率
在使用 PyPtt 進行自動化開發時,許多開發者可能會好奇:為什麼某些操作(如抓取看板文章)在不同環境下,速度差異如此巨大?除了網路品質外,核心差異往往在於是否有效利用了 Telnet 的 NAWS 協定(Negotiate About Window Size)。
今天我們將深入剖析 NAWS 的運作原理,並對照 PyPtt 的真實原始碼,了解這個機制如何被完整實作。
挑戰:為什麼標準 BBS 資料獲取很慢?
PTT 是基於 Telnet 協定的 BBS。傳統的 Telnet 客戶端預設視窗高度通常為 24 行。這表示,當程式需要獲取一百篇文章時,它必須進行多次「分頁(Paging)」互動:
- 請求一頁(24 行)
- 等待網路傳輸
- 解析終端機內容
- 發送「翻頁」指令
- 重複以上步驟
頻繁的網路往返(Round-trips)與解析開銷,是 BBS 資料獲取效能的主要瓶頸。而解開這個瓶頸的鑰匙,正是 NAWS。
NAWS 協定深度解析
什麼是 NAWS?
NAWS(Negotiate About Window Size)定義於 RFC 1073,是 Telnet 協定的標準擴充選項(Option Code = 31 / 0x1F)。它允許 Telnet 客戶端主動告知伺服器目前終端機的視窗尺寸(寬度與高度),讓伺服器能依據這個資訊調整輸出的分頁行為。
在沒有 NAWS 的情況下,PTT 伺服器只能預設以 24 行為一頁,不論客戶端實際上能顯示多少內容。
協商交握流程(Telnet Option Negotiation)
NAWS 的啟用分為兩個階段:能力協商與尺寸通報。
Phase 1 - 能力協商
客戶端發送 WILL NAWS,表示「我支援 NAWS,我會通報我的視窗大小」:1
2IAC WILL NAWS
0xFF 0xFB 0x1F
伺服器若支援,回應 DO NAWS,表示「好,請告訴我你的視窗大小」:1
2IAC DO NAWS
0xFF 0xFD 0x1F
Phase 2 - 尺寸通報(Subnegotiation)
協商完成後,客戶端透過 Subnegotiation(SB)封包傳送實際的視窗尺寸:1
2IAC SB NAWS [寬度 高位] [寬度 低位] [高度 高位] [高度 低位] IAC SE
0xFF 0xFA 0x1F 0x00 0x50 0x00 0x64 0xFF 0xF0
以上範例通報的尺寸為:
- 寬度:
0x0050= 80 欄 - 高度:
0x0064= 100 行
每當視窗尺寸改變,客戶端可以重新發送 SB 封包更新伺服器端的認知。PTT 伺服器在收到這個封包後,會立即以新的行數作為每頁的輸出量。
PyPtt 的 NAWS 實作
connect_core.py 負責底層的 WebSocket/Telnet 連線。在連線建立後,PyPtt 立即發出兩段連在一起的 NAWS 封包:第一段是 IAC WILL NAWS 能力宣告,第二段是 IAC SB 尺寸通報。1
2
3
4
5
6
7
8
9
10
11
12# connect_core.py L217–L227
# Respond to PTT's IAC DO NAWS (ff fd 1f) and announce terminal size.
# PTT honours any height >= 24; larger values reduce get_content iterations.
# PTT's hard cap is 100 rows regardless of what we send.
# h is validated to 24–254; 255 is excluded because 0xFF inside a
# Telnet SB payload must be doubled (RFC 854) — simpler to cap at 254.
h = self.config.screen_height
naws = (
b'\xff\xfb\x1f' # IAC WILL NAWS
+ b'\xff\xfa\x1f\x00\x50' + bytes([0, h]) + b'\xff\xf0' # IAC SB NAWS 80×h IAC SE
)
loop.run_until_complete(self._core.send(naws))
注意:RFC 854 規定 Telnet SB payload 中的
0xFF必須被跳脫為0xFF 0xFF,因此高度上限刻意設為 254 而非 255,以避免手動處理轉義邏輯。
除了連線時的初始化,connect_core.py 也實作了 set_screen_height() 方法,允許在 session 進行中隨時重新發送 NAWS 封包來調整視窗高度:1
2
3
4
5
6
7
8
9
10# connect_core.py L452–L460
def set_screen_height(self, height: int) -> None:
"""Send a mid-session NAWS to resize the terminal, then update config."""
naws = (
b'\xff\xfb\x1f'
+ b'\xff\xfa\x1f\x00\x50' + bytes([0, height]) + b'\xff\xf0'
)
loop = self._get_event_loop()
loop.run_until_complete(self._core.send(naws))
self.config.screen_height = height
這個設計讓 PyPtt 能夠在需要大量獲取資料的操作前臨時擴展視窗,完成後再還原,而不影響其他一般操作的預設行為。
VT100Parser 的狀態對齊
僅僅發送 NAWS 封包是不夠的。PyPtt 內部維護了一個 VT100Parser 來模擬終端機畫面。如果 Parser 不知道視窗已經擴大,它將無法正確處理超出 24 行的資料——後面的行會被靜默丟棄。
從 connect_core.py 可以看到,VT100Parser 在每次解析畫面時,都會直接從 config.screen_height 取得當前視窗高度:1
2# connect_core.py L254
vt100_p = screens.VT100Parser(receive_data_buffer, self.current_encoding, self.config.screen_height)
這意味著只要 set_screen_height() 同步更新了 config.screen_height,下一次解析就會自動用新的高度建立 Parser,不需要額外的同步步驟。
NAWS 封包決定伺服器「輸出多少」;VT100Parser 的高度參數決定客戶端「接收多少」。set_screen_height() 同時更新兩者,確保兩端始終一致。
expanded_screen:封裝成上下文管理器
_api_util.py 將 set_screen_height() 進一步封裝成 expanded_screen 上下文管理器,讓呼叫端不需要手動管理還原邏輯:1
2
3
4
5
6
7
8
9
10
11
12
13
14# _api_util.py L18–L36
_MAX_GET_CONTENT_HEIGHT = 100 # PTT server hard cap
def expanded_screen(api):
"""Temporarily resize the PTT terminal to its maximum (100 rows) and restore on exit."""
original_height = api.config.screen_height
if original_height < _MAX_GET_CONTENT_HEIGHT:
api.connect_core.set_screen_height(_MAX_GET_CONTENT_HEIGHT)
try:
yield
finally:
if original_height < _MAX_GET_CONTENT_HEIGHT:
api.connect_core.set_screen_height(original_height)
_MAX_GET_CONTENT_HEIGHT = 100 是 PTT 伺服器的硬性上限;不論你在 NAWS 封包裡填入多大的數字,PTT 最多只會輸出 100 行。
在 PyPtt 內部,get_content()(負責讀取文章全文)就是直接以 expanded_screen 包裹整個獲取迴圈:1
2
3
4# _api_util.py L55
with expanded_screen(api):
# 翻頁迴圈,每次最多讀取 100 行而非 24 行
...
實作指南:如何在您的程式中使用?
若您自己的邏輯也需要大量分頁操作,只需同樣包裹在 expanded_screen 中即可:1
2
3
4
5
6
7
8from PyPtt import API, _api_util
ptt = API()
ptt.login('your_id', 'your_password')
# 透過 NAWS 將視窗擴展至 100 行,減少翻頁次數
with _api_util.expanded_screen(ptt):
posts = ptt.get_post_list(board='Gossiping', index=1000)
離開 with 區塊後,expanded_screen 會自動透過 set_screen_height() 還原原始高度,並重新發送 NAWS 封包通知伺服器。
總結
NAWS(RFC 1073)是一個常被忽略但極具威力的 Telnet 擴充協定。PyPtt 的實作路徑清晰完整:
- 連線時在
connect_core.py發送初始 NAWS 封包(IAC WILL NAWS+IAC SB) - 需要擴展時,透過
set_screen_height()發送新的 NAWS 封包,同時同步VT100Parser的解析高度 expanded_screen上下文管理器自動管理擴展與還原,確保冪等性
透過將視窗高度從 24 行提升至 100 行,分頁互動次數可降低至原來的 1/4 到 1/5,在大量獲取資料的場景下效果尤為顯著。
參考
- PyPtt 原始碼:connect_core.py、_api_util.py。
- NAWS 規範: RFC 1073。