概述
068.从TCP/IP协议角度看一个Web请求的过程
首先这请求涉及到三个网络,一个是客户端局域网络,中间是一个广域网,第三个网络是服务器端局域网
用户在客户端局域网的某台主机上发起一个Web请求,首先由数据链路层,将请求数据发送到客户端的路由器上
到达路由器后,由网络层(IP层)在广域网中选择一个到达服务器端局域网路由器的最短的路径
从客户端主机开始,TCP协议将数据拆分成TCP认为合适大小的Segment段,然后将这些段顺序按照IP层选定的广域网路径发送到服务器端路由器
这些Segment段在传输过程中可能存在丢包或者广域网路径发生改变的情况
当服务器端的主机收到这些Segment段之后,因为TCP层是由操作系统的内核实现的,所以服务器端的主机内核在收到这些Segment段之后会发送给
服务器上的象tomcat等web容器,web容器处理完请求后生成一个响应报文交给内核,内核以原路返回的方式将报文发送给客户端
在整个构成中,如何选定数据传输的路径,是由网络层(IP层)和数据链路层来决定的
而如何构造一个消息,如何构造一个响应是由应用层来实现的
但是消息如何可靠的发送以及如何保证消息的顺序,都是由TCP层实现的
069.TCP协议的特点
1.它主要解决的问题是在IP协议之上解决网络通讯可依赖的问题
2.面向连接的,一定是点对点的连接,它不能像UDP协议那样向多个主机同时发送消息
3.可靠的,
4.双向传递(全双工)
5.基于字节流的 打包成报文段,保证有序接收,重复的报文段自动丢弃
缺点:不维护应用报文的边界,比如HTTP报文必须去定义rn来做结尾,或者使用Content-Legnth来确定报文的边界等
优点:不强制要求应用必须离散的创建数据块,不限制所传输数据的大小
6.流量缓冲 解决客户端和服务端处理消息的速录不一致的问题
7.可靠的传输服务(保证可达,丢包重试)
8.拥塞控制
070.数据传输过程中相应的报文头部的添加和消除过程
1.源主机在应用层构造一个消息
2.由tomcat等WEB容器发送给主机内核,内核将消息拆分成多个Segment段,并为每一个Segment段添加一个TCP头部
3.为Segment添加网络层(IP)头部
4.为Segment添加数据链路层头部
5.消息传递到物理层
6.这时消息到达路由器
7.路由器剥离数据链路层的头部
8.路由器接续网络层的头部,确定好数据传输路径
9.路由器重新添加上数据链路层头部
10.路由器重新添加上网络层头部,并将数据发送给交换机
11.交换机剥离数据链路层头部
12.交换机重新添加上数据链路层头部
13.数据到达目标主机的物理层
14.目标主机所在网络的交换机剥离消息的数据链路层头部
15.目标主机所在网络的路由器剥离消息的网络层头部
16.数据到达目标主机内核,内核剥离传输层头部
17.将数据发送给应用层
071.TCP协议的任务
1.主机内的进程寻址
2.创建、管理、终止连接
3.处理并将字节流打包成报文段(如IP报文)
4.传输数据
5.保持可靠性和传输质量
6.流控制和拥塞控制
072.TCP如何标识一个连接
TCP四元组(源地址,源端口,目的地址,目的端口) 目标地址用于确定主机,目的端口用于确定目标主机的哪一个进程
073.TCP报文段中比较重要的几个字段
1.Source Port 源地址+源端口
2.Destination Port 目标地址+目标端口 所以Source Port + Destination Port 是用来确定TCP连接的
3.Sequence Number Segment段的ID,用来丢包重发,重复丢弃,保证消息接收顺序等
4.Acknowledgment Number 确认报文的ID,即表示这是一个针对某个Segment的ACK
074.TCP Options
TCP的常规报文段的长度是20个字节,而20个字节后面是一个可选的值叫做TCP Options
TCP Options常用的选项有:
0 无意义的,表示选项列表末尾标识
1 无意义的,用于32位对齐
2 MSS值 握手时发送端告知可接收的最大报文段大小
3 窗口移位 指明最大窗口扩展后的大小
4 表明支持SACK选择性确认中间报文段功能
5 确认报文段 选择性确认窗口中间的Segments报文段
8 Timestamps时间戳 用于更精确的计算RTT,以及解决长肥网络中的PAWS问题
14 校验和算法 双方认可后,可使用新的校验和算法
15 校验和 当16位标准校验和放不下时,放置在这里
34 FOC TFO中Cookie
075.TCP三次握手的作用
1.同步Sequence序列号
初始序列号ISN
2.交换TCP通讯参数
比如MSS、窗口比例因子、选择性确认、指定校验和算法
076.TCP三次握手的过程
TCP三次握手过程中涉及到两种报文,一种是SYN,一种是ACK
1.客户端向服务器端发送一个SYN报文,告诉服务器端自己的初始Sequence序列号,假设是123456
2.服务器端给客户端回复一个ACK报文,表示收到了来自客户端的SYN报文,
ACK报文的Sequence字段的值是客户端发送过来的SYN+1 即123457
并且同ACK报文一起发送一个SYN报文告诉客户端服务器端的Sequence序列号 假设是654321
3.客户端回复一个ACK报文给服务器端,这个报文的Sequence字段的值是客户端发送过来的SYN报文的Sequence序列号+1 即654322
至此TCP连接的三次握手就建立完成了,注意在三次握手过程中的ISN是随机生成的,不重复的,不是从0开始
077.TCP的报文格式
源端口和目的端口:端口是应用层和传输层通信的桥梁,传输层的复用和分用功能都要通过端口才能实现
序列号:占4个字节,TCP连接中传输数据流中的每一个字节都编上一个序号,序号字段的值是之本报文段所发送的数据的第一个字节的序号
针对SYN报文而言,这个值指的是TCP连接的初始序列号
确认号:占4个字节,是期望收到的对方的下一个报文段的数据的第一个字节的序列号
数据便宜/首部长度:它表示TCP报文段的数据起始处距离TCP报文段的起始处有多远
保留:占6位,保留以后使用,但是目前为止是0
URG:=1时,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应该尽快传送,即优先级叫高的数据
ACK:=1时,表示该报文段时一个应答报文,只有当ACK=1是,确认号字段的值才有意义
PSH:=1时,接收TCP收到PSH=1时的报文段,就尽快地交付接收应用进程,而不是再等到整个缓存都填满了再向上交付
RST:=1时,表明TCP连接中出现严重差错(如由于主机宕机),必须释放当前连接,然后再重新建立连接
SYN:=1时,表示这个报文段是一个建立连接的SYN报文段
FIN:=1时,表示这是一个终止连接的报文
检验和:检验和字段检验的范围包括首部h和数据这两部分,在计算校验和时,要在TCP报文段的前面加上12字节的伪首部
紧急指针:指出在本报文段中紧急数据共有多少字节
选项-MSS:长度可变,最初的时候只规定了一种选项,即最大报文段长度MSS,
选项-窗口大小:占3字节,其中有一个字节表示移位值,新的窗口值等于TCP首部中的窗口位数增大到(16+S),
相当于把窗口值向左移动S位后获得的实际窗口大小,简单来说就是表示当前发送该报文段的TCP源还能接收的最大报文段大小-窗口大小
选项-时间戳:包含时间戳字段和,时间戳会送回答字段
选项-选择性确认:接收方收到了和前面的字节流不连续的两字节,如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但是
要把这些信息准确的告诉发送方,使发送方不要再重复发送这些已经接收到的数据
填充:这是为了使得首部长度是4的整数倍
078.TCP连接的状态
1.首先客户端和服务器端都处于CLOSE状态
2.当服务器监听一个进程端口之后就进入到LISTEN状态
3.客户端向服务器端发送第一个SYN报文后,就进入SYN-SENT状态
4.服务器端收到客户端发送过来的第一个SYN报文后,就从LISTEN状态进入SYN-RECEIVED状态
5.当服务器向客户端发送一个ACK报文后,客户端收到之后就从SYN-SENT状态变成ESTABLISED状态
6.这时客户端向服务器发送一个ACK报文,服务器端在收到这个报文之后连接的状态从 SYN-RECEIVED状态进入 ESTABLISH状态
查看内核TCP建立所有连接的状态
LINUX netstat -anp | grep tcp
WINDOWS netstat -anop
079.TCP三次握手的服务器端实现
在服务器内核太内存中存在着两个队列SYN队列和ACCEPT队列
1.客户端向服务器端发送第一个SYN报文,服务器端收到该报文后,会向SYN队列中添加一条连接信息数据(这时这个连接处于SYN-RECEOVED状态),并向客户端发送一个ACK
2.客户端收到服务器端发送过来的ACK报文,并向服务器端发送一个ACK报文,
3.服务器端在收到客户端发送过来的ACK报文中之后,将SYN队列中的对应的那条记录取出,塞入ACCEPT队列
这时当应用程序调用TCP的socket的accept方法时,就从ACCEPT队列中取出对应的连接拿去给客户端发送消息
080.服务器端TCP连接的超时时间与缓冲队列,在/etc/sysctl.conf文件中设置一下属性
net.ipv4.tcp_max_syn_backlog 用来设置内核中处于SYN-RECEVIED状态的TPC连接数,也就是SYN队列的大小
net.ipv4.tcp_synack_retries 用来设置,当服务器被动连接时,发送SYN/ACK的重试次数
net.ipv4.tcp_syn_retries 用来设置,服务器主动连接时,发送SYN的重试次数
net.ipv4.ip_local_port_range 用来设置建立连接时的本地端口的可用范围
081.Fast Open 功能(TFO)
在一般的WEB服务器中,每一次客户端和服务器发送一个应用消息(HTTP请求),都是需要与服务器端进行三次握手的(就是所谓的短链接)
每一次客户端与服务器端进行数据交互的过程是
客户端向服务器端发送一个SYN
服务器端向客户端发送一个SYN+ACK
客户端向服务器端发送一个ACK + DATA
服务器端向客户端发送一个DATA
即每进行一次DATA交换都至少需要两个RTT
Fast Open的工作原理是
第一次向服务器端发送消息和普通的一样,还是两个RTT,但是在服务向客户端发送SYN+ACK的时候会给客户端传送一个cookie
这样当第二次客户端与服务器端交换数据的过程就变成
客户端向服务器端发送 SYN+cookie+DATA ,服务器收到之后,根据cookie去直接从ACCEPT队列中取出相应的连接,
并交给应用层,应用层拿到这个连接后给服务器端返回一个DATA 这样就只需要一个RTT
TOMCAT不支持TFO NGINX 支持TFO
在LINUX上设置TFO
通过在/etc/sysctl.conf文件中设置属性net.ipv4.tcp_fastopen
0:关闭
1:作为客户端时可以使用TFO
2:作为服务器时可以使用TFO
3:都可以使用TFO
082.如何应对SYN攻击
攻击者在短时间内伪造不同的IP:PORT地址的SYN报文,因为源IP:PORT是伪造的,所以服务器收到SYN报文后,无法向伪造的主机发送ACK
这样就导致内核太的SYN队列被快速占满,使得服务器不能接收正常客户的请求,可以通过以下方式从一定层度上避免SYN攻击
net.core.netdev.max_backlog 接收自网卡,但是未被内核协议栈处理的报文队列长度
net.ipv4.tcp_max_syn_backlog SYN-RECEVIED状态的TCP最大连接数,也就是SYN队列的大小
net.ipv4.tcp_abort_on_overflow 超出处理能力时,对新来的直接回包RST,丢弃连接
net.ipv4.tcp_syncookie = 1 当SYN队列满时,接收到的SYN不进入SYN队列,而是计算出cookie再以SYN+ACK中的序列号返回给客户端,并直接在ACCEPT队列中建立一个连接
当客户端正常发送数据报文时,服务器根据报文中携带的cookie,从ACCEPT队列中取出对应的连接信息交给服务器端的应用程序使用
但是这样做有一个弊端,就是由于TCP头部的大小是固定大小的20字节,而cookie需要占用序列号空间,导致此时所有的TCP可选功能失效
(就是TCP OPTIONS失效),例如扩充窗口,时间戳等
083.TCP_DEFER_ACCEPT
就是服务器端在收到客户端发送过来ACK报文,如果这个ACK报文中没有携带DATA报文,那么则不激活应用程序去处理这个请求
而是在接收到后面的DATA报文之后,再去激活应用程序,这提高了应用程序的效率
084.MSS
max segment size 最大报文段大小
TCP层将接收到的数据都存放在一块特定的内存区域中,当应用程序调用socket的read方法时,可以将这块内存区域中的数据读取出来
比如JAVA中的SocketChannel.read 就是从这块内存区域中读取内容
而应用程序有数据要发送给客户端时,则先是将内容(可能是部分,也可能是全部,当使用缓冲区的时候就是部分)写入到特定的区域
然后这个TCP会从这块内存区域读取数据拆分成Segment报文段发送给对方的TCP,例如JAVA中NIO的SocketChannel.write就是干件事的
为什么TCP要对字节流进行分段
1.因为网络层(IP层)和数据链路层都有自己特定的内存大小,(也就是路由器和交换机等这些设备的内存空间只有有限的一点)
因此网络层和数据链路层一次能够处理的数据大小是及其有限的,如果TCP层不进行分段的话,那么IP层也会对消息进行分段
而IP层的分段是非常低效的
2.流量控制,即假设某一时刻客户端由于内存不够或者其他原因导致只能接收3个字节了,这时服务器端一次将所有的数据发送过取,客户端是显然无法处理的
MSS的值是在三次握手过程中协商好的
客户端的向服务器发送的第一个SYN报文中的TCP OPTIONS选项值为4,表示这是自己可处理的最大报文段大小
而服务器端向客户端回复的ACK报文里面,也用同一个选项告知服务器端自己可处理的最大报文段大小
085.重传与确认
PAR 客户端向服务器发送一个segment之后,启动一个定时器,直到收到客户端的确认segment时,停止当前计时器,表示一个segment发送完毕
如果计时器的值超过了重试时间值都没有收到服务器端发过来的ACK,那么就对这个segment进行重发,直达收到服务器发送过来的ACK为止
发送第二个segment时,重新启动定时器,因此它总是等到前一个segment的ack被接收到之后才发送第二个segment,它们时串行执行的,效率非常低
PAR 的改进版
为每一个segment设置一个序列号,服务器端回复ACK的时候也把这个序列号带上告诉发送端当前这个ACK是针对特定序列号的ACK
另外服务器端在发送ACK的时候还会带上当前服务器的缓冲区大小,告知客户端当前服务器还能接收的最大报文长度
这样发送端就可以并行地向服务器端发送消息了,而不必等到直到收到上一条消息的ACK后再发送第二条消息
RTT 一个消息从发送端发出开始,到接收到来自接收端的ACK为止,这一段时间就是一个RTT
在内核实现中每一个Sequence都有一个数据结构叫做TCB用来存储报文段发出和接收到ACK的时间,用这两个时间点相减就能计算出RTT
但是实际的RTT计算则更加复杂,是通过发送时间,接收时间和数据包中timestamp选项的回显时间来计算出来的
Sequence Number 在改进版本的PAR中,是针对每一个Segment段一个序列号
但是在TCP中是每一个字节一个序列号
RTO 同一个报文段,在首次发送时间点,与失败之后重发的时间点的差值,就叫做RTO,
RTO 的值最好是略大于RTT SRTT = (A * SRTT) * (1-A)*RTT) RTO = min[UBOUND, MAX[LBOUND, B*SRTT]]
最终经过大量的数据测试,RTO的取值,在全球范围内的操作系统有一个公认的计算公式
086.滑动窗口
滑动窗口的作用就是流量控制并保证数据不会丢包
一般而言滑动窗口的大小是固定的,发送端的发送窗口大小约等于接收端的接收窗口大小,即发送端会根据接收端的接收窗口大小来发送数据
在建立连接的SYN中,发送端会在第一个SYN报文中通过窗口大小选项来告知自己的接收窗口大小,
而接收端在回复第一个ACK的时候也会使用窗口大小选项告知发送端自己的接收窗口大小,发送端在接收到这个ACK之后将自己的发送窗口设置成接收到的ACK里的窗口大小的值
随着后面数据的发送,接收端的可用接收窗口会发生改变,这个改变会通过报文发送给发送端,让发送端根据这个可用窗口大小发送适量大小的数据
滑动窗口的大小跟应用处理数据的速度(即读取缓冲区数据的速度),以及操作系统缓冲区的大小是有关系的
发送窗口 假设三次握手之后确定了发送窗口的大小是20字节,但是我们要发送的总的数据大小是100字节,显然是不能一次就发送完的
发送窗口的快照
1.已发送b并且收到ACK的数据 假设是 1 ~ 31 字节
2.已发送未收到ACK的数据 假设是 32 ~ 45 字节
3.应用程序已经调用socket的write方式要求TCP发送的数据,还未发送,但是在接收方的可用接收窗口范围内的那一部分数据 假设是 46 ~ 51 字节
4.应用程序已经调用socket的write方式要求TCP发送的数据,还未发送但是在接收芳的可用接收窗口范围之外的那一部分数据 假设是 52 ~ 100字节
其中32 ~ 51 字节就是发送窗口(即建立连接时收到的第一个ACK里的WINDOWS选项的值) 而 46 ~ 51 称之为可用窗口
当我们把46~51字节的数据发送出去的时候,可用窗口就耗尽了,这时的快照是 32 ~ 51字节都是处于已发送但是未收到ACK的状态
然后我们又收到了32~36字节的ACK,这时的发送窗口就可以向高字节方向移动5个字节,这时发送窗口的快照是
1.已发送b并且收到ACK的数据 是 1 ~ 36 字节
2.已发送未收到ACK的数据 假设是 37 ~ 51 字节
3.因为有5个字节的数据收到ACK,所有又空出了5个字节的可用窗口,所以 未发送但是在接收方可处理范围内的数据是:52 ~ 56字节
4.未发送但是在接收方可处理范围外的数据是57 ~ 100字节
所以整体看起来就是一个方块在一条线上滑动,所以叫做滑动窗口,方块就是发送窗口,而线就是字节流
接收窗口 假设在三次握手时通过ACK发送给发送端的窗口大小是20字节,那么接收窗口的快照就如下以发送窗口的第一个快照为例
1-31字节位已接收的数据
32-51 字节是如果发送端发送来了少于或者等于20字节的内容,这时可以处理的长度,叫做可用窗口
51-无穷大 字节是如果送端发送来了超过20字节的内容,这时就不能处理的长度,叫做可用窗口
这时如果发送端发送一个超出20字节的内容,那么超出20字节的那一部分数据将被丢弃
这时如果应用程序没有能够及时的从TCP缓存中取走数据,那么接口窗口的可用窗口大小就有可能小于20字节
窗口关闭 假设接收端接收窗口的初始可用窗口为20字节,这时发送端发送了一个10字节的内容过来,接收到收到这10个字节的内容放入缓冲区
这时可用窗口变成了10字节,这时接收端通过ACK告知发送端可用窗口为10字节,但是应用程序没有来即使的读取这个缓冲区,
接收端收到ACK后知道对方的可用窗口现在只有10字节了,这时又给接收端发送了5字节的数据过去,接收端收到后,
又回复一个ACK告知可用窗口为5字节,但是应用程序依然没有来读取TCP的缓冲区,
接收端接收到ACK后得知对方的可用窗口只有5字节了,本来还没有发送的数据还有7字节,但是由于对方只有5字节的可用窗口了
于是又给对方发可一个5字节的数据过去,接收端收到这5字节的数据之后由于应用程序一直没有来读取TCO缓冲区的内容,所以
这时可用窗口变成了0,于是发送一个ACK给发送端告知可用窗口为0 当发送端收到这个windows为0的ACK后,自己的发送可用窗口变成0,不能再继续发送数据了
这就是窗口关闭
窗口关闭的时候,发送端是不会被动的等待接收端发送报文来告知它有可用窗口了,而是定时向接收端发送一种窗口探测报文去检测接收端的可用窗口大小
窗口压缩 就是服务器因为某些原因将每一个TCP连接的接收窗口突然压缩,比如某一个正在通信的TCP连接,本来的可用窗口是1000字节,但是现在突然被压缩成500字节了
但是这时客户端因为从上一个ACK那里得到得可用窗口大小是1000字节,于是给客户端发送了一个700字节的数据过去,
这时由于这个TCP连接的可用窗口只有500字节了,那么收到这700字节的数据中后面200字节将被丢弃。导致丢包
而实际上操作系统是不会发生这种情况的,操作系统会先压缩窗口,然后再压缩缓存
带宽时延积 就是带宽与时延的乘积,比如带宽是100M 而 时延是 1秒 那么贷款时延积就是 100M * 1M,通常比较合理的做法是将TCP连接的缓冲区大小设置成为带宽时延积
TCP缓存 TCP单个连接缓存分为两部分,一部分是可用窗口缓存,一部分是应用缓存
接收缓存 net.ipv4.tcp_rmem 最小值4096 默认值87380 最大值4194304
发送缓存 net.ipv4.tcp_wmem 最小值94500000 默认值915000000 最大值927000000
net.ipv4.tcp_adv_win_scale 表示应用缓存的比例,默认值为2,表示应用缓存占TCP缓存的1/4 如果是-2则表示接收窗口占TCP接收缓存的四分之一,如果=1表示各占一半
net.ipv4.tcp_moderate_rcvbuf = 1 表示开启自动调节TCP缓存大小
087.糊涂窗口综合征 SWS
假定得时server端非常繁忙,假设三次握手建立完毕之后发送端的发送窗口和接收端的接收窗口都是360字节
这时client发送360字节的数据给server,server收到360字节的数据之后,发现应用程序这时只能处理120字节的数据,
于是就给客户端返回一个ACK告知可用窗口为120字节,client收到都又发了一个120字节的数据过去,server收到之后发现应用程序只能处理
40字节的数据,于是又发送ack告知可用窗口为40字节,client收到之后又发送一个40字节的数据过去,server收到之后发现应用程序只能处理
20字节的数据... 这样就导致了client端总是发送小报文,传输效率非常底下
088.糊涂窗口综合征避免算法
David D Clark算法:窗口b边界移动值小于 min(MSS, 缓存/2)时,通知可用窗口围为0
Nagle算法:没有已发送未确认的报文段时,立刻发送数据
存在已发送未确认的报文段时,直到:1-没有已发送未确认报文段,或者2-数据长度达到MSS时再发送
在任何支持TCP标准套接字的操作系统上,将socket的TCP_NODELAY标志选项设置为true就能够金庸Nagle算法
089.TCP delayed acknowledgment 延迟确认
当有相应数据要发送时,ack会随着相应数据立即发送给对方,
如果没有响应数据,ack的发送将会有一个延迟,以等待看是否有响应数据一块发送
如果在等待发送ack的期间,对方又有新的数据段到达了,那么则立刻发送ack
等待的时常是:
TCP_DELACK_MAX = HZ/5
TCP_DELACK_MIN = HZ/25
其中HZ是操作系统的最小时钟频率,在LINUX中可以通过命令cat /boot/config-`uname -r` | grep '^CONFIG_HZ=' 来查看时钟频率
时钟频率是指,在单位时间内所产生的脉冲信号的数量,一般而言所说的时钟频率就是值CPU的主频,即CPU内核工作的时钟频率
在LINUX中可以通过设置套接字的TCP_QUICKACK=1来关闭TCP的延迟确认功能
090.TCP_CORK 和 sendfile
TCP_CORK的意思就是要求传输过程中的报文段大小不得小于某一个值,即所有的报文段都是大报文
TCP_CORK是LINUX系统特有的,它必须结合sendfile来使用
sendfile就是所谓的零拷贝技术,正常的TCP发送数据都是先将磁盘中的数据由应用程序拷贝到应用程序内存,然后再拷贝到TCP缓冲区,然后再发送
sendfile功能的操作流程是:直接从硬盘将数据拷贝到TCP缓冲区,所以称为零拷贝技术
091.拥塞控制
网络拥塞的形成原因:就是再带宽一定的链路上有多个TCP连接同时都在进行通信,假设存在一个链路A,它的带宽是1000M/S
TCP连接1与A之间的传输速率是700M/S,TCP连接2与A之间的传输速率是600M/S,那么当连接1与连接2的数据同时到达链路A的节点时
如果没有拥塞控制,那么就存在丢包的风险(最多可能丢掉300M的包)
拥塞控制的几个部分:慢启动、拥塞避免、快速重传、快速恢复
092.慢启动:
所谓慢启动就是基于谷歌提出的以探测带宽为依据的BBR算法来定义慢启动
慢启动时如何发挥作用的呢?慢启动中定义了一个拥塞窗口cwnd
在前面我们知道在没有加入拥塞控制以前,发送窗口约等于接收窗口(就是ACK报文中的WINDOW字段),我们称接收窗口为通告窗口 rwnd
在加入拥塞窗口之后,发送窗口 swnd = min(cwnd, rwnd)。
谷歌的带宽探测 BBR算法的执行过程是
第一个请求时的cwnd = 10个MSS (早期是1个MSS,2~4个MSS,后来谷歌经过大量的统计得出一般一个网页的大下差不多是10个MSS所以定成10)
收到第一个请求的ACK之后,CWND = 10 * 2 个MSS
收到第二个请求的ACK之后,CWND = 10 * * 2 个MSS 依次下去
就是逐步的探测网络的带宽,就是在开始通信之前我发送端和接收端都不知道当前这个链路的带宽时多大,先悠着点少发一点数据,当收到ACK确认没有丢包的时候在逐步增大cwnd
093.拥塞避免
如果没有拥塞避免,假设当CWND上升到80个MSS的时候,实际上已经造成拥塞丢包了,
慢启动阈值ssthresh(slow start threshold) ,即当慢启动过程以指数的形式使得CWND增到到阈值之后就不再以指数的形式增长,而是线性的增大
比如在CWND到达40个MSS的时候,下一次收到ACK的时候将CWND不再设置成为80个MSS而是设置成为41个MSS。当然并不是简单的+1 ,它有一个计算公式cwnd += SMSS*SMSS/cwnd
然后如果在进入线性增加的过程中发生了丢包事件,那么这时将ssthresh设定成为发生丢包时的cnwd的一半,然后再让发送端以初始的cwnd(10个MSS)重新进行慢启动过程
,然后到达新的ssthresh之后,又进入拥塞避免线性增长期
094.快速重传与快速恢复
失序数据段 client端向服务器端发送一个报文pkt0,server端回复一个ack0,client端又向server端发送一个报文pkt1,但是pkt1在传输过程中被丢掉了
这时client又向sever端发送一个报文pkt2,server端收到了pkt2之后回复一个ack,但是这个ack里面的确认序列号却是pkt1的序列号即pkt0的序列号+1,
因为TCP的序列号与确认序列号是累加的,后面如果server没有收到pkt1那么无论client向server在发送多少个报文,那么server回复的ack的序列号都是pkt1的序列号
这个pkt2,pkt3...就称为失序数据段
快速重传 假设的情况是client端发送了 pkt1 pkt2..pkt8到 server端,但是pkt5在传输过程中被丢掉了
那么当server端在收到pkt6的时候回复ack5,在收到pkt7的时候还是回复ack5,在收到pkt8的时候还是回复ack5
这时client端连续收到了三次ack5,这时它就知道pkt5发生了丢包,于是client不会等到重传定时器走完再重新发送pkt5而是立刻将pkt5发送给server
这时如果server收到了补齐的pkt5报文时,则立刻发送一个ACK,并且这个ack的确认序列号时它期待的下一个要传输过来的报文的序列号,这时应该是ack9
接收方 1.当收到一个失序数据段时,不走延迟确认立刻回复失序报文端的ack 2.当收到填充失序缺口的数据段时,不走延迟确认,而是立刻发送它期待的下一个ACK确认序列号
发送方 当3此收到相同的失序ACK时,不等待重传定时走完,而是立刻发送被丢包的数据报文
095.选择性确认SACK
在上一例子中, client端连续三次收到ack5的时候,它就知道pkt5发生了丢包,但是它并不知道pkt6,pkt7,pkt8有没有发生丢包,于是它可以采用两种重传策略
积极乐观:client端认为pkt6,pkt7,pkt8都被server端收到了,于是它只重传pkt5
消极悲观:client端认为pkt6,pkt7,pkt8也没有被server端收到,于是它重传pkt5、pkt6,pkt7,pkt8
有了SACK,SERVER端就明确的告知client端收到了哪些报文,当它在收到pkt8的时候,在ACK的报文头部的ackno任然是pkt5的SequenceNo,但是它的
OPTIONS的类型的值是4就表示支持SACK,如果type是5,那么OPTIONS后面的字节存储的就是pkt6,pkt7,pkt8的SequenceNo范围,这样当client连续三次收到ACK5的时候
由于三次握手的时候通过type=4,双方已经协商好支持SACK,这时就去查看这个ACK的OPTIONS,type=5,并且取出OPTIONS后面的字节数的内容,得知server端已经收到了pkt6,pkt7,pkt8
于是client端就很容易判断出只是pkt5发生了丢包,于是只重传pkt5
096.谷歌BBR拥塞控制算法
传统的拥塞控制算法是基于丢包来驱动的,而谷歌的BBR拥塞控制算法是基于测量来驱动的拥塞控制算法
它的总体思想是尽量的控制发送端的发送速率以找到RTT与带宽的最佳平衡点,使得瓶颈路由器的排队队列刚好为空
它具体的操作步骤是:
在数据刚开始传输的前一段时间之内进行测量,测量出RTprop(最小RTT) 和 BtlBw(最大带宽,即最大传输速率),得到这两个值以后就能够计算出最佳控制点从而控制发送速率
随着数据传输在广域网中的链路是会发生改变的,一旦链路发生改变那么RTprop和BtlBw都会发生变化,而BBR算法通过pacing_gain来重新计算RTprop 和BtlBw
pacing_gain的执行过程是周期性的按照1.25配速和0.75倍速来发送数据测量当链路发生改变时的RTT和带宽
现在假设一个20秒的测量周期在1秒的时候测量得到的带宽是10M(也即10M/S,或者10bps就是 网络带宽的基本单位是bit/s 即10M的话就是 1M=1000KB,而1K = 10000bit
所以10M = 1000 * 1000bit/s,实际上1bit通过光纤来传输就是1次电信号,10M就表示1秒钟之内可以传送1000 * 1000次电信号),而在5秒的时候链路发生了变化,
链路的带宽突然增大到20M(同时RTT没有变小),这时pacing_gen通过反复的周期性的测量就会逐渐的增大BtlBw,当到10秒的时候周期性测量还在继续,这时使用1.25倍速时
发现带宽没有变大,变成0.75倍速时带宽也没有变小,在执行几个周期发现带宽都没有变大或者变小,于是发送端的BtlBw就稳定在20M,跟我们链路的最大带宽吻合,
然后在20秒的时候,链路的带宽从20M突然降至10M,这时因为我们的发送速率还是按照20M的带宽对应的速率来发送的,于是在网络中飞行的报文就会积压,然后RTT也突然升
得很高,这时重新启动pacing_gain流程,反复执行0.75配速的时候发现RTT在大幅的下降,最后稳定在10M的水平
097.关闭TCP连接
初始的时候client和server都处于ESTABLISHED状态
假设client端要关闭连接,这时它会想服务器发送一个tcp报文,这个报文头部的FIN标志位被设置成1因此我们称这个报文为FIN,当发出这个FIN之后client端就进入FIN-WAIT-1状态
server端在收到这个FIN以后会向client端发送一个ACK1,这时server端处于CLOSE-WAIT状态,等待服务器端的应用层调用socket的close方法来关闭连接
client端收到ACK1之后,就从FIN-WAIT-1变成FIN-WAIT-2状态
这时如果server端的应用程序调用了socket的close方法时,操作系统的内核就会发一个报文给client端,并且这个报文的头部的FIN标志位被设置成1,我们也称之为FIN包,
在发送出这个FIN包之后,server端就进入了LAST-ACK状态
client收到server端发送过来的FIN包之后就会回复一个ACK2,然后立刻进入TIME-WAIT状态,在TIME-WAIT状态等待两个MSL单位时间之后就进入close状态
(MSL就是报文从client端发送到接收到ack这段时间可以理解为最大RTT,即报文的最大飞行时间)
server在收到client端发送的ACK2之后也进入close状态
098.关闭TCP连接优化
一般而言,TCP连接处于TIME-WAIT的状态要持续2分钟左右,而作为服务器而言如果有大量的TIME-WAIT状态的连接存在会影响机器的性能,也会导致端口被占用
net.ipv4.tcp_tw_reuse = 1 开启后,作为客户端时新连接可以使用任然处于TIME-WAIT状态的端口,如果net.ipv4.tcp.timestamps = 1存在,那么操作系统会拒绝上一个连接的迟到的报文
net.ipv4.tcp_tw_recycle = 1 默认=0,如果开启后,表示同时作为客户端和服务器都可以使用TIME-WAIT状态的端口,它是不安全的,无法避免报文延迟、重复等给新的连接造成混乱
当发现一个服务出现不响应TCP的SYN请求的时候就应该查看一下,服务器是否net.ipv4.tcp_tw_recycle和net.ipv4.tcp.timestamps是否同时开启了
如果是同时开启的话,那么在60S内同一源ip主机的socket connect请求中的timestamp必须是递增的,也就是说如果服务器打开了tcp_tw_recycle,就会检查时间戳,如果对方发来的包
的时间戳是乱跳的或者时间戳是滞后的,这样服务器肯定不会回复接收到的SYN请求,它把带有倒退的时间戳的包当作是上一个连接的重传数据,不是新的SYN请求,
因此,在开启tcp_tw_recycle的时候千万不要再开启tcp_timestamps
net.ipv4.tcp_max_tw_buckets = 262144 控制处于TW状态的最大连接数,如果到达最大连接数后又有新的TW连接产生,那么新的连接将被直接关闭
RST复位报文,即将报文头部的RST标志位设置为1,就是告诉对方直接关闭连接,绕开4次握手,这种情况在遇到一些突发事件或者非常严重的错误的时候会使用
099.KEEP-ALIVE
一个TCP连接如果长时间没有收到或者发送任何数据的时候,主机就会开启进入KEEP-ALIVE探测周期
net.ipv4.tcp_keepalive_time = 7200 (单位是秒)即一个TCP连接在两个小时都没有进行任何数据交互的时候会进入KEEP-ALIVE探测周期
net.ipv4.tcp_keepalive_intvl = 75 进入探测周期之后,每隔75秒就向对方发送一个探测报文,如果对方回复了,那么则进入下一个7200周期,如果没有收到那么75秒之后再次发送探测报文
net.ipv4.tcp_keepalive_probes = 9 一共发送9次探测报文后都没有收到对方的应答,那么则直接关闭连接
100.校验和
16位2字节的校验和,它校验TCP报文承载的数据、TCP头、IP层的源地址、IP层的目的地址进行校验,它采用的是累加和的校验方式,可以改变校验和算法,如果2字节不够放可以放到OPTIONS中
TCP头部包含了源端口和目的端口,IP头部包含了源IP地址和目的IP地址
101.PSH
TCP头部中的PSH标志位设置为1时表示:
假设应用程序发送了一个100M的数据,这100M的数据是被TCP拆分成很多个Segment报文段进行发送的,在发送最后一个Segment报文段的时候会将PSH标志位设置位1
接受端在接收到这个报文段之后发现它的PSH标志位是1,那么它将尽快将积压在TCP缓冲区的数据交给应用层去处理而不是等到挤压到多少字节后一次性交给应用层
102.URG
紧急标志位,TCP头部中的URG标志位是1,表示这个报文将被优先交给应用程序处理,比如使用FTP下载一个大文件的时候,点击了一个取消下载,那么取消下载这个指令将被文件所在的那一方优先处理
103.TCP连接的多路复用技术
A.非阻塞SOCKET 当我们使用非阻塞socket去读取数据得时候,不一定要等到内核的读缓冲区里一定有数据,而是有数据则将数据返回,没有数据也立刻返回,而不是内核的缓冲区没有数据
的时候就是read等待一个超时时间才返回,write写函数也是一样的,如果可用的内核的写缓冲区或则发送窗口为0的时候,就立刻返回而不是等到超时时间,如果写缓冲区有数据,那么就
根据数据量和发送窗口的大小能写多少就写多少
B.epoll 它维持了两个树蕨结构,一个是所有TCP组成的红黑树,一个是当对应读写缓冲区或者发生变化的TCP连接组成的队列,这样的话应用程序就只取处理队列里面的这些TCP连接就行了
而epoll应该是会去定时的检测红黑树中的TCP的读写缓冲区或者状态是否发生改变,然后将这些发生改变的TCP连接放入待处理队列,这样就算一个服务器有一百万个TCP连接,但是在某一
时刻可能只有几百个TCP的读写缓冲区或者状态发送了变化,于是就把这几百个TCP放入待处理队列,等待应用程序去处理
C.现在一种流行的web服务器编程理念是协程 = 非阻塞 + epoll + 应用程序同步
比如openrestry中的一段lua代码,它去连接redis的时候的处理方式是,connect方法在于redis服务器建立进行3次握手的时候,向redis服务器发送了一个SYN,而这时connect方法并不会
等待redis服务器返回ACK,而是切换到下一个TCP连接的处理,一旦内核收到redis服务器的ack之后,就给将处理结果返回给调用connect方法的应用程序方法
这样看来在connect方法内部是异步的,即发送SYN之后就切换了,而不是等待ACK,但是在外部调用connect方法的应用方法是同步的,应为它会等待connect方法返回伪代码如下
local client = redis:new()
client:set_timeout(30000)
local ok,err = client:connect(ip,port) //这里就是同步等待connect的返回,而connect方法的内部实际上是有一个进程在异步的处理TCP交互
if not ok then
ngx.say("failed", err)
return
end
104.四层LB到底做了些什么
客户端发送TLS/SSL到服务端的LB,LB可以将客户端发送来的TLS/SSL原封不动的交给应用层去处理,也可以在4层LB上安装相应的证书将接收到TLS/SSL解析成普通的TCP报文交给应用层处理
也可以是客户端发送的是不同的TCP报文,然后在4层LB上安装相应的证书将普通的TCP报文变成TSL/SSL报文交给应用层处理
如果服务端配置了一个4层负载均衡,于多太业务服务器处于一个企业内网里,客户端只与这个4层的LB建立TCP连接进行交互,而4层LB与多个业务服务器分别建立连接,
而内部还可以进行多层LB,比如业务服务器与数据库服务器之间再加入一个负载均衡,通常而言对外的负载均衡器要起到一个TLS/SSL的剥离和授信的作用
四层负载均衡与7层负载均衡的区别
在微服务中的api-gateway就属于7层负载均衡,它时通过请求的url来转发请求的,比如/user 和 /product 分别流向用户服务和产品服务的业务服务器,而这时如果/user服务有三台主机
那么ribbon的负载均衡策略,比如轮询或者随机选择一台IP:PORT的处理user服务的主机进行处理,这个就属于4层负载均衡,当然4层负载均衡一般都处于服务器的最前沿,就是在api-gateway
的前一个环节,还有数据链路层基于MAC地址的二层负载均衡,即请求到达一个虚拟的MAC地址,然后经二层负载均衡转发到具体某一个真是的MAC地址主机进行处理,
还有三层负载均衡,即消息先发到一个虚拟的IP地址,经由三层负载均衡转发到真是的具体的IP地址的主机进行处理
最后
以上就是默默小鸽子为你收集整理的TCP协议的全部内容,希望文章能够帮你解决TCP协议所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复