今天,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 是世界上最成功的失败项目,因为我根本没打算让它变得这么大!”


da_miao_zi
1 声望0 粉丝

软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《计算机是怎样跑起来的》《自制搜索引擎》等。