今天,cURL 無疑是全球最受歡迎的網絡工具之一,下載量突破百億次,幾乎每個開發者的工具箱裏都少不了它。無論是大型項目,還是寫着玩的小腳本,往往都要依賴 cURL 來進行數據傳輸。據説 NASA 使用了 cURL 進行火星探測器數據傳輸!這讓 cURL 成為第一個在地球外運行的開源軟件。
但你知道嗎?cURL的作者,瑞典軟件工程師 Daniel Stenberg,最初只是需要一個簡單的小工具,能夠從網站上下載貨幣匯率數據。對,你沒聽錯,就是這麼一個簡單的需求,竟然催生了足以改變 Web 的 cURL!
1996 年底,為了自動獲取匯率數據,Daniel 在網上找到了一個名為 httpget 的工具,由巴西程序員 Rafael Sagula 開發。那時的 httpget 只有 300 多行的 C 語言代碼,功能簡單且代碼相當粗糙。儘管如此,Daniel 覺得這總比沒有工具強,於是決定為這個工具貢獻自己的力量。他對 httpget 進行了修復和改進,很快就成為了項目的主要維護者。自此,他將自己的智慧與熱情傾注於這個不起眼的小工具,從這 300 行代碼開始,歷經二十多年,培育出瞭如今近 60 萬行代碼的 cURL。
下面我們就來看看 cURL 的前身 httpget 的源代碼。現存最古老的 httpget 源代碼是 http://curl.se/download/archeology/httpget-1.3.c,發佈時間在 1997 年 4 月至 8 月之間。
void main(argc,argv)
int argc;
char *argv[];
{
...
/* Parse <url> */ 1️⃣
if (3 != sscanf(argv[argc - 1], "%64[^\n:]://%256[^\n/]%512[^\n]", proto, name, path)) {
fprintf(stderr, "<url> malformed.\n");
exit(-1);
}
...
sockfd = socket(AF_INET, SOCK_STREAM, 0); 2️⃣
memset((char *) &serv_addr, '\0', sizeof(serv_addr));
// hp = GetHost(name)
memcpy((char *)&(serv_addr.sin_addr), hp->h_addr, hp->h_length);
serv_addr.sin_family = hp->h_addrtype;
serv_addr.sin_port = htons(port);
if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { 3️⃣
switch(errno) {
...
}
exit(-1);
};
fprintf(stderr, "Connected!\n");
send_get(sockfd, path); 4️⃣
skip_header(sockfd); 5️⃣
bytecount = 0;
for (;;) { 6️⃣
nread = read(sockfd, buf, BUFSIZE);
...
if (nread==0) {
...
close(sockfd);
...
exit(0);
}
...
fwrite(buf, 1, nread, stdout);
}
}
這 300 多行代碼可謂 中規中矩,沒有什麼高深的技巧,相當樸素,和剛入門的網絡編程初學者寫的代碼差不多。甚至在 代碼書寫格式 上,遠不如那些被大量 CRUD 訓練過的開發者寫得規整。
這個小工具的核心邏輯非常簡單。首先解析參數中的 URL,從中分離出協議名稱、主機名和文件路徑➀;這裏使用了類似匹配正則表達式的處理方式:
%64[^\n:]:讀取最多 64 個字符,直到遇到:或換行符,用於提取協議(如 HTTP 或 GOPHER);://:匹配字符串中的://,用於分隔協議和主機名;%256[^\n/]:讀取最多 256 個字符,直到遇到/或換行符,用於提取主機名;%512[^\n]:讀取最多 512 個字符,直到遇到換行符,用於提取路徑。
接下來創建 socket②,然後連接到 HTTP 服務器或代理服務器③。連接成功建立後,便發送 HTTP GET 請求④。最後,處理 HTTP 響應的頭部⑤和主體⑥,説是處理,好像挺複雜,其實只不過是直接跳過頭部,然後把主體原樣輸出。
這段代碼還用到了 4 個輔助函數:
readline():從 HTTP 響應中讀取一行數據,並將結果存儲在給定的緩衝區中;send_get():發送一個 HTTP GET 請求,請求中包含Pragma: no-cache頭,以確保獲取最新數據(因為是匯率,不能獲取緩存中的舊數據);skip_header():跳過 HTTP 響應頭;GetHost():解析主機名或 IP 地址。
另外,在處理 gopher 協議時,總感覺這裏有個 bug,ppath 在使用之前沒有被正確初始化:
void main(argc,argv)
int argc;
char *argv[];
{
...
char proto[64];
char name[256];
char path[512];
char *ppath, *tmp;
int defport;
...
/* Parse <url> */
if (3 != sscanf(argv[argc - 1], "%64[^\n:]://%256[^\n/]%512[^\n]", proto, name, path)) {
fprintf(stderr, "<url> malformed.\n");
exit(-1);
}
if (!strcasecmp(proto, "HTTP"))
{
defport = 80;
}
else
if (!strcasecmp(proto, "GOPHER"))
{
defport = 70;
/* Skip /<item-type>/ in path if present */
if (isdigit(ppath[1])) // ❗️❗️❗️ <-- ppath未正確初始化
{
ppath = strchr(&path[1], '/');
if (ppath == NULL)
ppath = path;
}
}
有趣的是,1996 年這個時間點恰好與 蒂姆·伯納斯-李爵士 提出 “機器可讀 Web” (machine-readable web)和 “語義網” 概念的時期重合。伯納斯-李意識到,Web 不僅僅是為人類設計的,還應該為機器提供訪問和交互的能力。這個思路為之後的 Web 服務、API 和 RESTful 架構的發展鋪平了道路,而像 httpget 這樣的簡單工具則代表了程序員們開始實際操作 Web 數據的早期嘗試。另一個著名的下載工具 wget 也誕生於同時代。
據説 Daniel 在一次接受採訪時曾表示,他當初寫 cURL 只是為了查詢匯率,沒想到後來變成了全球最常用的網絡工具之一。他開玩笑地説道:“cURL 是世界上最成功的失敗項目,因為我根本沒打算讓它變得這麼大!”