这个是我一直以来的短板,对 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 ack
。ack
的值为 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
E....V@.@................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
。此时 socket
的 fd
为 5
。然后是给 socket
加上 非阻塞(NONBLOCK)
标志。接着是 connect
。也就是 connect
的时候进行的 3 次握手。
这个时候,curl
进程开始调用 poll
函数。确认 socket
是否已经准备好,是否可写。之后开始向此 socket
发送数据。对应的 push
标志位的 tcpdump
。
发送完毕之后,再一次确认此 socket
是否准备好被读取,确认可以读取。从此 socket
中读取数据。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于