已成功選擇新聞組並取得最新文章編號 $last。本文示範如何批次讀取最後 10 篇文章,處理多行回應、錯誤碼判斷,並正確關閉連線。
<?php
$cfgLimit = 10; // 讀取筆數
$boucle = $last - $cfgLimit; // 起始編號
while ($boucle <= $last) {
set_time_limit(0); // 防止長時間腳本中斷
// 發送 ARTICLE 指令 (支援編號或 Message-ID)
fputs($usenet_handle, "ARTICLE $boucle\r\n");
$article = '';
$tmp = fgets($usenet_handle, 4096);
// 檢查回應碼:220 表示文章內容跟隨
if (substr($tmp, 0, 3) !== '220') {
echo "+----------------------+\n";
echo "Error on article $boucle: $tmp";
echo "+----------------------+\n";
} else {
// 讀取多行文章內容,以單獨一行 "." 結束 (.\r\n)
while ($tmp !== ".\r\n") {
$tmp = fgets($usenet_handle, 4096);
$article .= $tmp;
}
echo "+----------------------+\n";
echo "Article $boucle\n";
echo "+----------------------+\n";
echo $article;
}
$boucle++;
}
?>
NNTP 規定多行回應 (文章內容、HEAD、BODY) 以 單獨一行包含句點 . (即 .\r\n) 結束。讀取迴圈必須精確比對此終止標記,否則會漏讀或多讀。
注意: 文章內容本身可能包含單獨一行.的行 (如程式碼片段)。NNTP 規定發送端需在每行開頭為.的行前加上額外.(即..),接收端讀取時需移除這個轉義點。上述簡化程式碼未處理此轉義,實際應用需實現dot-stuffing解碼。
HEAD <id>:僅回應文章標頭 (From, Subject, Date, Message-ID, References 等),適合建立索引、列表預覽。BODY <id>:僅回應文章本文,不含標頭。ARTICLE <id> = HEAD + 空行 + BODY。根據需求選擇指令可大幅減少頻寬與處理時間。
<?php
// 發送 QUIT 指令 (禮貌性關閉)
fputs($usenet_handle, "QUIT\r\n");
$bye = fgets($usenet_handle, 1024); // 讀取 205 回應
echo "Server: $bye";
// 關閉 Socket 資源
fclose($usenet_handle);
?>
最佳實踐: 即使伺服器異常斷線,fclose() 也能確保本地檔案描述符釋放。生產環境建議包裝在 try/finally 或 register_shutdown_function 中保證執行。
將下載的文章持久化並建立全文索引,即可構建 Web 版新聞組搜尋引擎:
articles (msgid, newsgroup, subject, from_addr, date, headers, body, fetched_at),msgid 唯一索引。Message-ID 為主鍵判斷是否已抓取。FULLTEXT(subject, body)、PostgreSQL tsvector、或專用引擎 Elasticsearch, Meilisearch, Typesense。$last 至檔案/資料庫)。References 串聯)、RSS/Atom 輸出。| 層面 | 傳統 Socket 腳本 | 現代化方案 |
|---|---|---|
| 執行模式 | CLI 腳本、阻塞 I/O、單執行緒 | Swoole / OpenSwoole / RoadRunner 常駐記憶體、協程非阻塞、多 Worker 並發 |
| 協議處理 | 手工拼裝/解析指令 | 使用成熟函式庫 (如 php-nntp/php-nntp, symfony/mailer 等) |
| 資料儲存 | 直接輸出/簡單 INSERT | Doctrine ORM / Eloquent + Migration,事件溯源記錄抓取狀態 |
| 索引搜尋 | MySQL LIKE / 簡易 FULLTEXT | Elasticsearch + IK 分詞,向量搜尋支援語義查詢 |
| 監控告警 | 無 | Prometheus + Grafana 監控抓取延遲、錯誤率、佇列積壓 |
| 部署 | Cron + 腳本 | Kubernetes CronJob / Sidecar,健康檢查、滾動更新 |
<?php
// fetch_nntp.php
require_once 'NntpClient.php'; // 使用上篇封裝的類別
$config = [
'host' => env('NNTP_HOST', 'news.example.com'),
'port' => (int)env('NNTP_PORT', 119),
'user' => env('NNTP_USER'),
'pass' => env('NNTP_PASS'),
'groups' => explode(',', env('NNTP_GROUPS', 'php.general,php.dev')),
'limit_per_group' => 50,
'state_file' => __DIR__ . '/var/last_ids.json', // 記錄各群組最後抓取 ID
];
$client = new NntpClient($config['host'], $config['port']);
$client->connect();
if ($config['user']) $client->auth($config['user'], $config['pass']);
$state = json_decode(file_get_contents($config['state_file']), true) ?: [];
foreach ($config['groups'] as $group) {
$info = $client->group($group);
if (!$info) continue;
$lastFetched = $state[$group] ?? ($info['last'] - $config['limit_per_group']);
$start = max($info['first'], $lastFetched + 1);
$end = $info['last'];
for ($id = $start; $id <= $end; $id++) {
$article = $client->article($id);
if ($article) {
// 解析標頭、儲存資料庫、送索引佇列...
save_article($group, $id, $article);
}
}
$state[$group] = $end;
}
$client->quit();
file_put_contents($config['state_file'], json_encode($state));
echo "抓取完成 " . date('Y-m-d H:i:s') . "\n";
?>
本系列三篇文章完整演示了 PHP Socket 程式設計從建立連線、協議對話、資料讀取到資源釋放的全生命週期,並以 NNTP 為例展示了實際應用層協議的處理細節。雖然現代 Web 開發多基於 HTTP/REST/gRPC,但 Socket 程式設計能力對於理解網路底層、整合舊系統、開發高效能網路服務 (如即時通訊、遊戲伺服器、物聯網閘道) 至關重要。建議開發者在掌握阻塞 I/O 基礎後,進階學習 I/O 多路複用 (stream_select, libevent, Swoole 協程)、TLS/SSL 加密通訊 (stream_socket_enable_crypto)、協議狀態機設計,構建健壯、高併發的網路應用。