从 socket 实现一个简化版 wget

结论速览

  • 最小 wget 的流程是解析 URL、DNS、建立 TCP、发送 HTTP 请求、接收响应、去掉 Header、保存 Body。
  • DNS 通常不是简单的内核 syscall,由 libc resolver 配合网络 socket 完成,接口是 getaddrinfo()。
  • socket(AF_INET, SOCK_STREAM, 0) 里 AF_INET 是 IPv4、SOCK_STREAM 是 TCP,内核返回文件描述符。
  • connect() 背后的 ARP、SYN/ACK、重传、IP、Ethernet 全部由内核处理,应用不用自己实现 TCP。
  • HTTP Header 与 Body 用 \r\n\r\n 分隔;HTTPS 多一层 TLS,涉及证书、密钥交换、握手,学习阶段先用 HTTP 地址。

1. HTTP 下载本质

最简单的 wget 可以分成:

1
2
3
4
5
6
7
8
9
10
11
12
13
解析 URL
↓
DNS
↓
建立 TCP
↓
发送 HTTP 请求
↓
接收 HTTP 响应
↓
去掉 Header
↓
保存 Body

例如:

1
http://example.com/test.bin

可解析成:

1
2
3
Host = example.com
Port = 80
Path = /test.bin

2. DNS

可使用:

1
getaddrinfo()

例如:

1
2
struct addrinfo *res;
getaddrinfo("example.com", "80", NULL, &res);

它负责把域名解析成网络地址。

DNS 并不是一个简单的 Kernel syscall,通常由 libc resolver 配合网络 socket 完成。

3. 创建 TCP socket

1
int fd = socket(AF_INET, SOCK_STREAM, 0);

其中:

1
2
3
4
5
AF_INET
= IPv4

SOCK_STREAM
= TCP

Kernel 会返回一个文件描述符,例如:

1
3

4. 建立 TCP 连接

1
connect(fd, ...);

背后由 Kernel 处理:

1
2
3
4
5
6
ARP
TCP SYN
TCP ACK
重传
IP
Ethernet

应用不需要自己实现 TCP。

5. 发送 HTTP 请求

例如:

1
2
3
4
5
6
7
const char *req =
"GET /test.bin HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n"
"\r\n";

send(fd, req, strlen(req), 0);

6. 接收响应

1
2
3
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) {
...
}

服务器返回:

1
2
3
4
5
HTTP/1.1 200 OK
Content-Length: ...

<空行>
文件内容

HTTP Header 和 Body 通常用:

1
\r\n\r\n

分隔。

7. 保存文件

创建文件:

1
2
3
4
5
int out = open(
"test.bin",
O_CREAT | O_WRONLY | O_TRUNC,
0644
);

然后:

1
write(out, data, len);

最终调用链:

1
2
3
4
5
6
7
8
9
10
11
recv
↓
应用程序
↓
write
↓
Kernel VFS
↓
文件系统
↓
存储设备

8. 最小 wget 所需接口

核心 API:

1
2
3
4
5
6
7
8
getaddrinfo
socket
connect
send
recv
open
write
close

因此通过实现一个简化 wget,可以一次学习:

1
2
3
4
5
6
DNS
TCP
socket
HTTP
文件 I/O
Linux API

9. 为什么先不做 HTTPS

HTTPS 比 HTTP 多一层 TLS:

1
2
3
4
5
6
7
HTTP
↓
TLS
↓
TCP
↓
IP

TLS 涉及:

1
2
3
4
5
证书
密钥交换
加密算法
握手
认证

实际项目一般使用:

1
2
3
OpenSSL
mbedTLS
wolfSSL

学习阶段先做:

1
http://192.168.x.x/file.bin

更容易理解底层机制。