从 http 到 kernel(bpf)(一)

这个是我一直以来的短板,对 tcp/ip 理解地不够深入,所以我打算花一点时间,把这个部分整理出来。看看到底网络是一个什么样的东西。一点一点去看。希望能把这个部分学完并且整理完。

如果可能的话,最终希望能实现一个简单的 tcp/ip 协议栈。

从 http 到 tcp

http 服务端

首先我们需要一个简单的 http 服务器。因为当下的 http 服务器带有 https 服务。最快拥有一个 http 服务器的方式,是下载 nginx,修改一下端口参数,这里修改 nginx.conf,将 listen 改为 65500

...
    #gzip  on;

    server {
        listen       65500;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
...

然后运行 nginx

客户端

这里客户端使用 curl

curl -vvv http://127.0.0.1:65500
*   Trying 127.0.0.1:65500...
* Connected to 127.0.0.1 (127.0.0.1) port 65500 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:65500
> User-Agent: curl/7.83.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.20.2
< Date: Sat, 11 Jun 2022 13:17:04 GMT
< Content-Type: text/html
< Content-Length: 612
< Last-Modified: Sun, 28 Nov 2021 12:07:18 GMT
< Connection: keep-alive
< ETag: "61a370f6-264"
< Accept-Ranges: bytes
< 
....
* Connection #0 to host 127.0.0.1 left intact

可以看到这里打印出来的相关 header。正文被省略。

那用 tcpdump 抓个包看看,

sudo tcpdump -ni any port 65500
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
21:16:04.737246 lo    In  IP 127.0.0.1.35766 > 127.0.0.1.65500: Flags [S], seq 1616008903, win 65495, options [mss 65495,sackOK,TS val 579690500 ecr 0,nop,wscale 7], length 0
21:16:04.737302 lo    In  IP 127.0.0.1.65500 > 127.0.0.1.35766: Flags [S.], seq 323354686, ack 1616008904, win 65483, options [mss 65495,sackOK,TS val 579690500 ecr 579690500,nop,wscale 7], length 0
21:16:04.737342 lo    In  IP 127.0.0.1.35766 > 127.0.0.1.65500: Flags [.], ack 1, win 512, options [nop,nop,TS val 579690500 ecr 579690500], length 0
21:16:04.737476 lo    In  IP 127.0.0.1.35766 > 127.0.0.1.65500: Flags [P.], seq 1:80, ack 1, win 512, options [nop,nop,TS val 579690500 ecr 579690500], length 79
21:16:04.737506 lo    In  IP 127.0.0.1.65500 > 127.0.0.1.35766: Flags [.], ack 80, win 511, options [nop,nop,TS val 579690501 ecr 579690500], length 0
21:16:04.737630 lo    In  IP 127.0.0.1.65500 > 127.0.0.1.35766: Flags [P.], seq 1:239, ack 80, win 512, options [nop,nop,TS val 579690501 ecr 579690500], length 238
21:16:04.737683 lo    In  IP 127.0.0.1.35766 > 127.0.0.1.65500: Flags [.], ack 239, win 511, options [nop,nop,TS val 579690501 ecr 579690501], length 0
21:16:04.737730 lo    In  IP 127.0.0.1.65500 > 127.0.0.1.35766: Flags [P.], seq 239:851, ack 80, win 512, options [nop,nop,TS val 579690501 ecr 579690501], length 612
21:16:04.737750 lo    In  IP 127.0.0.1.56248 > 127.0.0.1.65500: Flags [.], ack 851, win 507, options [nop,nop,TS val 579690501 ecr 579690501], length 0
21:16:04.738298 lo    In  IP 127.0.0.1.56248 > 127.0.0.1.65500: Flags [F.], seq 80, ack 851, win 512, options [nop,nop,TS val 579690501 ecr 579690501], length 0
21:16:04.738437 lo    In  IP 127.0.0.1.65500 > 127.0.0.1.56248: Flags [F.], seq 851, ack 81, win 512, options [nop,nop,TS val 579690501 ecr 579690501], length 0
21:16:04.738496 lo    In  IP 127.0.0.1.56248 > 127.0.0.1.65500: Flags [.], ack 852, win 512, options [nop,nop,TS val 579690501 ecr 579690501], length 0

那根据三次握手规则,起初由客户端发起请求,客户端从端口 56248 发送到 65500。发送 Syn 包。此时 seq 的值为 1616008903。这里的包的大小是 0。这里的窗口大小是 65495

服务端在收到客户端的 syn 包后,返回一个 syn ackack 的值为 syn 包的 seq 的值加一。此时窗口大小为 65483

最后,服务器回复一个 ack 包。三次握手完毕。接下来开始数据交互。

客户端首先发起一个 push 包。看看具体这个包的内容。

21:52:41.917195 lo    In  IP 127.0.0.1.35766 > 127.0.0.1.65500: Flags [P.], seq 1:80, ack 1, win 512, options [nop,nop,TS val 581887680 ecr 581887680], length 79
[email protected]@................P.n.b.......w.....
"..."...GET / HTTP/1.1
Host: 127.0.0.1:65500
User-Agent: curl/7.83.1
Accept: */*

可以看得很清楚,是发送了 http GET 的请求头。对应的正是 curl 的请求头。那此时 curl 进程到底都做了一些什么呢。我们从对应的 SYS_CALL 来看。

strace -ftt -s 999 curl http://127.0.0.1:65500

....
21:57:55.034981 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 5
21:57:55.035136 setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
21:57:55.035173 setsockopt(5, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
21:57:55.035208 setsockopt(5, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
21:57:55.035240 setsockopt(5, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
21:57:55.035272 fcntl(5, F_GETFL)       = 0x2 (flags O_RDWR)
21:57:55.035301 fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0
21:57:55.035331 connect(5, {sa_family=AF_INET, sin_port=htons(65500), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
21:57:55.035498 poll([{fd=5, events=POLLPRI|POLLOUT|POLLWRNORM}], 1, 0) = 1 ([{fd=5, revents=POLLOUT|POLLWRNORM}])
21:57:55.035550 getsockopt(5, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
21:57:55.035593 getpeername(5, {sa_family=AF_INET, sin_port=htons(65500), sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 0
21:57:55.035638 getsockname(5, {sa_family=AF_INET, sin_port=htons(35766), sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 0
21:57:55.035834 sendto(5, "GET / HTTP/1.1\r\nHost: 127.0.0.1:65500\r\nUser-Agent: curl/7.83.1\r\nAccept: */*\r\n\r\n", 79, MSG_NOSIGNAL, NULL, 0) = 79
21:57:55.036729 poll([{fd=5, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 1 ([{fd=5, revents=POLLIN|POLLRDNORM}])
21:57:55.036781 recvfrom(5, "HTTP/1.1 200 OK\r\nServer: nginx/1.20.2\r\nDate: Sat, 11 Jun 2022 13:57:55 GMT\r\nContent-Type: text/html\r\nContent-Length: 612\r\nLast-Modified: Sun, 28 Nov 2021 12:07:18 GMT\r\nConnection: keep-alive\r\nETag: \"61a370f6-264\"\r\nAccept-Ranges: bytes\r\n\r\n<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", 102400, 0, NULL, NULL) = 850
....

首先是创建了 socket。此时 socketfd5。然后是给 socket 加上 非阻塞(NONBLOCK) 标志。接着是 connect。也就是 connect 的时候进行的 3 次握手

这个时候,curl 进程开始调用 poll 函数。确认 socket 是否已经准备好,是否可写。之后开始向此 socket 发送数据。对应的 push 标志位的 tcpdump

发送完毕之后,再一次确认此 socket 是否准备好被读取,确认可以读取。从此 socket 中读取数据。

参考

  • 网络
    118 引用 • 169 回帖 • 3 关注
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    872 引用 • 921 回帖 • 54 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...