计算机网络
一、HTTP与HTTPS
1、HTTPS RSA
- HTTP明文传输的风险
- 窃听风险
- 篡改风险
- 冒充风险
- HTTPS解决方案
- 信息加密
- 校验机制
- 身份证书
- RSA的四次握手
- 第一次握手:客户端发出消息,向服务器打招呼[Client Hello]
- 消息内容
- TLS版本号
- 支持的密码套件列表
- 生成第一个随机数(Client Random)
- 消息内容
- 第二次握手:服务端收到[Client Hello]消息后,确认TLS版本号是否支持
- 不支持:返回消息给客户端,告知TLS版本不支持,终止流程
- 支持返回消息
- 确认支持TLS的版本
- 生产第二个随机数(Server Random)
- 从客户端的密码套件列表选择一个合适的密码套件返回
- 套件格式:密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法
- 示例: TLS_RSA_WITH_AES_128_GCM_SHA256
- CA证书:
- 证书版本号
- 序列号
- 颁发者信息
- 主题信息
- 有效期
- 公钥信息
- 数字签名
- 颁发机构的数字签名算法
- 第三次握手:客户端收到服务端确认信息
- 返回消息:
- 生成第三个随机数(pre-master),校验完证书,通过[Client Key Exchange]消息传给服务端
- 很久三个随机数,客户端和服务端生成会话密钥,用于对称加密,之后客户端发一个[Change Cipher Spec],告诉服务端开始使用加密方式发送消息
- 客户端再发一个「Encrypted Handshake Message(Finishd) 」消息,把之前所有发送的数据做个摘要,再用会话密钥(master secret)加密一下,让服务器做个验证,验证加密通信「是否可用」和「之前握手信息是否有被中途篡改过」
- 返回消息:
- 第四次握手:服务器同样操作,发送[Change Cipher Spec]和[Encrypted Handshake Message]消息,双方确认无误开启加密传输
- 第一次握手:客户端发出消息,向服务器打招呼[Client Hello]
2、HTTPS ECDHE
- DH算法
- 协议双方都有的公共参数G,P
- 协议双方各自的私钥a,b
- 协议双方各自产生公钥A,B
- A = G ^ a (mod P)
- B = G ^ b (mod P)
- 根据离散对数的幂运算有交换律可以得到堆成加密的密钥K
- K = B ^ a (mod P) = A ^ b (mod P)
- DHE算法
- 由于之前的static DH算法不具备前向安全性(服务端的私钥不会发生变化,黑客可以通过海量的数据进行破解)
- DHE算法就是在DH算法的基础上,让每次通信过程的私钥随机生成一个临时的
- ECDHE算法
- ECDHE算法描述:
- 双方事先确定好使用哪种椭圆曲线,和曲线上的基点 G,这两个参数都是公开的;
- 双方各自随机生成一个随机数作为私钥d ,并与基点 G相乘得到公钥Q(Q = dG),此时小红的公私钥为 Q1 和 d1,小明的公私钥为 Q2 和 d2;
- 双方交换各自的公钥,最后小红计算点(x1,y1) = d1Q2,小明计算点(x2,y2) = d2Q1,由于椭圆曲线上是可以满足乘法交换和结合律,所以 d1Q2 = d1d2G = d2d1G = d2Q1 ,因此双方的 x 坐标是一样的,所以它是共享密钥,也就是会话密钥。
- 这个过程中,双方的私钥都是随机、临时生成的,都是不公开的,即使根据公开的信息(椭圆曲线、公钥、基点 G)也是很难计算出椭圆曲线上的离散对数(私钥)
- ECDHE的握手过程:
- 第一次和第四握手基本等同于RSA
- 第二次握手基本一样:
- 差异点:
- 服务打算为了证明自己的身份,发[Certificate]消息时,会把证书也发给客户端
- 发送完证书后,还会发送[Server Key Exchange]消息
- 选择好椭圆曲线,确定G点,公开给客户端
- 生成随机数作为服务端椭圆曲线的私钥,保留到本地
- 根据基点G和私钥计算出公钥,公开给客户端
- 最后发送[Server Hello Done]消息
- 以上都是一次交互里面携带的不同信息
- 差异点:
- 第三次握手同样类似:
- 校验证书,生成最终密钥,然后发送[Change Cipher Spec]消息和[Encrypted Handshake Message]
- 差异点:
- 最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的。
- ECDHE算法描述:
3、HTTPS的优化
-
分析性能损耗
- TLS协议握手过程
- 握手后的对称加密报文传输
-
密钥交换算法优化
- 选用ECDHE0密钥交换算法替换RSA算法
- RSA密钥交换算法的TLS握手过程,不仅慢,而且安全性也不高
- 尽量选择x25519曲线,该曲线是目前最快的椭圆曲线
- TLS握手的消息可以从2RTT减少1RTT,而且安全性也高,具备前向安全性
- 选用AES_128_GCM,它更短一点
- TLS升级
- TLS1.3大幅度简化了握手的步骤,完成TLS握手只要1RTT
- TLS1.3把Hello和公钥交换这两个消息合并成了一个
- 选用ECDHE0密钥交换算法替换RSA算法
-
证书优化
- 证书传输
- 减小证书的大小,所以,服务器证书尽量选择椭圆曲线(ECDSA)证书,而不是RSA证书,因为在同样安全强度下,ECC密钥长度比RSA短得多
- 证书验证
- CRL证书吊销列表,存储失效的证书
- 但是该方法实时性差
- 随着吊销证书的增多,列表就会越来越大,下载的速度就会越慢
- OCSP在线证书状态协议,它的工作方式是想CA发送查询请求,让CA返回证书的有效状态
- 该方法增加了一次网络请求,延时变大
- OCSP Stapling,原理:服务器向CA周期性地查询证书状态,获得一个带有时间戳和签名的响应结果并缓存它
- CRL证书吊销列表,存储失效的证书
- 证书传输
-
会话复用
-
Session ID
- 实现方式
- 客户端和服务端首次TLS握手连接后,双方会在内存缓存会话密钥,并用唯一的Session ID来标识
- 客户端发起连接时,hello消息里会带上Session ID,服务器收到后就会从内存中找,要是有就可以直接从会话密钥恢复会话状态;要是找不到,就会建立新的会话
- 缺点:
- 服务器必须保持每一个客户端的会话密钥,随着客户端的曾端,服务器的内存压力也会越大
- 现在网站服务一般是由多台服务器通过负载均衡提供服务的,客户端再次连接不一定会命中上次访问过的服务器,于是还是要走完整的TLS握手过程
- 实现方式
-
Session Ticket
- 实现方式
- 客户端与服务器首次建立连接时,服务器会加密[会话密钥]作为Ticket发送给客户端,交给客户端缓存
- 客户端再次连接服务器时,客户端会发送 Ticket,服务器解密后就可以获取上一次的会话密钥,然后验证有效期,如果没问题,就可以恢复会话了,开始加密通信
- 缺点:
- 每台服务器加密会话密钥的密钥是一样的,不具备向前安全性
- 无法应对重放攻击
- 攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的
- 实现方式
-
Pre-shared Key
-
前面的 Session ID 和 Session Ticket 方式都需要在 1 RTT 才能恢复会话。
而 TLS1.3 更为牛逼,对于重连 TLS1.3 只需要 0 RTT ,原理和 Ticket 类似,只不过在重连时,客户端会把 Ticket 和 HTTP 请求一同发送给服务端,这种方式叫 Pre-shared Key。
-
-
4、HTTPS/2的优化
-
HTTP/1.1协议的性能问题
- 现状:
- 消息的大小变大了
- 页面资源变多了
- 内容形式变多样了
- 实时性要求变高了
- 问题
- 延迟难以下降
- 并发连接有限
- 队头阻塞问题
- HTTP头部巨大且重复
- 不支持服务器推送消息
- 现状:
-
HTTPS/2的优化
-
兼容HTTPS/1.1
-
头部压缩
-
开发HPACKE算法,进行压缩
-
静态字典,只包含了61种高频出现的
- 高频出现的头部和字符串和字段建立了一张静态表
- 直接写入HTTP/2框架
-
动态字典
- 不在静态字典的进行动态构建,从第62步开始
- 在下一次放同样的字段时,就只用发index就好了
- 生效前提:必须同一个连接上,重复传输完全相同的HTTP头部
-
Huffman编码(压缩算法)
-
-
-
二进制帧
-
HTTP/2将HTTP/1.1的文本格式改进成了二进制格式传输数据
-
二进制帧的结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GDl9ZAWT-1693277647829)(.\image-20230821111027488.png)]
-
-
并发传输
- 通过Stream,多个Stream复用一条TCP链接,达到并发的效果
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTCIo8lW-1693277647830)(.\image-20230821151239493.png)]
- 1个TCP链接可以包含多个Stream
- Stream里可以包含多个Message,Message对应HTTP/1中的请求或响应,由HTTP头部和包体构成
- Message里可以包含多个Frame,Frame是HTTP/2最小单位,以二进制压缩格式存放HTTP/1中的内部(头部和包体)
- 不同的Stream帧是可以乱序发送的,Stream带有Stream ID
- 客户端和服务器双方都可以建立 Stream
- 同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧
GOAWAY
,用来关闭 TCP 连接 - 当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过 TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。
-
服务器主动推送资源
- 客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过
PUSH_PROMISE
帧传输 HTTP 头部,并通过帧中的Promised Stream ID
字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
- 客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过
-
5、HTTP与RPC
-
所属层级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qv4UBv0b-1693277647830)(E.\image-20230821153524908.png)]
-
二者概念介绍
- HTTP协议,超文本传输协议,是一种传输协议
- RPC,远程过程调用。它本身并不是一个具体的协议,而是一种调用方式
- 如果现在这不是个本地方法,而是个远端服务器 暴露出来的一个方法
remoteFunc
,如果我们还能像调用本地方法那样去调用它,这样就可以屏蔽掉一些网络细节 - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3C02owqk-1693277647831)(.\image-20230821154516849.png)]
- 如果现在这不是个本地方法,而是个远端服务器 暴露出来的一个方法
-
二者的差别
- HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。*很多软件同时支持多端,比如某度云盘,既要支持*网页版,还要支持手机端和 PC 端**
- 服务发现
- HTTP,通过DNS服务区解析的到它背后的IP地址,默认80端口
- RPC,一般会有专门的中间服务去保存服务名和IP信息,比如Consul或者Etcd,Redis,也有使用DNS的
- 底层连接形式
- HTTP:默认在建立底层TCP连接之后会一直保持这个连接(Keep Alive), 之后的请求和响应都会复用这条连接
- RPC:一样通过TCP长连接进行数据交互,但是RPC一般还会建立连接池。用完放回去取,下次复用
- 由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给HTTP加个连接池
- 传输的内容
- HTTP:请求头过于冗长
- RPC:PC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。
- 当然上面说的 HTTP,其实特指的是现在主流使用的 HTTP/1.1 ,
HTTP/2
在前者的基础上做了很多改进,所以性能可能比很多 RPC 协议还要好 ,甚至连gRPC
底层都直接用的HTTP/2
。
6、HTTP和WebSockt
-
使用HTTP不断轮询
- 以在网页前端代码里不断定时发HTTP请求到服务器,服务器收到请求后给客户端响应消息
-
长轮询
- 将等待时间设置比较长
- 实际上就是一直等待后端某个状态
-
WebSockt
-
HTTP协议下,同一时间里,客户端和服务端只能有一方主动发数据,是为半双工
-
WebSockt,将客户端和服务端建立连接,开启全双工通信
-
WebSockt握手
- 先进行TCP三次握手,然后发送一次http请求,若是需要转换成WebSockt协议,需要升级协议,返回相应的响应头,生成base64码
- WebSockt第一次握手(依旧是HTTP请求),带有特殊请求头,根据base64码以及相应公开算法生成对应的字符串,来校验身份
- 校验通过第二次握手(依旧是HTTP请求),返回101状态码,切换协议,之后就是WebSockt通信了
-
数据帧
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFse92ow-1693277647831)(.\image-20230821183337855.png)]
- opcode字段 :这个是用来标志这是个什么类型 的数据帧。比如。
- 等于 1 ,是指text类型(
string
)的数据包 - 等于 2 ,是二进制数据类型(
[]byte
)的数据包 - 等于 8 ,是关闭连接的信号
- 等于 1 ,是指text类型(
- payload字段 :存放的是我们真正想要传输的数据的长度 ,单位是字节 。比如你要发送的数据是
字符串"111"
,那它的长度就是3
。- 如果
最开始的7bit
的值是 0~125,那么它就表示了 payload 全部长度 ,只读最开始的7个bit
就完事了。 - 如果是
126(0x7E)
。那它表示payload的长度范围在126~65535
之间,接下来还需要再读16bit。这16bit会包含payload的真实长度。 - 如果是
127(0x7F)
。那它表示payload的长度范围>=65536
,接下来还需要再读64bit。这64bit会包含payload的长度。这能放2的64次方byte的数据,换算一下好多个TB,肯定够用了
- 如果
- payload data字段:这里存放的就是真正要传输的数据,在知道了上面的payload长度后,就可以根据这个值去截取对应的数据。
- opcode字段 :这个是用来标志这是个什么类型 的数据帧。比如。
-
使用场景
- 适用于需要服务器和客户端频繁交互的场景,如:聊天室,网页小游戏
-
二、TCP
1、TCP基本认识
-
TCP头格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yt24RFZk-1693277647832)(.\image-20230821194159907-16926196207351.png)]
-
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传个接受端主机,每发送一次数据,就累加 一次该数据数据字节数的大小。用于解决网络包乱序的问题
-
确认应答号:指下一次期望收到的数据序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题
-
控制位
- ACK:为1时,确认应答有效 ,TCP规定除了最初建立连接时的
SYN
包之外该为必须设置为1 - RST:为1时,表示TCP连接中出现异常必须强制断开连接
- SYN:为1时,表示希望建立连接,并在其序列号的字段进行序列号初始值的设定
- FIN:为1是,希望断开连接
- ACK:为1时,确认应答有效 ,TCP规定除了最初建立连接时的
-
-
什么是TCP,TCP的作用,以及工作在哪一层?
-
无论是
TCP/IP
还是OSI
模型都是工作在传输层 -
由于
IP
层是不可靠的,所以有了TCP
,因为TCP
是一个工作在传输层的可靠数据传输的服务,它能保证接收端接收的网络包是无损坏、无间隔、非冗余和按序的 -
TCP是面向连接的 、可靠的 、基于字节流的传输层通信协议
-
-
什么是TCP连接
-
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接
-
建立一个TCP连接是需要客户端与服务端搭乘三个信息的共识:
- Socket:由IP地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
-
-
如何唯一确定一个TCP连接
- TCP四元组唯一确定一个连接
- 源地址
- 源端口
- 目的地址
- 目的端口
- 对于IPv4,客户端IP数最多为
2^32
,客户端端口数最多为2^16
,最多连接数2^48
- 服务端最大并发TCP远不能达到理论值,会受到限制
- 文件描述符限制:每个TCP连接就是一个文件
- 系统级:当前系统可打开的最大数量
- 用户级:指定用户可打开的最大数量
- 进程级:单个进程可打开的最大数量
- 内存限制:每个TCP连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生OOM
- 文件描述符限制:每个TCP连接就是一个文件
- TCP四元组唯一确定一个连接
-
TCP和UDP的区别与应用场景
- 区别:
- 连接
- TCP是面向连接的传输层协议,需要先建立连接
- UDP不需要建立连接
- 服务对象
- TCP是一对一的两点服务
- UDP支持一对一、一对多、多对多的交互通信
- 可靠性
- TCP是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达
- UDP是尽最大努力交付,不保证可靠交付数据。但是也可以基于UDP实现一个可靠的传输协议
- 拥塞控制、流量控制
- TCP有拥塞控制和流量控制,保证数据传输的安全性
- UDP则没有,即使网络非常拥堵了,也不会影响UDP的发送速率
- 首部开销
- TCP首部长度较长,会有一定开销,首部在没有使用选项字段的时是20个字节,如果使用了选项字段则会变长
- UDP首部只有8个字节,并且是固定不变的,开销较小
- 传输方式
- TCP是流式传输,没有边界,但保证顺序和可靠
- UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序
- 分片不同
- TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片
- UDP的数据大小如果大于MTU大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传给传输层
- 连接
- 应用场景
- TCP面向连接的
FTP
文件传输- HTTP/HTTPS
- UDP面向无连接
- 包总量较少的通信,如
DNS
、SNMP
等 - 视频、音频等多媒体通信
- 广播通信
- 包总量较少的通信,如
- TCP面向连接的
- 区别:
2、TCP建立连接
-
三次握手
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQgBJEKe-1693277647832)(.\image-20230821205040316.png)]
- 前两次握手是不能携带数据的,第三次握手可以携带数据
-
TCP三次握手的原因
- 阻止重复历史连接的初始化(主要原因)
- 两次握手的话,就不会存在中间状态阻止历史连接,服务端在收到一个SYN后就会建立连接
- 同步双方的初始序列号
- 作用:
- 接收方可以去除重复数据
- 接收方可以根据数据包的序列号按序接收
- 可以标识发送出去的数据包中,哪些是已经被对方收到的
- 两次握手没办法保证双方都确认序列号
- 作用:
- 避免资源浪费
- 如果只有「两次握手」,当客户端发生的
SYN
报文在网络中阻塞,客户端没有接收到ACK
报文,就会重新发送SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的ACK
报文,所以服务端每收到一个SYN
就只能先主动建立一个连接
- 如果只有「两次握手」,当客户端发生的
- 阻止重复历史连接的初始化(主要原因)
-
TCP每次建立连接时,初始化的序列号都不一样
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面)
- 为了安全性,防止黑客伪造的相同序列号的TCP报文被对方接收
-
初始化序列号ISN的产生
ISN = M + F(localhost, localport, remotehost, remoteport)
M
是一个计时器,这个计时器每隔 4 微秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
3、TCP断开连接
-
四次挥手
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rj1Ca7r2-1693277647833)(.\image-20230822194426564.png)]
-
为什么挥手需要四次
- 关闭连接是,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接受数据。 - 服务端收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接 - 四次挥手在特殊情况下,可以优化成三次挥手:
- 服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序 :
- 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
- 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数
- 粗暴关闭和优雅关闭:
- close函数,同时socket关闭发送方向和读取方向,使用close函数进行关闭,当客户端再次接受数据的时候,客户端会触发RST报文,直接关闭连接,是粗暴关闭
- shutdown函数,可以指定socket只关闭发送方向而不关闭读取方向,也就是socket不再有发送数据的能力,但还是具有接收数据的能力,即使之后继续收到服务端传来的数据,依旧能够进行处理,然后就会经历完整的TCP四次挥手,所以,shutdown是优雅的关闭
- 三次挥手
- 当没有数据要发送并且开启了TCP延迟确认机制,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手
- TCP延迟确认机制
- 当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认 。 TCP 延迟确认的策略:
- 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
- 当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认 。 TCP 延迟确认的策略:
- 综上,我们经常见到的会是三次挥手,而不是理论中的四次挥手
- 服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序 :
- 关闭连接是,客户端向服务端发送
-
第一次挥手丢失了
- 客户端发送
FIN
报文后,会等待服务端发送的ACK
,若是报文丢失了,迟迟收不到ACK
,就会触发超时重传,重发次数由tcp_orphan_retries
参数控制,这个值一般为3 - 当客户端重传 FIN 报文的次数超过
tcp_orphan_retries
后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到close
状态
- 客户端发送
-
第二次挥手丢失了
-
表现和第一次挥手报文丢失的表现是一样的,客户端没有收到
ACK
,还是会触发超时重传 -
客户端收到
ACK
报文后,客户端就会处于FIN_WAIT2
状态:-
对于 close 函数关闭的连接,由于无法再发送和接收数据,所以
FIN_WAIT2
状态不可以持续太久,而tcp_fin_timeout
控制了这个状态下连接的持续时长,默认值是 60 秒。 -
对于shutdown函数关闭的连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于
FIN_WAIT2
状态(tcp_fin_timeout
无法控制 shutdown 关闭的连接
-
-
-
第三次挥手丢失了
- 服务端就会迟迟收不到客户端的
ACK
报文,然后触发超时重传,仍然有tcp_orphan_retries
参数控制 - 当服务端重传第三次挥手报文的次数达到了 3 次后,由于 tcp_orphan_retries 为 3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
- 客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接。
- 服务端就会迟迟收不到客户端的
-
第四次挥手丢失了
- 服务端依旧是收不到
ACK
报文,但是客户端已经更改状态为TIME_WAIT
状态了 - 当服务端重传第三次挥手报文达到 2 时,由于 tcp_orphan_retries 为 2, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。
- 客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。
- 服务端依旧是收不到
-
为什么需要
TIME_WAIT
状态- 防止历史连接中的数据,被后面相同四元组的连接错误的接收
- 保证被动关闭连接的一方,能被正确的关闭
-
TIME_WAIT
过多有什么危害-
第一是占用系统资源,比如文件描述符、内存资源、CPU资源、线程资源等
-
第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围-
客户端的端口可以重复使用吗?
- TCP连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生变化,那么就表示不同的TCP连接的。所以如果客户端已使用端口64992与服务端A建立了连接,那么客户端要与服务端B建立连接,还可以使用之前可能已经被使用过的端口,因为内核是通过四元组信息来定位一个TCP连接的,并不会因为客户端的端口号相同,而导致连接冲突的问题
-
多客户端可以bind同一个端口吗?
- 当IP + PORT不是全部相同时,就可以尽心bind()
- 一般而言,客户端不建议使用 bind 函数,应该交由 connect 函数来选择端口会比较好,因为客户端的端口通常都没什么意义。
-
客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
-
如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。
但是,只要客户端连接的服务器不同,端口资源可以重复使用的
-
-
如何解决客户端TCP连接TIME_WAIT过多,导致无法与同一个服务器建立连接的问题
- 打开
net.ipv4.tcp_tw_reuse
这个内核参数 - 开启这个内核参数后,客户端调用connect函数时,如果选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于
TIME_WAIT
状态,如果该连接处于TIME_WAIT
状态并且TIME_WAIT
状态持续的时间超过1秒,那么就会重用这个连接,然后就可以正常使用该端口了
- 打开
-
-
-
如何优化
TIME_WAIT
-
打开
net.ipv4.tcp_tw_reuse
和net.ipv4_tcp_timestamps
选项shell# 这是开启ipv4.tcp_tw_reuse的一个前提 net.ipv4.tcp_timestamps=1(默认即为 1) net.ipv4.tcp_tw_reuse = 1
-
开启内核参数后,复用处于
TIME_WAIT
的socket
为新的连接所用 -
注意:
tcp_tw_reuse
功能只能用客户端(连接发起方),因为开启了该功能,在调用connect(),内核会随机找一个time_wait
状态超过1秒的连接给新的连接复用
-
-
net.ipv4.tcp_max_tw_buckets
- 这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力
-
程序中使用
SO_LINGER
,应用强制使用RST关闭cstruct linger so_linger; so_linger.l_onoff = 1; so_linger.l_linger = 0; setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
-
如果
l_onoff
为非 0, 且l_linger
值为 0,那么调用close
后,会立该发送一个RST
标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT
状态,直接关闭。但这为跨越
TIME_WAIT
状态提供了一个可能,不过是一个非常危险的行为,不值得提倡
-
-
-
服务器出现大量
TIME_WAIT
状态的原因有哪些- 服务器出现很多
TIME_WAIT
状态的TCP连接,就是说明服务器主动断开了很多TCP链接 - 服务端主动断开连接的场景
- HTTP没有使用长连接
- HTTP/1.0默认不开启,需要在请求头中添加
Connection: Keep-Alive
,HTTP/1.1之后都是默认开启的 - 关闭长连接只需要在请求或响应头中添加
Connection:close
,客户端和服务端只要他们任意一方的HTTP Header中有Connection:close
信息,那么就无法使用HTTP长连接机制 - 根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接
- HTTP/1.0默认不开启,需要在请求头中添加
- HTTP长连接超时
- TCP连接长时间占用的话就会造成资源浪费,为了避免资源浪费,web服务软件一般都会提供一个参数,用来指定HTTP长连接的超时时间,比如:nginx提供的
keepalive_timeout
参数 - 假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个「定时器」,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。
- 在一段时间里面,突然有很多TCP连接超时
- TCP连接长时间占用的话就会造成资源浪费,为了避免资源浪费,web服务软件一般都会提供一个参数,用来指定HTTP长连接的超时时间,比如:nginx提供的
- HTTP长连接的请求数量达到上限
- Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。比如nginx的
keepalive_requests
这个参数 - 若此时
keepalive_requests
参数的默认值100,对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态
- Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。比如nginx的
- HTTP没有使用长连接
- 服务器出现很多
-
服务器出现大量
CLOSE_WAIT
状态的原因有哪些CLOSE_WAIT
出现的原因是被动关闭方 调用close函数关闭连接,无法从CLOSE_WAIT
状态转变为LAST_ACK
状态,说明被动关闭方没有调用close- 当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接
- 普通TCP服务端流程:
- 创建socket,bind绑定端口、listen监听端口
- 将服务端socket注册到epoll
- epoll_wait等待连接到来,连击到来时,调用accpet获取已连接的socket
- 将已连接的socket注册到epoll
- epoll_wait等待事件发生
- 对方连接关闭时,我方调用close
- 普通TCP服务端流程:
- 出现的原因:
- 没有将socket注册到epoll,这样有新连接到来的时,服务端没办法感知这个事情,也就无法获取到已连接的socket,那服务端没办法感知这个事情,也就无法获取到已连接的socket,那服务端自然就没机会对socket调用close函数了
- 新连接到来时,没有调用accept获取该连接的socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些socket调用close函数,从而导致服务端出现大量CLOSE_WAIT状态的连接
- 通过accept获取已连接的socket后,没有将其注册到epoll,导致后续收到
FIN
报文的时候,服务端没办法感知这个事件,那服务端就没机会调用close函数了 - 当发现客户端关闭连接后,服务端没有执行close函数,能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等
-
如果已经建立了连接,但是客户端突然出现故障了怎么办?
-
客户端出现故障指的是客户端的主机发生了宕机,或者断电的场景。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于
ESTABLISH
状态,占用着系统资源 -
为了避免这种情况,TCP搞了个保活机制。原理:
- 定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序
shellnet.ipv4.tcp_keepalive_time=7200 net.ipv4.tcp_keepalive_intvl=75 net.ipv4.tcp_keepalive_probes=9
- tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
- tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
- tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接
-
-
如果已经建立了连接,但是服务端的进程崩溃会发生什么
- TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
4、TCP重传机制
- 超时重传
- 重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的
ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传 - 两种情况发生超时重传:
- 数据包丢失
- 确认应答丢失
- 超时重发的数据,再次超时的时候,有需要重传的时候,TCP的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍,两次超时,就说明网络环境差,不宜频繁反复发送
- 重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的
- 快速重传
- TCP还有另外一种快速重传机制,它不以时间为驱动,而是以数据驱动重传
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7U80OJYW-1693277647833)(.\image-20230823211930939.png)]
- 快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题
- SACK
- 选择性确认重传机制
- 这种方式需要在 TCP 头部「选项」字段里加一个
SACK
的东西,它可以将已收到的数据的信息发送给「发送方」 ,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。 - 如果要支持
SACK
,必须双方都要支持。在 Linux 下,可以通过net.ipv4.tcp_sack
参数打开这个功能(Linux 2.4 后默认打开)
- Duplicate SACK
- 主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0nBLvapW-1693277647834)(.\image-20230823213754483.png)]
- 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
- 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500 ,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着
D-SACK
。 - 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了
- 好处
- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是「发送方」的数据包被网络延迟了;
- 可以知道网络中是不是把「发送方」的数据包给复制了;
5、TCP 滑动窗口
-
有了窗口,即使在往返时间较长的情况下,它也不会降低网络通信的效率
-
那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除
- 窗口大小由哪一方决定
- TCP头里面有个字段交
Window
,**这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。**所以,通常窗口的大小是由接收方的窗口大小来决定的 - 接收窗口和发送窗口的大小并不是完全相等的,接收窗口的大小约等于发送窗口的大小
- TCP头里面有个字段交
6、TCP流量控制
- TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制
- 无脑一直发,可能会频繁触发重传机制,导致网络流量被浪费
- 通过发送窗口和接收窗口控制的,而发送窗口和接收窗口中所存放的字节数,都是存放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整
-
窗口关闭
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KAF8C3V-1693277647835)(.\image-20230825183312454.png)]
-
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
-
-
糊涂窗口综合症
-
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症
-
主要现象
-
接收方可以通告一个小窗口
-
问题解决:
-
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为
0
,也就阻止了发送方再发数据过来。等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
-
-
-
发送方可以发送小数据
- 问题解决:
- 达到以下条件才能继续发送,未达到就一直囤积数据
- 条件一:要等到窗口大小 >=
MSS
并且 数据大小 >=MSS
; - 条件二:收到之前发送数据的
ack
回包;
- 条件一:要等到窗口大小 >=
- 达到以下条件才能继续发送,未达到就一直囤积数据
- 问题解决:
-
-
接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症。
-
7、TCP的拥塞控制
-
场景
- 在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时TCP就会重传数据,但是一重传就会导致网络负担更重了,于是会导致更大的延时以及更多的丢包,这个情况就会进入恶性循环被无限放大
- 拥塞控制的意义在于避免发送方发送的数据包填满整个网络
-
拥塞窗口
-
拥塞窗口(
cwnd
)是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化的 -
此时,拥塞窗口(
cwnd
)、发送窗口(swnd
)以及接收窗口(rwnd
)的关系swnd = min(cwnd, rwnd)
-
变化规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
- 只要网络中没有出现拥塞,
-
怎样察觉网络出现拥堵
- 发生超时重传,就会认为网络出现了拥堵
-
四个算法
- 慢启动
- 在建立连接后,一点一点提高发送数据包的数量
- 规则:当发送方每收到一个ACK,拥塞窗口的大小就会加1
- 结束增加标志:慢启动门限(
ssthresh
)- 当
cwnd
<ssthresh
时,使用慢启动算法。 - 当
cwnd
>=ssthresh
时,就会使用拥塞避免算法
- 当
- 拥塞避免算法
- 达到慢启动门限后发生
- 规则:当发送方每收到一个ACK,拥塞窗口的大小就会增加
1/cwnd
- 拥塞发生
- 发生拥塞就意味着要进行数据包重传,重传的两种机制,都会在拥塞发送后,立马修改慢启动门限,以及拥塞窗口大小
- 超时重传
ssthresh
设置为cwnd/2
cwnd
重置为初始值
- 快速重传
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;- 进入快速恢复算法
- 超时重传
- 发生拥塞就意味着要进行数据包重传,重传的两种机制,都会在拥塞发送后,立马修改慢启动门限,以及拥塞窗口大小
- 快速恢复
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的 ACK,那么 cwnd 增加 1;
- 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
- 拥塞窗口
- 慢启动
-
8、TCP的keep-alive与HTTP的keep-alive
- http的keep-alive
- 让一个TCP连接来发送多个HTTP请求/应答,避免了连接建立和释放的开销,这个方法称为HTTP长连接
- http的keep-alive是放在请求头中的一个参数
- HTTP1.1以后默认是长连接,即
Connection: keep-alive
默认携带 - 为了避免资源浪费的情况,web 服务软件一般都会提供
keepalive_timeout
参数,用来指定 HTTP 长连接的超时时间,该时间一般为60s,若60s内都没有发起新的请求,定时器一到,就会触发回调函数来释放该连接
- TCP的keep-alive
- TCP的keep-alive是一种保活机制
- 如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机宕机(注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机 ),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
- 应用程序若想使用 TCP 保活机制需要通过 socket 接口设置
SO_KEEPALIVE
选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制
- 区分:
- HTTP的keep-alive是由应用程序 实现的,TCP的则是由操作系统内核实现
- HTTP的keep-alive目的是使得多个请求能使用同一个TCP连接;TCP的keep-alive保证TCP连接是否是有效的,保证资源不会过多的浪费
参数,用来指定 HTTP 长连接的超时时间,该时间一般为60s,若60s内都没有发起新的请求,定时器一到,就会触发回调函数来释放该连接
- TCP的keep-alive
- TCP的keep-alive是一种保活机制
- 如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机宕机(注意不是进程崩溃,进程崩溃后操作系统在回收进程资源的时候,会发送 FIN 报文,而主机宕机则是无法感知的,所以需要 TCP 保活机制来探测对方是不是发生了主机宕机 ),或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
- 应用程序若想使用 TCP 保活机制需要通过 socket 接口设置
SO_KEEPALIVE
选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制
- 区分:
- HTTP的keep-alive是由应用程序 实现的,TCP的则是由操作系统内核实现
- HTTP的keep-alive目的是使得多个请求能使用同一个TCP连接;TCP的keep-alive保证TCP连接是否是有效的,保证资源不会过多的浪费