计网八股
0. Basics
Ethernet Frame(帧)
IP Packet(分组)
TCP Segment(节)
1. 经典浏览器敲回车面试题
Assumption
- Our computer is connected to the network via Ethernet.
- Our computer already has an IP address (more on what the IP address is later).
- Our computer sits behind a NAT (this is the case in many home networks).
- Our ARP cache is clear.
- Our DNS cache is clear.
- Our router's ARP cache contains an entry for its default gateway.
- Our network is 192.168.0.0/24.
- The URL we're going to use is http://www.example.com.
- The DNS server's cache has an entry for www.example.com.
- We're going to ignore TCP sequence numbers.
Analysis
家庭路由器的对内IP就是内部的网关,对外IP是是公网IP。
HTTP是基于TCP的,,首先要经过TCP三次握手,分层去看TCP三此握手的数据包需要的信息:
- TCP:本地端口是随机分配的,HTTP的端口是80;
- IP:本地已知,目的IP未知(只有URL);
- Ethernet:本地MAC已知,目的MAC不是www.example.com的MAC,而是内部网关的MAC地址,也是未知(假设ARP缓存清空)
两个未知是通过DNS和ARP进行获取的。
DNS是基于UDP的协议,分层去看所需信息:
- UDP:本地端口是随机分配的,DNS的端口是53;
- IP:本地已知,目的的DNS IP是写在本地配置中的;
- Ethernet:本地MAC已知,目的MAC是内部网关的MAC地址未知
1.1 ARP请求包
发送MAC层的广播
- Ethernet:src = AA:AA:AA:AA:AA:AA,dst = FF:FF:FF:FF:FF:FF
注意ARP其实是二层的协议,但是ARP包中是包含了IP地址的(路由器的内部IP地址已知)。
获得内部网关的MAC地址。
1.2 DNS请求包
经过NAT地址转换,到达DNS服务器,服务器回复www.example.com的IP地址。
1.3 TCP握手以及HTTP请求
。。。
2. HTTP1.0和HTTP2.0的区别?
- 结论1:从HTTP/1.0到HTTP/2,都是利用TCP作为底层协议进行通信的。
- 结论2:HTTP/1.1,默认使用HTTP长连接(keep-alive),只要任意一方没有明确提出断开连接则保持TCP连接状态,减少了建立和释放TCP连接的消耗和延迟(实际就是复用连接);
- 长连接是应用层(用户态)实现的;
- web服务软件一般会提供超时时间选项(Nginx的keepalive_timeout 默认60s),如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,服务端就会关闭该连接;
- 每个长连接上最大能处理的请求数量(nginx 的 keepalive_requests ),长连接接收的请求数量如果超过上限,服务器会关闭连接;
- 在禁用长连接的情况下,web服务一般都是由服务端关闭连接(服务器会出现TIME_WAIT状态的连接);
- 结论3:HTTP/2,引入了多路复用:连接共享,一个连接上可以有多个request,提高了连接的利用率,降低延迟。
HTTP/1.0
- 无连接和无状态:HTTP/1.0 每次请求都会建立一个新的TCP连接,请求完成后立即断开连接。
- 文本协议:HTTP/1.0 是基于文本的协议,易于读取和调试。
- 请求/响应模型:每次请求对应一个响应。
- 无内置压缩支持:虽然HTTP/1.0 通过 Content-Encoding 头部可以支持响应体的压缩,但这不是内置的。
- 队头阻塞问题:多个请求需要按照先进先出(FIFO)的顺序进行,即前一个请求没有完成,后一个请求就不能开始。
- 简单的头部元信息:HTTP/1.0 中,请求和响应头是比较简单的。
- 缺乏优先级/依赖性:所有请求都是独立处理的。
HTTP/2.0
- 多路复用:一个单一的TCP连接可以同时处理多个请求和响应。
- 二进制协议:HTTP/2 是基于二进制的协议,这使得协议解析变得更快和更高效。
- 头部压缩:HTTP/2 使用 HPACK 算法来压缩头部,以减少数据传输的大小。
- 服务器推送:服务器可以主动发送额外的资源,以加速客户端的页面加载。
- 优先级和依赖性:HTTP/2 可以设置请求之间的优先级和依赖性。
- 更强的流控制:除了TCP流控制,HTTP/2 还提供了其自己的流控制机制。
- 更安全(相对而言):虽然 HTTP/2 自身不是加密协议,但实际应用中,几乎所有的客户端和服务器都要求通过 HTTPS(即 HTTP over TLS)来使用 HTTP/2。
总体而言,HTTP/2 通过多种优化提供了比 HTTP/1.x 更高的性能,包括更低的延迟和更高的吞吐量。这些改进是为了应对现代网页日益增加的复杂性和需求。
3. TCP
TCP is a reliable byte stream protocol for the Internet.
1983年4.2BSD发布,第一个广泛应用的TCP实现。
一个比喻:用寄明信片的方式传送一本书(明文,乱序,可能丢);
To transmit a stream of data, TCP breaks the data stream into segments for transmission through the Internet, and resembles the segments at the receiving side to recreate the data stream.
How to be reliable and sequence-preserving?
- Assign every byte in the stream a sequence number (including start(SYN) / end(FIN) of stream);
- SYN和FIN就是字节流的起始和结束字节;
- 纯ACK不占用序列号(纯确认报文不在byte stream中);
- Cumulative positive acknowledge;
- Retransmit when didn't get ACK in time.
TCP is connection-oriented
- Connection: certain status infomation that TCPs initialize and maintain for each data stream, including including sockets, sequence numbers, and window sizes.
核心思想
- Cumulative positive acknwoledgement (累计确认) with retransmission;
- Flow control (sliding window);
- Congestion control;
(2&3 are optional, without sliding window is Single-MSS Stacks)
应用
FTP
HTTP / HTTPS
实现
三分天下:
- BSD iOS / maxOS
- Linux Android / Chrome OS
- Windows PC / Laptop
互联网绝大多数信息都是在这三个实现间流转。
连接建立与释放
三次握手
客户端需要创建socket并调用connect()
函数,socket()
函数返回的描述符默认是active socket,即需要主动发起连接的socket。
服务端在创建socket后涉及到三个操作:
bind()
:绑定IP与Port;listen()
:开启监听- socket进入listen状态(成为listening socket);
- 创建一个sock加入到一个全局的哈希表中;
- 内核为该listening socket分配半连接队列(SYN queue)和全连接队列(ACCEPT queue);
accept()
:等待连接- 如果全连接队列为空则阻塞;
- 否则从全连接队列返回一个新的socket fd(connected socket)
服务端收到第一次握手后,会将一个状态为SYN_RECV
的socket加入半连接队列(SYN queue),半连接队列是一个哈希表;
服务端收到第三次握手后,会将对应状态为SYN_RECV
的socket从半连接队列转移到全连接队列,状态转换为ESTABLISHED
,全连接队列是一个链表。
三次握手的主要目的:prevent old duplicate connection initiations from causing confusion (不是重传)
四次挥手
对socket
执行 close()
或 shutdown()
方法会发出FIN
(shutdown()
读不会发送FIN)。但实际上,只要应用程序退出,不管是主动退出,还是被动退出(因为一些莫名其妙的原因被kill
了), 都会发出 FIN
。
四次挥手中的第三次挥手,是由被动方主动触发的,比如调用close()
。
如果由于代码错误或者其他一些原因,被动方就是不执行第三次挥手,这时候,主动方会根据自身第一次挥手的时候用的是 close()
还是 shutdown(fd, SHUT_WR)
,有不同的行为表现。
- 如果是
shutdown(fd, SHUT_WR)
,说明主动方其实只关闭了写,但还可以读,此时会一直处于 FIN-WAIT-2, 死等被动方的第三次挥手。 - 如果是
close()
, 说明主动方读写都关闭了,这时候会处于 FIN-WAIT-2一段时间,这个时间由 net.ipv4.tcp_fin_timeout 控制,一般是 60s,这个值正好跟2MSL一样 。超过这段时间之后,状态不会变成 `TIME-WAIT`,而是直接变成`CLOSED`。
主动发起关闭连接的一方,才会有 TIME-WAIT
状态。
需要 TIME-WAIT 状态,主要是两个原因:
- 防止历史连接中的数据(Lost Duplicate)被「主动关闭连接」一方后面相同四元组的连接错误的接收;
- 保证「被动关闭连接」的一方,能被正确的关闭;
RST
RST是TCP头部中的一个标志位,目的是在异常状况下关闭连接
- RST占用序列号,如果接收到的RST不在接收窗口内,该RST会被忽略;
- RST不需要ACK确认;
发送RST的几种情况:
- 三次握手中,客户端收到不是自己期望的ACK,会发送RST;(三次握手的主要目的就是防止old duplicate connection initiation)
- 端口未监听:对于未进行过
listen()
的sock,收到SYN报文后内核会回复RST; - TCP报文的校验码错误,内核会直接丢弃,不会发送RST;
- 监听端口的应用程序崩溃,端口资源被释放,客户端内核发送消息,服务器内核会回复RST;
close()
系统调用:同时关闭接收和发送消息的功能- 本端:如果接收缓存区还有数据未读,提前close会清空接收缓存区,发送RST;
- 远端:远端close后收到数据,就会回一个RST;
TCP Keep-alive
TCP 的 Keepalive 这东西其实就是 TCP 的保活机制。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
重传
- 超时重传
- 快速重传:建立在滑动窗口的基础上,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
- SACK (Selective Acknoledgement)
- D-SACK (Duplicate Sack)
TCP流量控制
流量控制(flow control):to avoid network layer delivers data faster than application layer removes from socket buffer. The receiver controls sender, so sender won't overflow receiver's buffer by transmitting too much, too fast.
流量控制是为了避免发送方数据填满接收方的缓存。
接收方通过TCP头部中的rwnd字段控制发送方的发送速率,rwnd即为接收方的空闲buffer长度。
TCP拥塞控制
最早的TCP是没有拥塞控制的,1988年加入的。
拥塞(Congestion): too many senders sending too much data too fast for network to handle. (router buffers)
拥塞窗口(cwnd)是发送方维护的一个的状态变量,代表发送方的sending rate,它会根据网络的拥塞程度动态变化的
- 只要网络中没有出现拥塞,cwnd 就会增大;
- 但网络中出现了拥塞,cwnd 就减少;
加入了拥塞窗口的概念后,发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
- send cwnd bytes, wait RTT for ACKS, then send more bytes
拥塞控制的思想是AIMD(Additive Increase Multiplicative Decrease),主要是四个算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
慢启动
每次发送cwnd个字节,从1开始随RTT指数增长直到ssthresh (slow start threshold)(是丢包出现前的cnwd的一半)
- 收到一个 ACK 确认应答后,cwnd 增加 1
拥塞避免
线性增长
- 每当收到一个 ACK 时,cwnd 增加 1/cwnd
直到发生超时重传/快速重传
超时重传:拥塞发生算法;
快速重传:快速回复算法;
拥塞发生
如果发生超时重传:
ssthresh
设为 cwnd/2
;
cwnd
重置为 1
(是恢复为 cwnd 初始化值,这里假定 cwnd 初始化值 1,Linux 针对每一个 TCP 连接的 cwnd 初始化值是 10)
快速恢复
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的 ACK,那么 cwnd 增加 1(加 1 代表每个收到的重复的 ACK 包,都已经离开了网络);