tcp/ip
可靠性
校验和
代码语言:javascript复制 TCP首部有2个字节表示校验和,如果收到校验和有误的数据包,TCP会直接丢弃数据包,等待重传
序列号
每个包的TCP首都都有4个字节的序列号,用来解决乱序和重复问题(根据序列号对收到的包进行正确的排序,再交给应用层;会丢弃掉序列号相同的数据包)
❝序列号回绕 原因:序列号大小为4个字节,当传输的数据超过2^32后,下一个报文的序列号可能会变成比上一个更小。 解决办法:序列号为无符号整数,比较时将(seq2-seq1)转成有符号再比较,即可正确判断大小 ❞
重传机制
- 什么时候会重传?
❝发送数据包后,会启动一个定时器,如果在一定时间(RTO 超时重传的时间)内没有收到对端的ACK确认,会进行重传,称为超时重传 当发送端收到3个或以上相同的ACK包时,就意味着之前有报文丢失了,会立刻进行重传,称为快速重传 ❞
- 重传次数
❝最大重传次数由/proc/sys/net/ipv4/tcp_retries2决定,但实际重传次数会受网络情况RTT大小影响 重传间隔采用指数退避算法,即重试等待间隔越来越长 tcp会根据tcp_retries2计算出一个timeout值,如果数据包第一次发送的时间距离现在的时间间隔,超过了timeout值,就会丢弃,不再重传,所以,在rtt较小情况下,重传次数由tcp_retries2决定,传了retries2次,时间也就超过了timeout值,会进行丢弃;但是,在rtt较大情况下,不需要重传retries2次,就已经到了timeout时间,就会将数据包丢弃了 ❞
- SACK
❝接收端使用SACK来记录自己接收到的数据包的序列号范围,发送端通过这个可以知道需要重传哪些数据包 ❞
流量控制
- 为什么需要流量控制
❝数据包到接收端的接收缓冲区后,应用程序从缓冲区读取数据,但可能由于应用程序处理速度较慢,导致接收缓冲区被占满了,这个时候发送端就应该得知道接收端的这个情况,并等待接收端接收缓冲区有空闲空间之后再继续发送数据 ❞
- 各类窗口
❝(接收端)接收窗口 接收端收到报文后,会在ACK报文中带上当前接收端接收缓冲区剩余空闲空间大小,这个就是接收窗口 ❞
❝(发送端)发送窗口/滑动窗口 ❝在发送端角度上看,数据包根据发送状态和确认状态可以分为4类:
- 已发送并已经被确认的数据
- 已发送但还未被确认的数据
- 没发送但接收端可以接收的数据(表示数据已经在发送缓冲区,并且接收端已经告知有空间接收的这部分数据)
- 没发送而且接口端无法接收的数据(表示已经超出接收端接收能力的那部分数据)
❝发送窗口 = 区域2 区域3 ❞
- 控制流程
❝双方在三次握手过程中告知彼此自己的接收窗口大小(也就是确认了区域2 区域3的大小),然后在传输过程中,发送端根据收到的ACK报文中的确认号和当前接收窗口,相应的滑动发送窗口,并调整下一次发送数据的量 ❞
拥塞控制
- 为什么需要拥塞控制
❝为了避免造成网络堵塞,发送端需要限制自己的发送数据量;如果拥塞窗口小于接收窗口,则设备可以在等待确认之前传输多达拥塞窗口中定义的字节数。相反,如果接收窗口小于拥塞窗口,则设备可以在等待确认之前最多传输接收器窗口中定义的字节数。 ❞
- 不同情况的堵塞
❝接收方收到乱序报文,发送方快速重传 ❝
- 慢启动: 在三次握手后,通过ack获取对端的接收窗口大小,同时初始化各自的拥塞窗口(默认初始拥塞窗口大小为10个MSS),每收到一个ack,cwnd 1,所以一次rtt后,cwnd变为之前的两倍
- 拥塞避免: 存在一个慢启动阈值(ssthresh),当cwnd> ssthresh时,每次rtt,cwnd 1
- 快速恢复: 将ssthresh设为当前cwnd的一半(ssthresh = cwnd/2);将cwnd设置为ssthresh;拥塞窗口线性增加
❞ 网络堵塞,发送方收不到ACK ❝
- 慢启动
- 拥塞避免
- 网络堵塞,发送方没有收到ACK,将ssthresh设为当前cwnd的一半(ssthresh = cwnd/2),cwnd=1,重新进行慢启动和拥塞避免算法
❞ ❞
各种概念
MTU和MSS
MTU是链路层的概念,网络传输中的数据包大小受以太网的帧大小限制,最大帧是1518,最小帧是64,去掉头部和CRC校验字段,剩下的大小就是链路层的有效荷载,而该网卡支持的最大有效荷载就是MTU
受MTU的影响,需要在发送方限制数据包的大小,即将原本较大的数据包进行分段处理,考虑到在TCP/IP各层中,只有传输层有重传机制,在传输过程中,分段发生丢失、损坏时,可以通过TCP的重传机制保证接收方能收到完整的数据包,所以分段的工作应该由传输层完成。换句话说,由于MTU的存在,TCP传输层每次发送的数据包大小也收到了影响,这个值就是MSS(max segment size),表示TCP能发送的最大报文段:MSS = MTU - IP首部 - TCP首部
MSL(报文最大生存时间)和TTL
MSL是 TCP 报文在网络中的最大生存时间。这个值与 IP 报文头的 TTL 字段有密切的关系;TTL是一个 IP 报文最大可经过的路由数,每经过一个路由器,TTL 减 1,当 TTL 减到 0 时这个 IP 报文会被丢弃
socket选项
SO_LINGER(影响close调用的行为)
- l_onoff=0(禁用linger特性(默认))
- close() 立即返回
- 如果有数据残留在发送缓冲区中,系统将尝试把这些数据发送给对端,之后再正常进行4次挥手结束连接
- l_onoff=1(启用linger特性)
❝l_linger=0❝
- close()立即返回
- 丢弃缓冲区的数据,并发送RST给对端,所以对端可能只收到了部分数据
❞l_linger=xxx❝
- close()不会立刻返回,会最多阻塞xxx时间
- 最多等待xxx时间,如果超时了数据还没传输完,会发送RST给对端断开连接,并丢弃缓冲区的数据
SO_REUSEADDR(端口复用)
服务端主动断开连接后,连接需要等待2MSL后才会释放,在此期间,启动服务会报「Address already in use」错误,开启SO_REUSEADDR后,可以解除这个限制
SO_REUSEADDR对FIN_WAIT2和TIME_WAIT连接都生效,值得注意的是,由于对FIN_WAIT2状态也允许端口复用,所以,重启后的服务程序有可能收到非期望数据
SO_REUSEPORT
「作用」
- 允许多个进程监听同一个端口,在内核级别实现负载均衡,并避免多进程程序中的惊群效应(新连接请求时,多个进程都被唤醒,但最终只有一个进程处理请求)
- 实现滚动更新
「底层实现原理」内核将listen状态的socket存放在32个槽位的哈希桶中,相同hash值的端口存放在同一个槽中,使用链表存放槽中不同端口的socket,当有请求时,先定位到哈希槽,然后遍历链表,对每个socket进行打分,取出得分最高的socket进行处理 (linux内核<4.5)对于启用了SO_REUSEPORT的socket,遍历链表后最高得分的socket会有多个,然后通过随机算法取出其中一个. (linux内核>=4.5) 由于每次请求都要遍历一遍链表,效率较低,所以引入了SO_REUSEPORT group,找到匹配的socket后,进行二次哈希找到对应的group组,从中选择一个进行请求处理
各种特性
nagle算法(减少频繁发送小包给对端)
「原理」
- 第一次发送时,不需要等待,立刻将包发送给对端
- 后面的数据需要满足一定条件才会进行发送
如果当前有【已发送未确认】的数据报文时,TCP会先将待发送数据先放到缓冲区,直到数据包大小达到MMS时,才会进行发送
如果之前发送的数据包都已经收到ACK了,会立刻发送数据包
「优缺点」 优点: 在网络延迟较高情况下,能有效避免大量的小包在网络中进行传输,提高带宽利用率(每个报文的有效数据较多) 缺点: 因为数据包可能会进行合组再一起发送出去,所以客户端传输数据会有一定延迟,对于需要实时预览的应用程序(ssh),nagle算法不太适用 默认开启,可在服务端通过设置TCP_NODELAY进行关闭
收到数据包后,什么时候会回复ACK?
「延迟确认」 收到数据包后,不会立刻返回ACK,会等待一段时间再确认,如果这段时间本端刚好有数据要传给对端,ACK可以随着数据一起发送出去,如果一段时间后还没有数据要传给对端,也会返回ACK确认 「立刻回复的场景」
- 收到了大于一个frame 的报文,需要调整窗口大小
- 处于 quickack 模式(quick && not pingpong)
当一端未发送数据时,该端默认将该连接视为非交互式连接(not pingpong),当该端发送数据后,该端会将该连接视为交互式,当延迟确认时由于超时返回ack(定时器内本端没有数据需要传给对端)时,该端又会将连接变为非交互式.
- 收到乱序包
- 如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!
TCP头部字段解析
TCP首部
端口号(源端口和目的端口)
各占2个字节,用来标示不同的应用程序,主机收到数据包后根据不同的目的端口号将数据包传递给不同的应用程序处理
❝保留端口:范围是0-1023,要监听这些端口需要root权限 已登记端口:范围是1024~49151,普通用户也能监听的端口范围 临时端口:一般客户端去连接服务端服务的时候,系统会为该连接分配一个临时端口(源端口),在 Linux 上能分配的端口范围由 /proc/sys/net/ipv4/ip_local_port_range 变量决定,在需要主动发起大量连接的服务器上(比如网络爬虫、正向代理)可以调整 ip_local_port_range 的值,允许更多的可用端口❞
序列号
每个包的TCP首都都有4个字节的序列号;序列号指的是报文段第一个字节的序列号;在SYN报文中的序列号称为初始序列号(ISN),用来交换连接双方的初始序列号;其他报文中的序列号用来解决乱序和重复问题
确认号
占4个字节;收到数据包后,TCP会发送ACK(确认号),ACK的值是下次希望收到的序列号值;确认号有两个作用:1、告知发送方序列号小于ACK的报文段都收到了;2、通知发送方下次应该要发送序列号为多少的报文
TCP flags
- SYN(Synchronize):用于发起连接数据包同步双方的初始序列号
- ACK(Acknowledge):确认数据包
- RST(Reset):这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无能为力处理
- FIN(Finish):通知对方我发完了所有数据,准备断开连接,后面我不会再发数据包给你了。
- PSH(Push):告知对方这些数据包收到以后应该马上交给上层应用,不能缓存起来
- 窗口大小
❝TCP首部只有16位表示窗口大小,也就是最大窗口大小才65535个字节,但有些报文的大小已经远远超过了65535个字节,所以引入了「窗口缩放」选项的比例因子,可选的值为0-14,表示将窗口扩大到原来的n^2倍,所以,实际的报文大小为「窗口大小」* (「窗口缩放」^2)❞
- 可选项
❝MSS: 最大段大小选项,是 TCP 允许的从对方接收的最大报文段 SACK: 选择确认选项 Window Scale: 窗口缩放选项❞
开始连接
三次握手
❝「降低三次握手带来的性能消耗的手段」 重用同一个tcp连接,避免重复创建和销毁 TCP快速打开(TFO, 在握手过程中传输数据)❝
- 客户端发送一个 SYN 包,头部包含 Fast Open 选项,且该选项的Cookie 为空,这表明客户端请求 Fast Open Cookie
- 服务端收取 SYN 包以后,生成一个 cookie 值(一串字符串)
- 服务端发送 SYN ACK 包,在 Options 的 Fast Open 选项中设置 cookie 的值
- 客户端缓存服务端的 IP 和收到的 cookie 值
第一次过后,客户端就有了缓存在本地的 cookie 值,后面的握手和数据传输过程如下: 1.客户端发送 SYN 数据包,里面包含数据和之前缓存在本地的 Fast Open Cookie。(注意我们此前介绍的所有 SYN 包都不能包含数据)
- 服务端检验收到的 TFO Cookie 和传输的数据是否合法。如果合法就会返回 SYN ACK 包进行确认并将数据包传递给应用层,如果不合法就会丢弃
- 服务端程序收到数据以后可以握手完成之前发送响应数据给客户端了
- 客户端发送 ACK 包,确认第二步的 SYN 包和数据(如果有的话)
- 后面的过程就跟非 TFO 连接过程一样了
「客户端和服务端要求」 在Linux支持TFO的内核版本下(Client内核版本为3.6;Server内核版本为3.7),在sysctl.config(vim /etc/sysctl.conf)中添加:net.ipv4.tcp_fastopen = 3, 其中1表示客户端开启,2表示服务端开启,3表示客户端和服务器同时开启
半连接和全连接队列
❝「半连接队列」 当客户端发起 SYN 到服务端,服务端收到以后会回 ACK 和自己的 SYN。这时服务端这边的 TCP 从 listen 状态变为 SYN_RCVD (SYN Received),此时会将这个连接信息放入「半连接队列」;服务端发送ACK SYN后,会开启一个定时器,如果超时还没收到ACK,将会进行重传,重传的次数由tcp_synack_retries参数决定 半连接满后,服务端会拒绝新来的请求❞
❝「全连接队列」 服务端发送ACK SYN并收到客户端的ACK后,连接会从半连接队列移到全连接队列中,等待应用调用accept取走,应用调用 accept() 函数会移除队列头的连接 全连接满后,服务端会丢弃客户端发来的ack(此时服务端会认为连接未建立成功,会重传ACK SYN)❞
SYN_BLOOD 攻击
❝「原理」客户端大量伪造 IP 发送 SYN 包,服务端回复的 ACK SYN 去到了一个「未知」的 IP 地址,这些处于SYN_RCVD的连接占满服务端的半连接队列大小,导致服务端无法处理其他正常请求❞
❝「应对方案」减少 SYN ACK 的 重试次数;及时将这些连接从半连接队列中清除出去 使用 tcp_syncookies 机制 ❝原理:服务端收到 SYN 包以后不会立刻将连接放到半连接队列中,而是根据这个 SYN 包计算出一个 Cookie 值,作为握手第二步的序列号回复 SYN ACK(服务端并不保存cookie),等对方回应 ACK 包时校验回复的 ACK 值是否合法,如果合法才三次握手成功,才将其放入全连接队列中等待处理 /proc/sys/net/ipv4/tcp_syncookies 默认 为1, 表示队列满时启动❞❞
结束连接
close和shutdown的区别
❝「int close(int sockfd)」 close会关闭两个方向的数据流 ❝读方向上,内核会将套接字设置为不可读,任何读操作都会返回异常; 输出方向上,内核会尝试将发送缓冲区的数据发送给对端,之后发送fin包结束连接,这个过程中,往套接字写入数据都会返回异常。 若对端还发送数据过来,会返回一个rst报文❞ ⚠️套接字会维护一个计数,当有一个进程持有,计数加一,close调用时会检查计数,只有当计数为0时,才会关闭连接,否则,只是将套接字的计数减一❞
❝「int shutdown(int sockfd, int howto)」 shutdown显得更加优雅,能控制只关闭连接的一个方向 ❝
howto = 0
关闭连接的读方向,对该套接字进行读操作直接返回EOF;将接收缓冲区中的数据丢弃,之后再有数据到达,会对数据进行ACK,然后悄悄丢弃。howto = 1
关闭连接的写方向,会将发送缓冲区上的数据发送出去,然后发送fin包;应用程序对该套接字的写入操作会返回异常howto = 2
0 1各操作一遍,关闭连接的两个方向。❞ ⚠️shutdown不会检查套接字的计数情况,会直接关闭连接❞
四次挥手
❝「为什么需要在TIME_WAIT等待一段时间」 避免新连接(使用同一个五元组的连接)收到旧连接的数据包,造成数据混乱 保证在ACK丢失后,可以进行重传,保证被动关闭连接端可以正常关闭连接(LAST_ACK->CLOSE)❞
❝「等待的时间为什么是2MSL」 保证新连接肯定不会收到旧连接的报文(因为报文在网络中最多生存1MSL) 在主动关闭方发送ACK后,被动关闭方正常情况下1个MSL内肯定可以收到,否则,被动关闭方会重发FIN包,主动关闭方会在1MSL收到,所以,这一来一回最久就是2MSL了❞
❝「TIME_WAIT连接太多会有什么问题」 ❝场景:客户端主动断开连接后立刻进行重连服务器,会导致客户端上有大量的TIME_WAIT状态 影响:客户端上临时端口不够用(大量端口处于TIME_WAIT)❞ ❝场景: 服务端主动断开连接,然后客户端立刻重连,如此往复,在服务端上会有大量的TIME_WAIT状态连接 影响:
- 服务端:占用服务端内存和CPU
- 客户端:客户端上的临时端口不够用(大量端口对应连接的服务端处于TIME_WAIT)
❝「TIME_WAIT太多时的场景及解决办法」 使用nginx等负载均衡连接后端服务,客户端断开连接后,nginx也会断开与后端服务的连接,导致nginx上存在大量的TIME_WAIT ❝调整net.ipv4.ip_local_port_range参数,增加临时端口的数量 使用连接池连接后端服务 添加nginx机器数量 添加nginx的配置ip数量 在nginx机器上启用tcp_tw_reuse参数❞ 服务端主动断开连接,导致服务端上有大量的TIME_WAIT ❝启用tcp_tw_recycle参数(慎用!确保客户端不是nat环境)❞❞
❝「相关调优参数」 net.ipv4.tcp_timestamps ❝属于tcp头部选项字段,由类型、长度、发送时间戳、回显时间戳4部分构成,共10个字节 需要连接双方都开启才能工作, 是否使用该特性是在三次握手中的SYN报文中协商确定的
- 发送方发送数据时,将一个发送时间戳 放在发送方时间戳TSval中
- 接收方收到数据后,在回复的报文中,将收到的时间戳填到Tsecr中,再把自己的时间戳填到TSVal中
❝net.ipv4.tcp_tw_reuse ❝需要开启net.ipv4.tcp_timestamps 用于客户端主动断开连接,在客户端机器上启用 重用连接后,会更新连接的时间,收到时间戳小与新连接时间的数据包都会被丢弃(解决了新连接收旧连接数据导致数据混乱的问题) 重用time_wait连接流程(把处于TIME_WAIT的主动连接/断开端称为A,对端成为B) ❝情况一:旧连接ACK未丢失,还在传输过程中,导致B还没收到ACK,处于LAST_ACK状态
- A发送SYN报文,处于SYN_SENT状态
- B收到旧连接的ACK后,进入CLOSE状态
- A没有收到ACK,所以会进行重传,之后就就是正常的三次握手了
情况二:旧连接ACK丢失,导致B还没收到ACK,处于LAST_ACK状态
A发送SYN报文,处于SYN_SENT状态
B由于迟迟没有收到ACK,所以重传FIN报文
A收到FIN后,回复一个RST报文
A没有收到SYN的ACK,所以会进行重传,之后就就是正常的三次握手了
情况三:B处于CLOSE状态 正常三次握手 net.ipv4.tcp_tw_recycle 需要开启net.ipv4.tcp_timestamps 用于服务端主动断开连接,在服务端机器上启用 开启后,tcp会快速回收处于TIME_WAIT的连接,并且记录下最后一次收到数据包的时间戳,之后在这个连接上如果收到早于这个时间戳的数据包,会直接丢弃 ⚠️如果是处于NAT网络或使用负载均衡连接后端服务的情况下,从服务端的角度看,是一个IP(负载均衡等代理)与它建立大量的连接,代理(负载均衡)很可能会使用「服务端还处于TIME_WAIT的socket」去建立新连接,这时候如果新连接中的时间戳比服务端记录的早,就会导致创建失败了(各客户端的时间可能不会百分百同步)
http/https
http各版本迭代
http://www.ruanyifeng.com/blog/2016/08/http.html
强缓存与协商缓存
强缓存
不需要访问服务器,直接使用本地磁盘/内存资源缓存
协商缓存
访问服务器,由服务器决定是否使用浏览器上的缓存
「流程」
- 第一次请求资源,服务器在响应头部中会返回expires(http1.0、GMT 格式的时间点字符串,代表资源失效的时间)、Cache-Control(http1.1、缓存策略)、Last-Modified(http1.0 资源最后修改时间)、Etag(http1.1 资源hash值)
expires/cache-control
- expires是1.0的规范,由于记录的是绝对时间,当客户端和服务端时间不同步时,会导致缓存混乱
- cache-control是1.1的规范
max-age:记录了缓存有效期,相对时间 缓存策略:
- no-cache 不使用本地缓存。需要使用协商缓存。
- no-store直接禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源
- public 可以被所有用户缓存,包括终端用户和 cdn 等中间件代理服务器
- private 只能被终端用户的浏览器缓存
优先级:cache-control > expires Last-Modified/Etag
- Last-Modified是1.0的规范,记录资源最后修改时间,存在以下缺点
- 有时候资源只是修改了更新时间,本身内容没有变化,此时不希望让用户重新拉取资源
- last-modified的记录粒度是秒,如果一个资源在1秒内被修改多次,无法感知
- 有些时候无法得到资源的最后修改时间
2. Etag是1.1的规范,记录资源的hash值
优先级:Etag > Last-modified
- 第二次请求资源时,先根据缓存中的cache-control/expires确定是否直接使用本地缓存
- 若无法使用本地缓存,浏览器会向服务器发起请求,请求头部带上If-Modified-Since=上一次的Last-modified;If-None-Match=上一次的Etag
- 服务器检查If-None-Match/If-Modified-Since, 如果判定资源没有修改,直接返回304;如果资源发生修改了,返回资源数据
优先级:If-None-Match > If-Modified-Since
https连接建立过程
https连接建立
「流程」
- 客户端获取服务端的证书
- 客户端生成一个随机数,用上一步证书中的公钥进行加密,并将加密后的信息发送给服务端
- 服务端获取后,通过私钥进行解密,获取到随机数
- 客户端和服务端通过随机数进行对称加解密