TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
cwnd
>= ssthresh
(阈值)时,就会使用「拥塞避免算法」,TCP发送方会切换到拥塞避免模式,此时每次接收到一个ACK时,cwnd的大小只会增加一个MSS(最大报文段长度),而不是每次都加倍。这样可以避免发送过多的数据导致网络拥塞。ssthresh
和 cwnd
变化如下:
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;ssthresh
设为 cwnd/2
,cwnd
重置为 1
cwnd
和 ssthresh
已被更新了,然后,进入快速恢复算法如下:
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了);连接 服务对象
可靠性 拥塞控制、流量控制
头部长度不同 分片不同
20
个字节,如果使用了「选项」字段则会变长的。在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
sock
加入到这个队列中,队列内的sock
都处于SYN_RECV
状态。sock
取出,放到全连接队列中。队列里的sock
都处于 ESTABLISHED
状态。这里面的连接,就等着服务端执行accept()后被取出了。socket
,新建一个socket对象
bind
,将 socket 绑定在指定的 IP 地址和端口;
listen
,进行监听,服务器端处于listen状态
socket
执行listen
时,内核都会自动创建一个半连接队列和全连接队列。accept
,等待客户端连接;accept 成功返回是在三次握手成功之后
accept方法
只是为了从全连接队列中拿出一条连接,本身跟三次握手几乎毫无关系。connect
,向服务端的地址和端口发起连接请求;connect 成功返回是在第二次握手
在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素。一来一回,才能确保双方的初始序列号能被可靠的同步。
如果客户端发送的 SYN
报文在网络中阻塞了,重复发送多次 SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
需要注意的是第三次握手是可以携带数据的,前两次握手是不可以携带数据的
tcp_syn_retries
内核参数决定;(由于没有收到ACK)tcp_synack_retries
内核参数决定。(由于没有收到ACK)那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传,经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
TCP协议中的ACK(Acknowledgment)报文通常不会重传。
ACK报文是TCP用于确认接收到数据的报文,用于确认对方发送的数据已经成功接收。在TCP的可靠传输机制中,发送方发送数据后,会等待接收方发送ACK报文进行确认。如果发送方在一定的超时时间内没有接收到ACK报文,则会认为数据没有成功送达,并进行重传(这里是对应报文不是ACK报文)。
在TCP协议中,数据的重传是由发送方负责的,而ACK报文的重传通常不会由接收方触发。一般情况下,接收方只需要发送ACK报文来确认已接收到数据,而不会因为没有接收到ACK报文而进行重传。接收方只有在需要通知发送方数据丢失或者需要进行流量控制等特殊情况下,才会发送特定的控制报文,如SACK(Selective Acknowledgment)报文或者窗口更新报文。
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
避免 SYN 攻击方式,可以有以下四种方法:
FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。ACK
应答报文后,之后进入 FIN_WAIT_2
状态。FIN
报文,之后服务端进入 LAST_ACK
状态。FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭。2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
tcp_orphan_retries
参数控制。2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
32768~61000
,也可以通过 net.ipv4.ip_local_port_range
参数指定范围。当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
答案是可以
什么是 TCP 延迟确认机制?
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:
当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
TCP 的 Keepalive 这东西其实就是 TCP 的保活机制
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件(默认是两个小时),那么内核里的 TCP 协议栈就会发送探测报文。
什么是粘包问题?
粘包的问题出现是因为不知道一个用户消息的边界在哪,如果知道了边界在哪,接收方就可以通过边界来划分出有效的用户消息。
一般有三种方式分包的方式:
总体来说分为以下几个过程:
TCP/IP 四层模型 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成:
每一层的封装格式:
WebSocket 和 HTTP 协议都是基于 TCP 连接实现的,但是它们的长连接机制有一些区别。
HTTP 长连接通常使用 HTTP/1.1 中的 "keep-alive" 机制,在客户端和服务器之间完成一次请求/响应后,会继续使用已建立的 TCP 连接来发送和接收多个请求/响应。这种方式可以减少频繁地建立、断开 TCP 连接所带来的性能开销,从而提高网络传输效率。但是,使用 HTTP 长连接时,客户端和服务器之间的通信仍然需要按照请求-响应模式进行,且无法实现即时通信和实时数据传输。
WebSocket 的长连接是指在服务器和客户端之间建立一条持久的双向通信通道,该通道会一直保持打开状态,直到显示地关闭。这种连接不仅可以支持即时通信和实时数据传输,而且可以使得服务器可以主动向客户端推送消息,从而实现更高效的数据传输和通信。此外,WebSocket 还有一个心跳检测机制(即“ping/pong”),用于检测连接是否处于活动状态,并保证连接的稳定性。
综上所述,HTTP 的长连接主要是为了减少建立和断开连接所带来的性能开销,而 WebSocket 的长连接则是为了支持实时通信和实时数据传输,并保持连接的稳定性。此外,WebSocket 通过心跳检测机制可以更好地保护长连接,而 HTTP 长连接则没有这个机制。
TCP握手之后HTTPS需要四次握手建立连接。
1. ClientHello
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello
请求。
在这一步,客户端主要向服务器发送以下信息:
(1)客户端支持的 TLS 协议版本,如 TLS 1.2 版本。
(2)客户端生产的随机数(Client Random
),后面用于生成「会话秘钥」条件之一。
(3)客户端支持的密码套件列表,如 RSA 加密算法。
2. SeverHello
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello
。服务器回应的内容有如下内容:
(1)确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
(2)服务器生产的随机数(Server Random
),也是后面用于生产「会话秘钥」条件之一。
(3)确认的密码套件列表,如 RSA 加密算法。
(4)服务器的数字证书。
3.客户端回应
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。
如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
(1)一个随机数(pre-master key
)。该随机数会被服务器公钥加密。
(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
4. 服务器的最后回应
服务器收到客户端的第三个随机数(pre-master key
)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。
然后,向客户端发送最后的信息:
(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。
HTTP/0.9:
HTTP/1.0:
HTTP/1.1:
优化部分:
减少 HTTP 请求的次数
通过压缩响应资源,降低传输资源的大小
支持持久连接
HTTP/2:
HTTP/3:
RPC(Remote Procedure Call)协议是一种用于实现分布式系统中不同计算机之间通信的协议。它允许一个计算机程序在网络上请求另一个计算机上的服务,就像调用本地函数一样,而不需要开发者显式处理网络通信细节。
在 RPC 中,客户端程序发起请求,称为远程调用(remote call),而服务器程序提供服务并响应请求。客户端和服务器之间的通信过程对于开发者来说是透明的,就好像调用本地函数一样。
一般来说,RPC 协议涉及以下几个要素:
虽然大部分 RPC 协议底层使用 TCP,但实际上它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。
首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 IP 地址和端口。这个找到服务对应的 IP 端口的过程,其实就是服务发现。
在 HTTP 中,你知道服务的域名,就可以通过 DNS 服务去解析得到它背后的 IP 地址,默认 80 端口。
而 RPC 的话,就有些区别,一般会有专门的中间服务去保存服务名和IP信息,比如 ZK,甚至是 Redis。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。
以主流的 HTTP/1.1 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(Keep Alive),之后的请求和响应都会复用这条连接。
而 RPC 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个连接池,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,用完放回去,下次再复用,可以说非常环保。
由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池,比如 Go 就是这么干的。
可以看出这一块两者也没太大区别,所以也不是关键
基于 TCP 传输的消息,说到底,无非都是消息头 Header 和消息体 Body。
Header 是用于标记一些特殊信息,其中最重要的是消息体长度。
Body 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 Json,Protobuf。
这个将结构体转为二进制数组的过程就叫序列化,反过来将二进制数组复原成结构体的过程叫反序列化。
对于主流的 HTTP/1.1,虽然它现在叫超文本协议,支持音频视频,但 HTTP 设计初是用于做网页文本展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 Json 来序列化结构体数据。但是这里面的内容非常多的冗余,显得非常啰嗦。
而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。
nginx 中的 src/http/ngx_http_special_response.c
文件中对 499 状态码进行了定义:
c复制代码 ngx_string(ngx_http_error_494_page), /* 494, request header too large */
ngx_string(ngx_http_error_495_page), /* 495, https certificate error */
ngx_string(ngx_http_error_496_page), /* 496, https no certificate */
ngx_string(ngx_http_error_497_page), /* 497, http to https */
ngx_string(ngx_http_error_404_page), /* 498, canceled */
ngx_null_string, /* 499, client has closed connection */
从注释上,我们可以看到 499 表示客户端主动断开连接。
表面上 499 是客户端主动断开,然而在实际业务开发中,当出现 HTTP 499 状态码时,大部分都是由于服务端请求时间过长,导致客户端等的“不耐烦”了,因此断开了连接。
在客户端请求服务端接口时,有些接口请求确实很慢。这种情况呢,就是接口是真的慢,不是偶然现象,是什么时候请求都慢,这个也最好解决,优化接口即可。
就是连续两次过快的 post 请求就会出现 499 的情况,是 nginx 认为这是不安全的连接,主动断开了客户端的连接。
可能是机器这段时间执行定时任务之类的
MYSQL 会有将脏页数据刷到磁盘的操作,这个我们具体我们会有一片单独的文章介绍。在 MYSQL 执行刷脏页的时候,会占用系统资源,这个时候,我们的查询请求就有可能比平时慢了。
用户态:
close()
方法:在用户态,应用程序发起了关闭套接字连接的请求。内核态: