動態

詳情 返回 返回

難以想象!cURL的前身竟是如此粗糙的300多行代碼? - 動態 詳情

今天,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 是世界上最成功的失敗項目,因為我根本沒打算讓它變得這麼大!”

user avatar u_16985197 頭像 jianweilai 頭像 anjingdexiaoyanyao_ciaxxr 頭像 idiomeo 頭像 chenzhuodeyagao 頭像 danxiaodezixingche 頭像 shenchendebanma 頭像
點贊 7 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.