计网八股文
HTTP
经典面试题:当输入网址后,在网页渲染页面前,期间发生了什么
- 浏览器工作的第一步就是对URL进行解析, 生成发送给服务器的请求信息。下面对URL组成成分进行解析:
URL组成成分:
- 协议:http
- 主机名:www.baidu.com
- 数据源(目录名和文件名,用斜杠分隔)
当没有指定数据源时, 服务器会返回一个默认的HTML文件,通常是index.html。
Http基本概念
q1:http是什么?
http名为超文本传输协议。顾名思义,也就是能够拆分成三部分:超文本、传输、协议。
- 超文本:服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态
- 传输
- 协议:计算机之间交流通信的行为规范、相关的各种控制和错误处理方式。
q2:http常见的状态码有哪些?
- 1xx类:提示信息,中间状态
- 2xx类:成功状态
- 200 OK:请求成功
- 204 No Content:请求成功,但没有返回内容,即没有body
- 206 Partial Content:请求成功,但只返回部分内容
- 3xx类:重定向状态
- 301 Moved Permanently:永久重定向
- 302 Found:临时重定向
- 304 Not Modified:资源未修改,使用缓存
- 4xx类:客户端错误
- 400 Bad Request:请求错误
- 401 Unauthorized:未授权
- 403 Forbidden:禁止访问
- 404 Not Found:资源未找到
- 5xx类:服务端错误
- 500 Internal Server Error:服务器内部错误
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:服务不可用
- 504 Gateway Timeout:网关超时
q3:HTTP常见字段有哪些?
- Host字段:主机名,用于指定服务器的域名。
- Content-Length字段:表示本次回应的数据的长度
- Connection 字段:用于客户端要求服务器用Http长连接机制,以便其他请求复用。只要任意一端没有主动断开连接,则保持TCP连接状态。
- Connection: keep-alive:保持连接,客户端向同一个服务器的多个请求是可以复用同一个连接的。
- Content-Type字段:表示本次回应的数据类型
- Accept字段:表示客户端能够接受的数据类型
- Content-Encoding字段:表示数据的压缩方式
- Accept-Encoding字段:表示客户端能够接受的压缩方式
GET和POST
q1:GET和POST的区别是什么?
GET是拉的过程,POST是推的过程。GET从语义上说是从服务器获取指定的资源,POST从语义上说是根据请求负荷(报文body)对指定的资源进行处理。
HTTP 缓存技术
对于像GET这样的安全的幂等的调用,其结果是可以被缓存的,以减轻服务器负担。缓存有两种实现方式:强制缓存和协商缓存。
强制缓存: 浏览器判断缓存没有过期,直接使用浏览器的本地缓存。
SSL/TLS协议运行机制
q1:http和https的区别?
- http是超文本传输协议,信息是明文传输,存在安全风险的问题。由于其明文传输的特性,存在三种安全问题:通信内容的窃听、传输内容的篡改、服务端的冒充风险。
- https是在http层和tcp/ip层之间加入了ssl/tls协议,使得传输的内容加密,保证了传输的安全性。https在与服务端进行三次握手之后,会进行第三次握手的时候,会进行ssl/tls协议的握手,握手成功后,才会进入加密报文的传输。
- 两者的端口也不同:http默认端口是80,https默认端口是443。
q2:那你来解释一下SSL/TLS协议的握手过程?
我直接用手画的两张图来解释:
SSL/TLS握手流程:
session key 明文加密解密过程:
q3:请你来回答一下半对称加密?
公钥加密,私钥解密:这个目的是为了保证内容传输的安全,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;(能确认消息是由客户端发送的)
私钥加密,公钥解密:这个目的是为了保证消息不会被冒充,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的。(能确认消息是由服务端发送的)
哈希值加密可以保证数据的完整性,但不能保证消息来源的可靠性。所以需要结合半对称加密方式来保证消息的来源是可靠的。
q4:什么是数字证书? 数字证书是用来解决服务端冒充的问题的。数字证书是由第三方机构颁发的,能够证明其来源的合法性,防止服务端被冒充的风险。这里可以详细阅读q2中手画的SSL/TLS握手流程图,其中确保握手安全就是通过半对称加密和数字证书来保证的。
HTTPS的应用数据的完整性
q1:HTTPS是如何保证数据的完整性的?
首先我们要知道TLS协议分为两个部分:握手协议和记录协议。
- 握手协议就是我前面手画的SSL/TLS握手流程,用来保证通信双方的身份和生成会话密钥。
- TLS 记录协议负责保护应用程序数据并验证其完整性和来源
下面这张图详细介绍了TLS记录协议的流程图:
- 首先,消息被分割成多个较短的片段,然后分别对每个片段进行压缩。
- 接下来,经过压缩的片段会被加上消息认证码(MAC 值,这个是通过哈希算法生成的),这是为了保证完整性,并进行数据的认证。通过附加消息认证码的 MAC 值,可以识别出篡改。
- 与此同时,为了防止重放攻击,在计算消息认证码时,还加上了片段的编码。
- 再接下来,经过压缩的片段再加上消息认证码会一起通过对称密码进行加密。
- 最后,上述经过加密的数据再加上由数据类型、版本号、压缩后的长度组成的报头就是最终的报文数据。
q4:https一定安全吗? 在真正能够验证了服务端密钥的合法性的前提下,https是安全的。但是如果你使用了不安全的数字证书,那么这个违法的服务端就会进行代理,导致你的数据被窃取。所以在使用https的时候,一定要保证数字证书的合法性。
HTTP/1.1、HTTP/2、HTTP/3 演变
q1:HTTP/1.1 提高了什么性能
- 长连接
- pipeline网络传输可以并发的发送多个请求,而不是等待上一个请求的返回再发送下一个请求(但是只是针对客户端而言,服务端还是需要按顺序处理请求、而且也没有什么浏览器会使用这个技术)
q2:HTTP/2做了什么优化
- 头部压缩
- 二进制格式
- 并发传输
- 服务器主动推送资源
我得好好讲讲HTTP/2的并发传输!
首先我们知道在常规的Http/1.1中,存在队头阻塞的问题,但是在HTTP/2中很好的解决这个问题,引入了Stream的概念,并且多个Stream复用在一个TCP连接中。
从上图可以看到,1 个 TCP 连接包含多个 Stream,Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。
针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应。
但是!HTTP/2也是存在队头堵塞的问题,但是这个问题深入到TCP协议栈中。HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
q3:HTTP/3做了哪些优化
HTTP/3把HTTP/2的下层的TCP替换为UDP。但是大家可能会好奇,UDP不是无序且不安全的吗?实际上基于UDP的QUIC协议是一个基于UDP的安全可靠的传输协议,可以类似于TCP。下面来详细讲讲QUIC协议。
QUIC协议
QUIC协议可以从一下三个方面认识:
- 无队头阻塞;
- 更快的连接建立;
- 连接迁移;
无队头阻塞
- QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。
- QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。
- 所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。
就是说,QUIC可以把一次HTTP请求流化,并且与其他的流化的HTTP请求并行传输,并且互不影响,这样就能解决tcp阻塞问题。
更快的连接建立
QUIC协议的握手非常的快,只需要一个RTT(Round-Trip Time,往返时间)就能完成握手,而TCP至少需要两个RTT。因为握手的目的就是确认双方的连接ID,连接迁移时基于连接ID实现的,这是非常快的。
但是HTTP/3的QUIC协议并不是与TLS分层的,而是内部集成了TLS,所以在QUIC协议中,TLS的加密和解密是在QUIC协议中完成的,而不是在应用层完成的。再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商。
连接迁移
QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
RPC
q1:既然已经有了HTTP协议,那又为什么还要有RPC协议
IO 多路复用(非常关键、后台开发考察到的可能性非常大)
TCP协议栈
tcp协议和udp协议一起组成OSI模型中的传输层,负责互联网中端到端理念中,端的工作内容。熟悉tcp协议,明确tcp工作流程,是每一个后台开发甚至程序员的基本素养。
对于tcp的理解,我将按照需求驱动来讲解tcp协议的各个部分。
TCP基本认识
在认识tcp的第一步是要把tcp的segment结构图深深的刻进自己的dna里!
- 2字节:源端口号
- 2字节:目标端口号
端口的概念是为了标识占有信息,标识socket占用这个端口,体现连接性中的不可同时连接多个目标,是面向连接的重要体现。为什么面向连接要强调占用性,可以联想一下电话通信,我们要保证数据有序流式可靠传输,是不是在电话拨通时不能被其他人打扰,反映到tcp上就是建立连接的双方数据传输过程不被其他socket打扰。
- 4字节:序列号seq – 有序性
- 4字节:响应号ack – 可靠性
- 1字节:数据偏移offset,该值乘以4得到TCP头部长度,也是数据部分距离头部起始位置的偏移量,当没有“选项”时该值为5(即头部长度为20字节)。
- 3bits:保留位
- 9bits:ns、cwr、ece、urg、ack、psh、rst、syn、fin
NS:全称是Nonce Sum,其中Nonce表示number used once,即一次性数字。在TCP的上下文中,这是一种用于增加随机性和防御黑客预测序列号攻击的手段。
CWR:全称为 Congestion Window Reduced。它表示已经减小了拥塞窗口(Congestion Window)。
- 当接收方收到拥塞通知后,它可以通过设置CWR标志位来告知发送方已经采取了措施来减缓数据传输。
- 发送方也可以主动减小拥塞窗口,并设置CWR标志位,以通知接收方它已经采取了措施来缓解拥塞。
ECE:全称为 ECN-Echo,用于回应显式拥塞通知(ECN)。ECE标志位通常与CWR标志位一起使用。当TCP连接的一端收到设置了CE标志位的IP报文时,它可能减小拥塞窗口并设置CWR标志位,同时将ECE标志位设置为1来通知本端已经采取了拥塞控制措施。
URG:全称为Urgent Pointer,用于指示TCP报文中有需要尽快传送到应用层的紧急数据(Urgent Data)。URG标志位与紧急指针(Urgent Pointer)字段一起使用,紧急指针指定了相对于“序列号”的偏移量,表示紧急数据的结束位置。
ACK:全称为 Acknowledgment,指示TCP报文中的确认号字段是否有效。接收方用于通知发送方确认号之前的字节序列都已经成功接收。
PSH:全称为 Push Function,用于指示接收端应该尽快将收到的数据推送(Push)给应用层。在TCP中,数据流是被缓存的,并非每收到一个字节的数据就立即传递给应用层,而是等待收到一定的数据量后再一起传递给应用层。但是,有时应用层可能需要尽快收到数据,而不希望等待缓存区填满。这时就可以使用PSH标志位。
RST:全称为 Reset Connection。当一个TCP连接遇到异常或错误情况时,可以发送RST报文来快速中断或拒绝连接。
SYN:全称为 Synchronize Sequence Numbers,用于在建立TCP连接时进行序列号的同步。TCP使用三次握手来建立连接,其中SYN标志位在握手过程中扮演着重要的角色。
- FIN:全称为 Finish,用于在TCP连接的关闭阶段表示发送方已经完成数据的发送。TCP使用四次挥手来关闭连接,其中FIN标志位在挥手过程中扮演着重要的角色。
- 2字节:窗口 – 窗式传输
- 2字节:checksum – 可靠性
- 2字节:紧急指针
- 0~10字节:options
- data
上面就是一张tcp数据帧包括的所有内容了,是tcp数据传输的基本单元,也是tcp头是保证tcp工作的重要结构。每一个成员字段会在后面的学习中知道它的作用。
tcp协议始终围绕一个主题:克服底层互联网的分组交换、尽力而为带来的不可靠和无序的问题。这个想法要在我们理解每个tcp工作机制部分要时刻回忆起,体会它是如何为这个需求服务的。任何一门技术都是为了解决需求诞生的。
面向连接–连接建立、连接断开
面向连接这个概念其实非常抽象,很多人在完整学完tcp后,都无法讲清楚面向连接到底是什么意思,只是简单的理解为数据的传输是有关系状态的,需要维护传输数据的关系;
总结成一句话,为了保证数据传输的有序性和可靠性,需要在发送端和接收端之间维护一段时间的状态(关系)。可以想象成打电话,A和B打电话,A说的话在B处是有序且可靠的,但是代价就是要保持通话的关系,这就叫连接。所以也有一句笑话是这样说的,如果二战时期没有抵制电话网,可能就没有互联网了。
建立连接的过程是为之后保证数据有序可靠传输的准备工作,首先讲下建立连接的过程–三次握手。
TCP三次握手
首先我们要了解tcp握手的原因,原因其实就是获取数据可靠传输需要的信息,有两个:
- 确认对端在线,保证可靠性。
- 需要保证传输的包的顺序,所以要确认这次连接里传输数据的起始序列号。因为数据是双向传输的,所以需要两边都要确认对端序列号。保证有序性。
第一次握手和第二次握手,A知道了B是可连接的以及知道了B知道自己的起始序列号;第三次握手,B知道A是可连接的和A知道B的起始序列号。
给一张非常经典的图:
这里就有个很好的面试问题?当已经建立完两次握手之后,服务端如果收到另外一个客户端的ack,会让当前的连接完成第三次握手吗?
答案是当然不会。首先另外一个客户端的建立的是另一个连接,ip和port信息都不一样,如果这个时候服务端收到这个客户端的ack,会返回rst,表示没有遵循tcp协议连接过程,需要重新建立连接。
这里可以引出一个问题,如何分辨收到的ack到底是第一次发给我的,还是对于我之前发送的syn+ack的回应。答案是内核会通过tcp四元组进行查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline struct sock *__inet_lookup(struct net *net,
struct inet_hashinfo *hashinfo,
struct sk_buff *skb, int doff,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const __be16 dport,
const int dif, const int sdif,
bool *refcounted)
{
u16 hnum = ntohs(dport);
struct sock *sk;
sk = __inet_lookup_established(net, hashinfo, saddr, sport,
daddr, hnum, dif, sdif);
*refcounted = true;
if (sk)
return sk;
*refcounted = false;
return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
sport, daddr, hnum, dif, sdif);
}
先查询这个四元组是否已经在连接池里,然后再检查listener里是否存在连接,没有就直接返回send_reset, 发送rst。如果在连接池内就接受数据,如果在Listeners里就处理tcp状态,并且是TCP_LISTEN就进行第一次握手;如果状态时TCP_SYN_RECV状态,就要在缓存中记录客户端的syn包的内容,以便在收包过程中进行查找,也就是生成一个半连接体插入到半连接队列中。半连接队列的大小是backlog,表示可以同时连接的最大数量。
同时我们在考虑一个问题,如果说有攻击者估计发起多个客户端,只进行到第二次连接,这样不就导致服务端的半连接池被占满,无法进行新的连接吗?这就是著名的synflood攻击的原理。
解决方案是syncookie。既然sunflood能攻击的原因是,进行第一次握手后,服务端要缓存客户端信息在半连接上。我们可以不记录这个信息,这样就不需要缓存客户端信息在半连接上了。那我们又要如何知道发送来的ack是属于哪个半连接的呢?答案就是把第一次握手得到的信息放到发送回的数据包中,让客户端在发送的时候在发回来,然后解析这个信息和四元组就可以知道验证。为了要保证封装进数据包的内容够小,先将信息做哈希做哈希运算,这个运算出来的数据就叫做cookie。
下面来详细讲一下tcp三次握手时,内核到底发生了什么,各种标志时如何确定的
首先我们需要知道,tcp三次握手的过程实际上时连接体状态机的转变过程,服务端和客户端有各自的不同状态。服务端调用完listen后,会从CLOSE态转变为LISTEN态,客户端调用完connect后吗,会从CLOSE态转变为SYN_SENT态。这个时候客户端发送来一个数据包,这个数据包是如何组成的上文有讲,我们要知道里面有两个非常重要的位信息:SYN,表示该包是为了建立连接,seq,序号字段,表示该包在客户端的产生序号。
这个connect调用后客户端发送的第一份报文称为SYN报文。
第一次的seq时客户端随机生成的client_isn,SYN的标志置为1。
服务端接受到SYN报文后,拿到客户端的四元组信息,服务端也随机初始化自己的SYN+ACK报文序号seq(server_isn),将此序号填入seq,将接受到的client_isn+1填入ack,SYN和ACK标志都置为1,然后发送给客户端,此时服务端的状态变为SYN_RECV。
服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态
以上就是三次握手的过程。注意在第三次握手时,报文是可以携带应用层数据的,这个时候客户端可以开始传输数据了。
listen backlog
这里说明下listen系统调用的backlog参数。我们要知道backlog队列是什么。backlog队列也叫半连接队列,储存服务端中进入seq_recv状态的socket。
q1:为什么TCP握手需要三次?
- 阻止重复的历史连接的初始化
- 三次是同步双方初始化序列号的最小握手次数
- 避免浪费资源
阻止重复的历史连接的初始化
假设客户端发送了一个连接请求到服务端,但是由于网络的原因,服务端没有收到这个请求,那么客户端会再次发送一个连接请求,这个时候服务端就会收到两个连接请求,如果只需要两次握手,那么服务端就会认为这两个连接请求是同一个连接,这样就会导致历史连接的初始化,这样就会导致资源的浪费。但是如果有第三次握手,客户端就可以向服务端发送RST回复,说明这个连接已经被中止了,正在重新连接。这样就可以防止被网络等各种原因阻塞的历史连接导致了连接被重置。
那如果第二次握手之后,客户端发送完ack进入established态,发送的ack包丢了,导致服务端没有进入esblished态,是否会造成客户端发送数据丢失?
不会,因为客户端在建立连接之后的发送的数据包都会带有ack确认号,且和ACK包里的ack是一样的。
同步双方初始化序列号
首先来谈谈序列号的作用。服务端客户端双方都需要维护序列号,序列号的作用:
- 识别重复发送的数据包
- 根据数据包的序列号按序接受
- 可以标识发送出去的数据包哪些使被客户端收到的,收到的数据包seq = 接受的ack-1
这样在三次握手的时候,双方都可以初始化自己的序列号,这样就可以保证数据包的顺序。
避免浪费资源 浪费资源这个问题其实在好处一就已经解释了,如果tcp只用两次握手,那么就会将历史连接设置为Extablished态,这样就会浪费资源。
TCP 连接断开
然后我们在来讲解一下建立连接的双方如何断开连接。断开连接的过程主要是四次挥手。
下面来详细讲讲各个阶段发生了什么。
- 客户端打算关闭连接,此时会发送一个TCP首部中FIN位置为1的报文,即FIN报文。之后客户端进入FIN_WAIT_1状态。
- 服务端收到FIN报文后,发送回一个ACK报文,进入CLOSE_WAIT状态。然后进入处理。
- 客户端收到ACK报文后,进入FIN_WAIT_2状态。
- 服务端处理完后,发送一个FIN报文,进入LAST_ACK状态。
- 客户端收到FIN报文后,发送回一个ACK报文,进入TIME_WAIT状态。
- 服务端收到ACK报文后,直接进入CLOSED状态,释放相关TCP连接资源。
- 客户端经过2MSL后,进入CLOSED状态,释放相关TCP连接资源。自此TCP四次握手结束。
注意:和三次握手不同,四次握手是可以由双方主动发起的,也就是说这张图是可以反转的,主动断开连接的一方有TIME_WAIT状态。
q1:TCP四次挥手为什么需要四次?
首先挥手过程需要发送两次FIN报文,且需要被ACK确认。然后这两次FIN发送又有不同的作用,第一次发送是由主动方发送的FIN报文,表示不在发送数据但是还可接受数据。被动方接受到FIN报文后,会发送ACK表示知道收到FIN了,但是被动方可能还有在接受FIN报文之前的数据没有发送出去,当所有待处理的数据全部发送完后,被动方就会发送FIN报文,表示自己也不发送数据但可以接受数据。主动方接收到FIN后向被动方发送ACK报文,表示知道了FIN报文。被动方接收到ACK报文后,就可以关闭连接了。
但是主动方需要有个time_wait状态,这个状态是为了保证被动方接收到ACK报文,然后再关闭连接。这个状态的时间是2MSL,MSL是最大报文段生存时间,一般是30s,所以time_wait状态是60s。至于具体为什么需要time_wait状态,下面会详细介绍。
q3:四次挥手如果分别丢失会发生什么?
第一次挥手丢失 && 第二次挥手丢失:
我们要知道四次挥手是在那个调用中完成的,是close调用中。客户端调用close调用后就会发送FIN报文。如果收不到来自被动方的ACK报文,就会触发超时重传机制。重发次数由系统内置参数决定就是tcp_orphan_retries。如果重发次数用完,直接断开连接。(超时重传每次等待时间是上一次的两倍)
第三次挥手丢失:
主动方收到来自服务端的ack回复后,就会进入FIN_WAIT_2状态,这个状态不会持续太久,60秒内没有收到来自被动方的FIN直接主动断开连接进入TIME_WAIT状态。
服务端收到FIN报文后,内核会自动回复ACK。此时进入CLOSE_WAIT状态。当应用层在被动方进入CLOSE_WAIT状态后调用read / write时,调用会返回错误表示连接已断开,此时应用层就会调用close关闭连接。调用了close后,内核就会发送FIN报文,进去LAST_ACK状态。同样如果收不到ack就会超时重传,超过重传次数就会直接断开连接。
第四次挥手丢失:
一张图直接说明第四次挥手丢失会发生什么:
这样就告诉我们为什么需要TIME_WAIT状态。在TIME_WAIT状态内,主动方可能会不断收到来自被动方重传的FIN报文,这样主动方就需要不断回复ACK,并且重置MSL计时器。
TIME_WAIT状态
q4:为什么TIME_WAIT需要定时2msl?
MSL: Maximum Segment Lifetime,最大报文段生存时间,是指一个报文在网络中最长的存活时间。这个时间是由网络中最长的存活时间决定的,一般是30s。所以2MSL就是60s。MSL的值是由TTL影响的。TTL是IP段可以经过的最大路由数,一般是64。内核认为TTL跳过64次需要的时间是30秒,如果30秒内没有到达,就说明网络包已丢失。一来一回需要等待 2 倍的时间。可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
q5:为什么需要TIME_WAIT状态?
为什么需要time wait呢?在发送完ack后直接关闭连接不就好?为什么只有主动方才需要time_wait状态呢?
- 防止历史连接中的数据,被后面相同的四元组的连接错误的接受;
- 保证被动关闭连接的一方能够正确的被关闭。
q6:服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。
所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。 那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。
我们先来分析一个普通的 TCP 服务端的流程:
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生 对方连接关闭时,我方调用 close
可能导致服务端没有调用 close 函数的原因,如下。
第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。 不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。
第二个原因: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。 发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。
第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。 发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。
第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。 可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close。
连接阶段
建立连接是tcp面向连接的体现,是保证数据流式传输,有序可靠机制正常工作的提前准备工作。tcp的核心还是在连接阶段,这里你会知道tcp到底是如何保证数据传输的有序可靠的。
确认机制–ack,每个tcp头中都包括ack,ack表示对面下一个发送的tcp报文中的seq,也是对对面上一个发送的报文被系统接收的确认。ack机制不仅仅是连接建立、断开阶段中发挥作用,在连接阶段也发挥着重要作用。
可靠性保障—重传机制
数据包在网络中传输,极有可能遇到传输过程中遇到路由器丢包的情况,这是由于互联网尽力而为的设计思想,网络层不保证数据包一定到达终点。为了保证可靠性,如果发现丢包了咋办,那只能重新发送叻。这就是重传的必要性。发送端会检测丢包,根据检测方法的不同,划分为两种重传机制。注意这两种重传机制是同时运行的。
超时重传
发送端一段事件收不到ack,会认为发送的数据包丢失了,会重新发送。超时重传不能触发太多次,会造成网络资源的浪费,如果对方已经挂了,不管你发送多少数据包都不会有回应的。
可以配置tcp_retries2
来决定重传的次数问题。超时重传的超时时间是RTO的倍数,第一次就1倍,第二次就2倍;RTO / retransmission timeout 的计算是根据RTT / Round-Trip Time来的。如何计算RTT呢?RTT是数据报文从发送端传送到接收端再返回发送端所经历的时间。RTT的计算依赖于option中的timestamp中的TSval / 发送时刻,TSecr / 接收时刻,会在发送端设置。
RTT = TSecr - TSval
超时重传有个缺点,就是重传效率太低,每次重传时间都是上一次的倍数。这时候提出优化的方案,快速重传。快速重传是通过ack时钟来驱动的。
快速重传
什么是ack时钟,就是每次接收到ack就是一次tick。当接收到三次重复的ack时,就会触发快速重传。
选择性快速重传
了解窗口传输的同学知道,快速重传会把窗口内未被确认的数据全部重传,但是窗口内可能只有头部的一段数据未被确认,后面的数据都被正确的发送到接收方。那么发送端如何知道之后发送的哪些数据被ack了呢,这里就用到了选择重传。选择重传 / sack,会在option字段中填充sack块,sack块包括8字节,4个字节的失序数据的起点序列号,4个字节的失序的结尾序列号+1;一个报文段中最多携带三个sack,也就是重传只能最多重传三个丢失数据。
延迟确认
接收端并不会一收到数据包就发送ack报文,而是会开启定时器等待其超时,再发送ack报文。等待时间段 ,发送端极有可能会发送数据,那么ack信息就会携带在这个报文中,并关掉定时任务。这样就能减少网络中的ack报文的数量,减低网络中传输的数据量。
有序性保证—乱序恢复机制
接收端接收到乱序的数据时,如何处理。如何识别乱序呢,当接受到的数据的seq大于此时ack报文的ack时,就认定这个数据包是乱序的。接收端接收到乱序数据,不会立刻将其放入读取缓冲区中,而是会放入一个优先级有序队列。当收到当前ack的seq数据时,会检查队列的队头的报文的seq是不是当前ack,是则推出放入缓冲区并继续检查。
队列的底层数据结构是红黑树,红黑树是有序的数据结构。
流量控制
流量控制可以说是tcp中最重要的模块,他是保证整个互联网稳定工作的基础。所谓流量控制,就是控制发送端发送数据的上限,即下一次发送数据最多能够发送多少数据。
首先要控制的是两端,也就是互联网中的端到端;根据对端的状态,来解决自己的发送流量。tcp协议是全双工协议,双方地位是平等的,也就是既是发送端又是接收端。端会维护两个窗口,接收窗口和发送窗口,也就是两个缓冲区。
发送窗口划分为四个部分:已确认已发送、发送但未确认、空余窗口、不可使用;
1
2
3
4
----------------------------------------------------------
| | | |
SND.UNA SND.NXT SND.WND
----------------------------------------------------------
SND.UNA: 等待确认的第一个seq序列号
SND.NXT: 下一个发送数据时会使用的seq序列号
SND.WND: 发送窗口大小
接收窗口也被划分为三部分:已接受未消费、可接收、不可接收
1
2
| |
RECV.NXT RECV.WND RECV.NXT: 下一个发送数据时会发送的ack
RECV.WND: 接收窗口大小
接收方每次发送数据包,会将自己的RECV.WND - RECV.NXT发送给发送端作为发送端的awnd。但是以接收窗口当前可用空间作为发送窗口是存在问题的,原因仍然是网络传输的不稳定。由于网络窗口带来的延迟性,发送窗口的设置并不具备实时性;同时网络存在丢包问题,发送的新的窗口信息的包可能会丢失,同样带来不实时性的问题。可能会带来两个问题:零窗口和糊涂窗口。
零窗口: 接收端处理数据太慢,导致可用接收窗口为0并且设置到发送窗口;在接收端发送新的可用接收窗口时,报文包丢失导致发送端一直不发送数据,导致死锁。解决方法很简单,就是在进入零窗口阶段,单独发送探测报文探测是否存在可用接收窗口。
糊涂窗口: 原因有两个:接收端处理数据太慢,导致可用接收窗口大小小于MSS,发送端发送的报文携带的数据太少,导致网络传输效率变慢。另一个单独是应用层面的问题,应用层每次发送的数据太少导致的。
解决方法:
Nagle算法
接收端层面,当接收端的可用窗口小于MSS和一半的接收窗口时,直接视为零窗口处理;发送端层面,可以在传输层层面解决,协议栈会将发送的少量数据缓存起来,等待发送的数据大于MSS时,才会把数据发送出去;但是紧急数据和FIN报文是不受这个影响的。这也就是导致tcp流式传输粘包的问题。
拥塞控制,来叻来叻,终于来了,拥塞控制作为流量控制的另一个模块,除了端到端需要考虑,数据在传输时会进入网络中,当大量的tcp连接发送数据,导致网络中传输的数据量过大时,会导致路由器处理不过来而丢包,这种现象就是拥塞现象。
如何避免拥塞现象呢,当然就是在发生拥塞的时候减少流量。在拥塞控制中,同样具备拥塞窗口这一概念,tcp的实际发送流量等于发送窗口和拥塞窗口的较小值。拥塞控制算法大致由三个部分组成:慢启动、拥塞避免、快速恢复。下面一一介绍这几个算法,以及算法背后的解决的问题和本质。
我们要留意两个变量,在这三个算法中非常重要:拥塞窗口 / cwnd和慢启动阈值 / sshresh
慢启动
慢启动发生在建立连接之处,tcp连接刚刚建立,需要知道要发送多少流量好。那当然是越多越好,慢启动就是这个过程,通过指数级的增加流量来快速接触到可能导致拥塞的最大流量。每收到一个ack,cwnd为之前的1倍。就是随着RTT指数级增长。这个增长速度是很快的,很快发送端就被允许发送大量的数据,很快就会导致cwnd超过awnd,也就是超过接收端能够处理的上限。所以慢启动阶段执行的过程是非常快的,几乎可以忽略不记,这个阶段流量的控制来自接收端。
1
2
when recv ack and cwnd < sshresh
cwnd = cwnd * 2
当然cwnd不会无止尽的增长下去,会有慢启动阈值 / sshresh来控制。这也就是我们之后要谈的拥塞避免阶段。
拥塞避免
当cwnd > sshresh时,cwnd的更新就会采用拥塞避免算法。我们先来讲下拥塞避免下对cwnd的更新算法:
1
2
when sshresh < cwnd and time go rtt
cwnd += mss
每经过一个RTT,cwnd增加1个mss,直到发生拥塞为止。和慢启动不同,拥塞避免在增长频率和增长幅度都小。
如何检测到网络中发生拥塞呢,当发送端发现发生快速重传,也就是丢包发生,就认定此时的网络处于轻微拥塞状态,这个时候要将网络恢复到不拥塞的状态,也就是进入快速恢复阶段。
快速恢复
- sshresh = cwnd
- cwnd = sshresh / 2 + 3
- when 到达恢复点 cwnd = sshresh 并进入拥塞避免
为什么快速恢复时,要将cwnd设为慢启动阈值+3,这是由于数据包守恒原理,当收到三个冗余ack时,说明有三个数据包被正确送达至接收端,也就是经过了3个RTT时间,所以此时cwnd要执行三次线性增长。
流量控制的本质
流量控制到这里就基本讲完了,讲完工作机理,我想再讲讲这些工作机制背后的设计理念和设计人员的考量。流量控制本质上是为了提高端到端层面上tcp协议的协议效率,一个是尽可能减少不必要(可能会丢包)的发送,一个是尽可能降低头部和data的比例。
从流量控制策略上看,我们不难发现这套策略实际上是不完美的,会引发一些问题。比如上面介绍的零窗口和糊涂窗口问题以及ack报文单独存在带来的网络开销。这些问题实际上都是影响着tcp协议的工作效率的问题,都引发了网络利用率的降低。于是我们引入了解决这些问题的方案,零窗口–探测报文;糊涂窗口–Nagle算法;ack报文单独存在–延迟ack。这些解决方案都在影响着我们何时发数据,最少能够发送多少数据。也就是设计人员通过这些流量控制方案,使得这些问题不发生。
仔细研究这些方案,我们就会惊奇的发现,方案的执行居然会影响不同的问题,也就是一个方案的执行会增强另一个方案的效果。这就是这些流量控制方案的精妙之处。下面来详细解释一下。
Nagle算法和延迟ack,我们知道Nagle算法是为了避免发送少量数据的发送,希望发送报文数据得到积累,其阻止报文发送的条件是没有收到ack。我们再看延迟ack,我们在等待发送ack时,发送端会在这个时候积累发送报文,是不是增强了Nagle算法。
拥塞控制的本质
首先要知道tcp是端到端的协议,所有端点在网络中都是公平的,地位平等的。所有端点理应去争夺最大的网络宽带,但是当两个及两个以上的连接的数据导航来到同一个路由器上,彼此竞争这台路由器上的最大宽带必然会导致某个连接发送的数据因为其他连接对宽带的挤占而丢失,这就是为什么拥塞会发生的原因,也是我们去进行拥塞控制的原因。
所以我们的拥塞控制需要解决的需求就是:保证端点的公平性,所有的端点都具备占有路由器所有宽带的权力;检测拥塞并退出拥塞。慢启动和拥塞避免都是为了尽可能使得端占有路由器的宽带,快速恢复则是通过减少拥塞窗口来实现退出拥塞。
UDP / 用户数据报协议
我们知道网络层的设计理念是分组转发,具备无连接的特性。udp实际上就是把这一特性保留,提供最简单的传输层协议,让用户层决定大部分的事情,比如保证可靠性和有序性的措施。udp简单来说就是对在某端设置mailbox的概念,这个mailbox和某端口bind在一起,其他端的socket可以往这个mailbox投递数据报文。既然是mailbox,那么socket可以匿名投递,也可以实名投递。是一个普通的生产者消费者模型,外部socket生产消息,目的端消费消息。
UDP Header
同样是需要了解udp报文头的字段组成:
- 2 bytes:source port optional 可以选择设置或不设置,取决于socket有没有bind
- 2 bytes:dest port 目的端口
- 2 bytes:数据长度
- 2bytes:checksum 简单的端到端校验数据能力
Ip相关八股
IP层主要位于OSI模型中的第三层–网络层。实现主机与主机之间的通信,也叫点对点(end to end)通信。
IP基础知识
为保证正常通信,需要给每个设备配置正确的IP地址。IPv4地址由32位正整数构成,每8 位为组,共分为 4 组,每组以「.」隔开,再将每组转换成十进制。例:192.168.0.1。IP由网络标识和主机标识组成,网络标识用于标识网络,主机标识用于标识主机。IP地址分为 A、B、C、D、E 五类,其中 A、B、C 三类用于主机,D 类用于多播,E 类用于实验。如何判断网络标识和主机标识的位数呢,答案是子网掩码。
- 主机号全为0,表示某个网络;
- 主机号全为1,表示网络内的所有主机,即广播地址;
说到广播地址,广播地址是什么?
广播地址是用于在用一个链路中相互连接的主机之间发送数据包。广播地址可以分为本地广播和直接广播两种。在子网内进行进行广播的地址叫做本地广播地址,直接广播是指在不同子网之间进行广播的地址,也就是某个子网内的主机向另一个子网内的所有主机进行广播。
D类和E类地址的作用:D类E类地址没有主机号,D类用来多播,E类是预留号
说到多播,什么是多播?
多播(Multicast)是一种网络通信方式,它允许将数据包从一个源发送到多个指定的接收者,而不是发送给网络中的所有主机(广播)或单个主机(单播)。多播使用D类IP地址(224.0.0.0到239.255.255.255)来标识多播组,任何希望接收多播数据的主机可以加入相应的多播组。
- 224.0.0.0 ~ 224.0.0.255 为预留的组播地址,只能在局域网中,路由器是不会进行转发的。
- 224.0.1.0 ~ 238.255.255.255 为用户可用的组播地址,可以用于 Internet 上。
- 239.0.0.0 ~ 239.255.255.255 为本地管理组播地址,可供内部网在内部使用,仅在特定的本地范围内有效
由于ip地址分类存在弊端,所以引入CIDR无地址分类。a.b.c.d/x。子网掩码还可以划分子网网络地址,没错,在内网里还可以接着划分子网。也就是一个内网中有多个子网。网络号是由x决定的。
公有ip地址和私有ip地址。
IP地址和路由控制。
IP地址中的网络号是负责进行路由控制的。重要的是我们需要知道自己的主机的路由表和路由器上的路由表有什么不同。主机的路由表的表项一般由两项组成,一是广播地址,二是默认路由。发送子网的ip会被广播,发送其他网络的ip会被默认路由route到router上进行网络转发。
路由器的路由表则记录了和路由器有直接链路连接关系的各个网络的下一跳信息。
IP的分片和重组
IP的分片和重组会导致IP分片丢失导致整个IP包的作废。所以在TCP中引入MSS,也就是数据最大长度,使其和tcp头ip头加起来不会超过MTU,实现在TCP层面分片,由于TCP的可靠性来保证分片的可靠性。但是UDP不提供重传机制,所以UDP的报文尽量不要超过MTU;
Ipv6的基本认识
IPv6一共128位,每16位为一组,用:隔开。
Ipv4和IPv6的头部!最好记下来,了解每个部分的作用