用 Bash 寫一個簡單的 HTTP 客戶端 文章首圖

用 Bash 寫一個簡單的 HTTP 客戶端

用 Bash 寫一個簡單的 HTTP 客戶端

在 Linux 系統管理與自動化腳本開發中,我們經常需要與遠端伺服器進行資料交換。雖然 curlwget 是處理 HTTP 請求的標準工具,但它們通常作為黑盒存在。對於想要深入理解網路協議底層運作,或是需要在極簡環境(如嵌入式系統)中實現特定邏輯的管理員來說,直接使用 Bash 搭配原生網路設備檔案來建構 HTTP 客戶端,不僅是一種技術挑戰,更是一個絕佳的學習契機。

這篇文章將引導你使用 Bash 撰寫一個最基礎的 HTTP 客戶端,並透過 /dev/tcp 偽裝置發送原始 HTTP 請求。我們將拆解整個過程,從建立 TCP 連線到解析回應,讓你徹底掌握這門「古老而強大」的技巧。

為什麼要學這個?

在現代 DevOps 流程中,依賴標準工具並無不妥。然而,了解底層機制能帶來幾個實際好處:

  1. 除錯能力:當 curl 失敗時,你能夠手動建構請求來檢查伺服器是否正確接收標頭。
  2. 環境限制:在某些最小化安裝的容器或救援模式中,可能無法安裝額外的套件,但 Bash 與核心網路功能始終可用。
  3. 教育意義:這是理解 HTTP 協議結構(請求行、標頭、主體)最直觀的方式。

核心原理:/dev/tcp

Bash 內建了一個名為 /dev/tcp/host/port 的偽裝置。當我們對其進行讀寫操作時,Bash 會自動建立一個 TCP 連線。這使得我們無需編譯 C 程式或使用 nc(Netcat)等外部工具,僅憑純 Bash 即可完成網路通訊。

注意:此功能依賴 Bash 的 noclobber 選項未開啟,且通常預設啟用。若發現無法使用,請確認你的 Bash 版本支援該功能(幾乎所有現代發行版皆支援)。

實作步驟

我們將撰寫一個名為 simple_http_client.sh 的腳本,它接受主機名、埠號以及 URI 路徑作為參數。

1. 腳本程式碼

請在終端機中建立並編輯檔案:

#!/bin/bash

# 檢查參數數量
if [ $# -lt 3 ]; then
    echo "用法: $0 <host> <port> <uri>"
    echo "範例: $0 www.example.com 80 /"
    exit 1
fi

HOST=$1
PORT=$2
URI=$3

# 建立 TCP 連線並發送 HTTP 請求
# 使用 exec 將檔案描述符 3 綁定到連線
exec 3<>/dev/tcp/$HOST/$PORT

# 建構 HTTP/1.1 請求
# Host 標頭是必需的,因為現代伺服器常使用虛擬主機
HTTP_REQUEST="GET $URI HTTP/1.1\r\n"
HTTP_REQUEST+="Host: $HOST\r\n"
HTTP_REQUEST+="Connection: close\r\n"
HTTP_REQUEST+="User-Agent: Bash-HTTP-Client/1.0\r\n"
HTTP_REQUEST+="\r\n" # 空行表示標頭結束

# 發送請求
echo -ne $HTTP_REQUEST >&3

# 讀取回應直到連線關閉
RESPONSE=""
while IFS= read -r line; do
    # 避免無限迴圈,當連線關閉時跳出
    if [ -z "$line" ]; then
        break
    fi
    RESPONSE+="$line\n"
done <&3

# 關閉檔案描述符
exec 3<&-

# 顯示回應內容
echo "=== HTTP 回應 ==="
echo -e "$RESPONSE"

2. 賦予執行權限並測試

chmod +x simple_http_client.sh
./simple_http_client.sh www.example.com 80 /

若一切正常,你將看到類似以下的輸出:

=== HTTP 回應 ===
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Date: ...
...

程式碼深度解析

  1. exec 3<>/dev/tcp/$HOST/$PORT: 這行是核心。它打開一個雙向的檔案描述符 3,連接到指定的 TCP 端點。這類似於在 C 語言中使用 socket()connect()

  2. echo -ne $HTTP_REQUEST >&3: 我們將建構好的 HTTP 請求寫入檔案描述符 3\r\n 是 HTTP 協議要求的行結尾符號,必須使用 -e 參數來解析。

  3. while IFS= read -r line; do ... done <&3: 從檔案描述符 3 讀取回應。由於 HTTP 回應可能包含多個行,我們使用迴圈逐行讀取。當連線關閉(例如伺服器發送 Connection: close 後),讀取操作會終止。

常見問題與解決方案

Q1: 執行時出現 "Bad file descriptor" 錯誤

這通常發生在 Bash 版本較舊或環境配置異常時。請確認你的 Bash 版本大於 2.05b。你可以透過 bash --version 檢查。此外,確保你沒有在其他地方佔用檔案描述符 3。若仍失敗,可嘗試改用檔案描述符 4 或更高數字,並同步修改腳本中的 3

Q2: 回應內容不完整或亂碼

HTTP/1.1 預設使用持久連線(Keep-Alive),但我們的腳本強制關閉連線(Connection: close)。如果伺服器支援分塊傳輸(Chunked Transfer Encoding)或壓縮(gzip),純文本讀取可能會出現問題。對於簡單的 HTML 頁面通常無礙,但若處理 JSON API,請確保伺服器未啟用壓縮,或後續加入解壓縮邏輯。此外,部分伺服器可能因未正確設定 Host 標頭而拒絕請求,請務必保留該標頭。

小結

透過這篇文章,我們成功使用純 Bash 撰寫了一個基礎的 HTTP 客戶端。雖然這段程式碼無法取代 curl 的功能完整性(例如 HTTPS、重定向處理、表單提交等),但它清晰地展示了 HTTP 協議的請求/回應模型以及 Bash 處理網路 I/O 的能力。

對於進階應用,你可以將此腳本擴展為支援 HTTPS(透過 openssl s_client)、處理 Cookie 或解析 JSON 回應。掌握這些底層技巧,將使你在面對複雜的系統除錯與自動化場景時,擁有更強大的工具與信心。下次當 curl 不聽話時,不妨試試看這個最原始的方法,也許你會發現不同的解決思路。