我想统计一下,TCP/IP 尤其是TCP协议,能搜到的常见的问题,整理起来,关键词添加在目录中,便于以后查阅。
目前预计整理共3篇:
[TCP] TCP/IP 基础知识问答 :基础知识
[TCP] TCP/IP 基础知识问答(2) :TCP协议相关知识
[TCP] TCP/IP 基础知识问答(3) :UDP协议相关知识
文章目录
- TCP协议相关知识
-
- 什么是TCP
- MTU和MSS分别是什么?
- 沾包和拆包
- TCP头包含哪些信息
- 常见TCP的连接状态有哪些?
- TIMEWAIT状态存在的意义
- TIMEWAIT过多的危害
- [服务器出现大量 CLOSE_WAIT 状态的原因](#服务器出现大量 CLOSE_WAIT 状态的原因)
- 如何优化TIMEWAIT
- 为什么需要三次握手,两次不行吗?
- 三次握手的过程可以携带数据吗
- 挥手为什么需要四次
- 挥手可以是3次吗
- 在四次挥手中,为什么发起端进入TIME_WAIT状态要有2MSL等待
- 什么是半连接队列
- listen的第二个参数
- [没有 listen,能建立 TCP 连接吗?](#没有 listen,能建立 TCP 连接吗?)
- [服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,客户端可以和服务端通信吗?](#服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,客户端可以和服务端通信吗?)
- SYN攻击是什么
- TCP如何确保可靠性的
- 序列号
- [TCP 重传机制](#TCP 重传机制)
- [TCP 流量控制](#TCP 流量控制)
- [TCP 拥塞控制](#TCP 拥塞控制)
-
- 拥塞窗口
-
- 慢启动
- 拥塞避免
- 拥塞发生
-
- [超时重传- 拥塞发生算法](#超时重传- 拥塞发生算法)
- [快速重传- 拥塞发生算法](#快速重传- 拥塞发生算法)
- 快速恢复
- 超时重传的拥塞算法图像
- 快速重传的拥塞算法图像
TCP协议相关知识
什么是TCP
TCP是面向连接的,可靠的,基于字节流的通信协议。
MTU和MSS分别是什么?
MTU是最大传输单元(maximum transmission unit)。
由硬件规定,比如以太网是1500字节.
MSS是最大分节大小(maximum segment size)。
在TCP传输中,MTU - IP头长度 - TCP头长度 == MSS, 在通信时,发送端会在TCP头中包含MSS的大小。
沾包和拆包
现象
沾包现象,不是TCP的bug,而是TCP传输带来的特点。
在程序执行的过程中,并不是调用send()函数后,就会直接发送出去,而是把应用层缓冲区中的
数据拷贝到内核缓冲区中,再拷贝到TCP协议栈的缓冲区中,然后由TCP协议栈来控制发送。
协议栈可能会把多个比较短的数据,合并成一个包来发送。也可能汇报超过MSS大小的数据,拆分成多个包来发送。
TCP协议是基于字节流的,协议栈理解的数据是没有边界的,没有包的概念。
沾包和拆包中的包,是用户态理解的概念,例如我多次调用Send(),就是用户态发出了多个包到内核态,如果这多个包都缓存在协议栈缓冲区等待发送,协议栈不会区分这是由几个Send()传输过来的数据,可能直接把所有数据都发出去了。
对策
- 发送固定长度的数据
- 用特殊结束符,比如\r\n,标记本条数据结束
- 给数据包添加头和尾
另外,如果程序设计式样允许,可以再Send()一次后,Sleep()一下,避免数据被沾包合并发送。
TCP头包含哪些信息
TCP头包含以下内容:
源端口和目的端口 (IP地址在IP头中),各16位,共4字节;
序列号和确认号 ,用于TCP包的顺序确认,各4字节,共8字节。
头部长度 ,TCP存在选择字段,所以TCP头是可以变长的(20-60字节),长度存储在此字段,4位,和保留字段g共占8位,1个字节。
标志字段 标志这个TCP包的作用,最常见的是ACK标志。
常用的标志位是:
ACK:确认序号有效标识
PSH:告诉协议栈,应尽快讲此报文交付给应用层
RST:重建连接标识。即TCP连接出现错误,连接断开,需要重新建立连接
SYN:发起连接时的标志
FIN:释放连接
窗口大小 这个值是接收端期望接收的字节数。窗口最大为65535字节。(如果有缩放因子选项,还要另外计算)。因为滑动窗口机制,需要告诉发送端,接收端还有多少窗口大小能接收数据。如果发送端,收到回复包中窗口大小的值小于MSS或者等于0,则说明接收端接收窗口空间不足,发送端协议栈会暂停发送数据。
校验和 发送端协议栈计算和写入,接收端进行校验数据
紧急指针 只有当URG标志置1时紧急指针才有效。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。紧急指针指出在本报文段中紧急数据共有多少个字节(紧急数据放在本报文段数据的最前面)。不常用。
以上为固定字段,共20字节
选项 会存储一些特殊选项,例如:窗口缩放因子,选择性ACK等
参考:TCP头部格式和封装
常见TCP的连接状态有哪些?
共11个状态。
LISTEN:socket在执行listen()后,会进入LISTEN状态,监听套接字
SYN_SEND:主动端在发出第一次握手的SYN包后会进入这个状态
SYN_RECV:被动端在收到第一次握手包,发出第二次握手的SYN ACK包后,会进入这个状态
ESTABLISHED:在双方完成三次握手后,都会进入这个状态,表示建立连接
(四次挥手)
FIN_WAIT1:主动断开端发送完第一次挥手的FIN ACK包后进入这个状态
CLOSE_WAIT:被动端在发出第二次挥手的ACK 包后,进入这个状态
FIN_WAIT2:主动端在收到第二次挥手的包以后,等待收到第三次握手的时候的状态
LAST_ACK:被动端在发出第三次挥手后,进入这个状态
TIME_WAIT:主动端在发出第四次挥手后,进入这个状态,会保持2MSL再进入CLOSED
CLOSED:断开连接的状态
CLOSING:是应对四次挥手的意外情况,主动方发出FIN ACK包,同时收到了FIN ACK包,就会进入CLOSING状态,等到收到了ACK,就会进入TIMEWAIT状态。
TIMEWAIT状态存在的意义
- 确保被动段开方能顺利断开连接
- 防止收到上一个相同连接的历史报文
TIMEWAIT过多的危害
-
如果主动断开连接的是客户端,TIMEWAIT在客户端出现过多,可能会导致端口耗尽:
每个TCP连接都是一个四元组,通过一个四元组就可以确定一个连接。
当存在TIMEWAIT时,这个连接就被占用了。客户端想要再连接服务器就需要建立新的连接,使用新的端口,但是客户端的端口是有限的,所以TIMEWAIT过多,可能导致端口耗尽。
-
如果出现在服务端,说明服务器主动断开了大量连接。服务器出现大量的TIMEWAIT可能会占用系统资源。
原因可能如下:
- 出现了大量的HTTP短连接。不论客户端还是服务端的HTTP头出现Connection:Close 都会使HTTP变成短连接,服务器会主动断开连接。这要排查服务器或者客户端时候哪里设置了Connection:Close ,要改成长连接
- HTTP长连接超时。这要排查,是不是什么问题导致客户端长时间无法向服务端发包
- HTTP 长连接超出允许的数量。
服务器出现大量 CLOSE_WAIT 状态的原因
原因是服务器作为被动断开方,因为某些原因没能调用close()结束连接。
需要调查为什么没有执行close()。
如何优化TIMEWAIT
优化系统参数:
修改/etc/sysctl.conf文件,一般涉及下面的几个参数:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout = 修改系统默认的 TIMEOUT 时间
net.ipv4.tcp_max_tw_buckets = 5000 表示系统同时保持TIME_WAIT套接字的最大数量,(默认是18000). 当TIME_WAIT连接数量达到给定的值时,所有的TIME_WAIT连接会被立刻清除,并打印警告信息。但这种粗暴的清理掉所有的连接,意味着有些连接并没有成功等待2MSL,就会造成通讯异常。一般不建议调整
net.ipv4.tcp_timestamps = 1(默认即为1)60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。也就是说服务器打开了 tcp_tw_reccycle了,就会检查时间戳,如果对方发来的包的时间戳是乱跳的或者说时间戳是滞后的,那么服务器就会丢掉不回包,现在很多公司都用LVS做负载均衡,通常是前面一台LVS,后面多台后端服务器,这其实就是NAT,当请求到达LVS后,它修改地址数据后便转发给后端服务器,但不会修改时间戳数据,对于后端服务器来说,请求的源地址就是LVS的地址,加上端口会复用,所以从后端服务器的角度看,原本不同客户端的请求经过LVS的转发,就可能会被认为是同一个连接,加之不同客户端的时间可能不一致,所以就会出现时间戳错乱的现象,于是后面的数据包就被丢弃了,具体的表现通常是是客户端明明发送的SYN,但服务端就是不响应ACK,还可以通过下面命令来确认数据包不断被丢弃的现象,所以根据情况使用
其他优化:net.ipv4.ip_local_port_range = 1024 65535 增加可用端口范围,让系统拥有的更多的端口来建立链接,这里有个问题需要注意,对于这个设置系统就会从1025~65535这个范围内随机分配端口来用于连接,如果我们服务的使用端口比如8080刚好在这个范围之内,在升级服务期间,可能会出现8080端口被其他随机分配的链接给占用掉,这个原因也是文章开头提到的端口被占用的另一个原因
net.ipv4.ip_local_reserved_ports = 7005,8001-8100 针对上面的问题,我们可以设置这个参数来告诉系统给我们预留哪些端口,不可以用于自动分配。
复制
优化完内核参数后,可以执行sysctl -p命令,来激活上面的设置永久生效
来源:https://cloud.tencent.com/developer/article/1589962
短连接改为长连接
setsockopt()设置SO_REUSEADDR
服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。
服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报Address already in use。
setsockopt()设置SO_REUSEPORT
1、允许将多个socket绑定到相同的地址和端口,前提每个socket绑定前都需设置SO_REUSEPORT。如果第一个绑定的socket未设置SO_REUSEPORT,那么其他的socket无论有没有设置SO_REUSEPORT都无法绑定到该地址和端口直到第一个socket释放了绑定。
2、attention:SO_REUSEPORT并不表示SO_REUSEADDR,即不具备上述SO_REUSEADDR的第二点作用(对TIME_WAIT状态的socket处理方式)。因此当有个socketA未设置SO_REUSEPORT绑定后处在TIME_WAIT状态时,如果socketB仅设置了SO_REUSEPORT在绑定和socketA相同的ip和端口时将会失败。解决方案
(1)、socketB设置SO_REUSEADDR 或者socketB即设置SO_REUSEADDR也设置SO_REUSEPORT
(2)、两个socket上都设置SO_REUSEPORT
为什么需要三次握手,两次不行吗?
不可以。
三次握手的目的:
- 确认两端手法能力正常
第一个握手包,证明发起端,发送数据能力正常。
第二个握手包,证明接收端,接受能力和发送能力正常。
第三个握手包,证明发起端,接收能力正常。
如果只有两次我收就建立连接,而发起端是无法发出第三次握手包的,那就会导致这个连接一直占用连接队列。 - 避免历史报文的影响
如果两次握手就建立连接。
客户端发出一个SYN包,然后宕机,立即重启后,又发出一个新SYN包。
如果服务器先收到旧SYN包,服务器返回一个SYN+ACK包
,此时服务器就认为连接已经建立了。
但是客户端收到这个SYN+ACK包后,通过ack num 发现不是自己刚刚发出的,不会进入ESTABLISHED状态,没有建立连接,返回一个RST报文。
在收到RST报文前,服务器以为建立了连接,有可能给客户端发送出数据了,浪费服务器资源。
而三次握手就可以避免这个问题。
服务器先收到旧SYN包,回复一个SYN+ACK包。
客户端收到以后,通过ack num 发现不是自己刚刚发出的,不会进入ESTABLISHED状态,没有建立连接,返回一个RST报文,服务器收到以后也不会进入ESTABLISHED状态。等收到信的SYN包后,再进行三次握手建立连接。
- 同步序列号
客户端发出SYN包, 以及初始化的syn num。
服务器收到以后,发出SYN+ACK包,发出初始化的syn num 和 ack num,ack num是SYN包的shn num + 1,代表服务器已经同步客户端的序列号。
客户端收到服务器的SYN+ACK包后,获取了服务器的syn num ,同步了服务器的序列号。
服务器收到客户端发出的ACK包,知晓客户端已经同步了服务器的序列号。
三次握手的过程可以携带数据吗
第三次握手可以。
挥手为什么需要四次
第一次挥手包,是发起端告诉接收端,我数据处理完了,要断开连接。
第二次挥手包,是接收端告诉发起端,我知道了,等我处理数据。
第三次挥手包,是接收端告诉发起端,我数据处理完了,要断开连接。
第四次挥手包,是发起端告诉接收端,我知道了,连接断开。
此时才可以确认双方都同意要断开连接了,没有数据发送了。
挥手可以是3次吗
可以。
被动挥手端 在收到第一次FIN包后,会进入CLOSE_WAIT状态,返回ACK包。等待数据处理完成,然后再发送FIN包。
如果满足以下条件,可以实现ACK包和FIN包一起发送。
1.没有应用层数据需要处理
2.TCP启动了延迟确认
延迟应答
延迟应答是默认开启的。
可以通过setsockopt 的 TCP_QUICKACK 选项关闭延迟确认。
接收方如果每次收到来自发送发的数据包后都立刻回复确认应答的话,可能会返回一个较小的窗口,这个窗口是用来通知发送方下次发送数据的大小。主要是因为接收方会先将数据放到缓冲区,待上层应用层将这些数据取走。而由于刚收到数据,应用层还没来得及取,此时缓冲区的可用空间变小,就会通知发送方要减少下次发送的数据长度。
当接收端收到这个小窗口的通知以后,会以它为上限发送数据,从而又降低了网络的利用率。
除此之外,如果只是单纯的发送一个确认应答,代价又会很高。因为IP头部有20字节,TCP头部也有20字节(此处不计选项)。
原文链接:https://blog.csdn.net/LOOKTOMMER/article/details/121522110
所以TCP的延迟确认,就是:
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
- 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方(捎带应答)
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
延迟确认等待时间大概100-200ms。
在数据包通信频繁的时候,延迟确认是有好处的。但是如果数据包通信不频繁,延迟确认可能会导致通信效率降低,可以配置系统参数缩短延迟确认时间,或者setsockopt 关闭延迟确认。
在四次挥手中,为什么发起端进入TIME_WAIT状态要有2MSL等待
什么是MSL
MSL是报文最大生存时间(maximum segment lifetime),即一个数据包在网络中最大存在的时间。
为什么等待2MSL
发起挥手方在发出第四次挥手包后,进入TIME_WAIT状态,但是有可能这个包丢失了。发出第三个挥手包的接收方,迟迟等不到第四个挥手包,他会等Min(1MSL,超时时间)的时间,重发第三个挥手包。
重新发出的第三个挥手包,最长会经过1 * MSL 时间,到达发起方。
这样就出现了2MSL时间。
等待2 MSL时间,可以避免,因为丢包而导致的对端无法正常断开连接。
另外,也是为了防止网络中还有发给发起方的数据包没有收到,如果没有2MSL的等待,发起方断开连接后,迅速重启了连接,可能会收到上一个连接的包。
什么是半连接队列
如果服务器接收到了客户端发来的第一次握手包,会把这个连接放入半连接队列。
当三次握手完成后,会把这个连接放入全连接队列,等待Accept()函数调用。
listen的第二个参数
listen函数的第二个参数,是backlog。
这个参数的含义,不同的地方有不同的解释,比如与有的地方规定他是半连接队列和全连接队列的和。
在linux的新版本和Windows中,他表示的是全连接队列的大小。
而半连接队列的大小,Linux系统中由系统参数tcp_max_syn_backlog来控制,
全连接队列的大小,取backlog参数和系统参数somaxconn的较小者。
Linux中,在listen状态下,netstat或者ss 命令显示的 RECV-Q表示当前accept队列大小,SEND-Q表示accept队列的最大大小。
Windows中也有SOMAXCONN的宏,如果Listen()的第二个参数设置为这个宏,就会采用系统中的最大合理值来设置全连接队列的大小。
没有 listen,能建立 TCP 连接吗?
可以。
TCP Socket 在Bind()后,就可以connect它自己的IP和Port。
服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,客户端可以和服务端通信吗?
不可以。服务端的TCP的半连接队列和全连接队列是在Listen()的时候实现的。
由于没有Listen(),也就没有队列,没有地方存储客户端的连接。
如果此时,客户端对服务端发起了连接建立,服务端会回 RST 报文。
SYN攻击是什么
服务端在收到第一次握手的SYN包以后,会把连接加入半连接队列,为连接分配资源,并发处二次握手的包,
等待第三次握手的包,如果第三次握手的包超时没有收到,就会再次重发二次握手的包,要重发好几次,才会把连接移出半连接队列。
SYN攻击就是攻击者模拟大量的IP地址,向服务端发送大量的SYN包,来占用服务器资源,使服务器无法响应正常的连接。
在linux中可以用 netstat 命令检查处于SYN_RECV状态的TCP连接,如果出现很多随机地址,可能是SYN攻击。
通常现在的网关已经可以过滤SYN攻击了。
也可以在系统这设置减少超时事件、或者增大半连接队列。
也可以通过SYN Cookie技术防御SYN攻击。
半连接队列、全连接队列、SYN攻击,参考:
会把这个连接放入半连接队列。
TCP如何确保可靠性的
- 三次握手、四次挥手,建立可靠连接
- 通过序列号进行应答机制,可以丢弃重复的包,可以发现丢包
- 超时重传避免丢包
- 拥塞控制,避免网络拥堵导致的丢包
- 流量控制,避免接收方处理不完数据,接口窗口占满导致的丢包
- 校验和,TCP头中有校验和数据段,可以用来校验数据是否损坏
序列号
TCP头中有序列号(seq num)和确认号(ack num)各占4字节。
TCP协议会为发送的数据中的每一个字节分配序列号,本次发送的包中的第一个字节的序列号就是TCP头中的seq num。当对端收到了这个包以后,返回的确认包中,ack num 就是 接收到的seq num + 数据字节数 + 1。
ISN(Initial Sequence Number)是固定的吗
不是,初始的seq num不是固定的,是随机的。
是为了避免,收到了就得连接中的包的
序列号回绕了怎么办
序列号只有32位,也就是一个无符号int型的大小,是存在序列号用尽,从头开始的可能的。
为了避免这种情况,就有了TCP时间戳选项。
TCP时间戳
TimeStamp选项是默认开启的,它会随着时间增长,如图。
如果出现树序列号绕回,可以通过时间戳比较是不是最近的通信。
时间戳共8个字节,4个字节是发送该包的事件,4个字节是
TCP 重传机制
通过序列号与确认号,TCP协议可以确保数据的有序传输。也可以发现有哪些数据丢包了。当发生丢包时,就会启动重传机制。
超时重传
RTT
RTT是往返时延,Round-Trip Time ,即一个数据包发出,到收到确认包的时间。往返时延是一个动态变化的值。
RTO
RTO是超时重传时间,Retransmission Timeout ,RTO应该略大于RTT。
当发出的数据包丢失或者确认包丢失,都会引起超时重传。
TCP协议栈发出包的时候,会启动一个定时器,当达到RTO时间时,即发生了超时,认为数据包丢失,会再次发出包,然后重启RTO计时,此时的RTO应该是上一次的2倍。
这样每一次重新计算的RTO都是上一次的两倍,一般会进行3轮超时重传,如果还没有收到确认应答,会在等待一段时间后(2MSL?)关闭连接。
一旦发生了超时,会被认为网络拥塞,就会触发TCP的拥塞控制策略。
快速重传
为了避免每次都等到超时时间到了,才开始重传。TCP还有快速重传机制。
如果发送端发送了多个包,序列号为,1、2、3、4、5。
接收端收到了1,回复 ACK ack num = 2;
没收到2。
收到了3,回复 ACK ack num = 2;
收到了4,回复 ACK ack num = 2;
此时虽然没到超时事件,但是发送收到了连续三个确认包,要求发送2号包,就可以判断2号包丢包了。
此时虽然没有超时,客户端会立刻重传2、3、4、5包。
因为客户端不知道接收端除了2号包,3、4、5号包有没有收到,如果只重传了2号包,如果3号也丢包了,那还要等超时或者3次3号包的确认包才能确定丢包,效率很低,所以会全部重传。
如果想要避免全部重传,可以使用SACK(选择性重传)。
为何快速重传是选择3次ACK
3次是一个经验值。如果是2次ACK就进行重传,会收到更大的包乱序的影响。
详细参考:TCP 快速重传为什么是三次冗余 ACK,这个三次是怎么定下来的? - 车小胖的回答 - 知乎
SACK
SACK,Selective Acknowledgment,选择性确认。
启动了SACK功能后,会在TCP头部的选项区域中,增加一个SACK字段。
当发生快速重传的时候,服务器可以在SACK字段中记录收到了哪些包。
这样发送端就可以只重传丢失的包了。
但是SACK不是默认开启的。
需要通信双方在三次握手协商是否开启。如下图,一个SYN包中有SACK选项。
D-SACK
D-SACK,Duplicate SACK, 用于接收方告诉发送方哪些数据重复接收了。
如果接收端的ACK确认包丢失了,会导致发送端重传,当接收端接收到重传包后,发现重复接收,就会发送D-SACK,告诉发送端,重复接收了,我已经收到过这个包。
D-SACK可以方便发送端判断网络状况。
D-SACK使用的是TCP头中SACK的字段。
在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能
TCP 流量控制
滑动窗口
为了避免发送一个包,必须等到收到确认包才能继续发下一个包的低效率通信。
TCP协议采用了窗口机制。
每个TCP协议栈都一个发送窗口,一个接收窗口。窗口就是一个动态的缓冲区,当服务端的接收空口空闲的时候,客户端可以不必等待确认应答到来,就向服务端发送多个包。窗口动态变化的过程就是滑动窗口机制。
窗口的大小
在TCP的头中,有一个Windows字段,这就是窗口的大小。是接收方用于告诉发送方自己还有多少的接收窗口大小。发送方不会发送超出接收方窗口大小的数据。强行发出会导致数据丢失。
累计应答
由于接收端有接收窗口,可以接收多个TCP报文 ,所以即使中间有个报文的去人包丢失了也没关系,只要最终的报文的确认包成功发出去了就好。发送端你收到了最终发送的报文的确认包就知道前面全部数据都收到了。这就是累计应答。
滑动窗口缩放因子
滑动窗口缩放因子,Window Scaling。
在TCP的头的Windows字段,记录了窗口的大小,但是它是16位的,最大就是65536,在如今已经不满足要求了。
TCP头还有缩放因子选项,可以扩大窗口。
假设window scale为7,也就是要将Window Size的值左移七位,即乘以128。window scale最大为14。
在整个双方的交互过程中,发送方和接收方Window size scaling factor乘积因子必须保持不变,但是发送方的乘积因子和接收方的乘积因子可以不同,由各自决定。
例如这个包,TCP的头的windows字段是64240,缩放因子是左移8位,即放大256倍。此时表示的窗口的大小是:64240 * 256。
发送窗口的控制
发送窗口部分的内存,通过3个指针 进行管理,其中两个绝对指针,一个相对指针。
绝对指针是指向具体序列号数据的指针,相对指针是通过绝对指针地址计算后计算出来的指针。
第一个绝对指针,指向已发送但是没收到确认数据的第一个字节。
第二个绝对指针,指向还没发送且可以发送的数据的第一个字节。
第三个相对指针,指向还没发送且不可发送的数据的第一个字节,通过第一个指针 + 滑动窗口大小(滑动窗口大小是会变化的)得到。
第一个指针指向的数据 和 第二个指针指向的数据,合在一起就是发送方的滑动窗口。
第二个指针指向的数据,就是可用窗口。
接收窗口的控制
发送窗口部分的内存,通过**=2个指针**进行管理,其中一个绝对指针,一个相对指针。
第一个绝对指针,指向可以接收数据但是还没收到数据的空间的第一个字节。
第二个相对指针,指向还没收到数据并且不可接收数据的空间的第一个字节,通过第一个指针 + 滑动窗口大小(滑动窗口大小是会变化的)得到。
第一个指针指向的数据 就是发送方的滑动窗口。在第一个指针之前,是已收到并且已确认的数据。(由于ACK直接由TCP协议栈回复,默认无应用延迟,不存在"已接收未回复ACK)
滑动窗口的流量控制
TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
死锁
在服务器因为某些情况,导致窗口关闭后,客户端不可以发送数据。
服务器窗口再度开放后,会给客户端发送一个ACK报文,告诉窗口非0。
如果这个报文丢失了,就会造成死锁。
死锁的解决方法
TCP 为每个连接设有一个持续定时器,只要客户端收到服务端的关闭窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文。服务器在ACK报文中会告知窗口大小。如果还是关闭窗口,客户端就再次计时。
如果客户端多次进行窗口探测,服务器窗口都是关闭,可能会断开连接。
糊涂窗口综合征
本部分内容引用自:小林Coding - 糊涂窗口综合征
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。
所以,糊涂窗口综合症的原因是:
接收方可以通告一个小的窗口
而发送方可以发送小数据
对策方向是:
让接收方不通告小窗口给发送方
让发送方避免发送小数据
对策
让接收方不通告小窗口
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
延迟确认
==延迟确认也有避免发送小窗口的作用==参照上面延确认内容
等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
怎么让发送方避免发送小数据呢?
使用 Nagle 算法。
Nagle算法
Nagle :/ˈneɪgəl/
该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:
条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS;
条件二:收到之前发送数据的 ack 回包;
Nagle算法会避免发送小数据。但是有些场合可能就是需要发送小数据。可以
可以在 SetSockopt中通过TCP_NODELAY 选项来关闭本Socket的Nagle算法。
TCP 拥塞控制
流量控制,是用来控制窗口的。
拥塞控制是根据网络状况而进行的控制。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大...
所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。
于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
拥塞窗口
TCP用发送窗口,和拥塞窗口。
拥塞窗口是根据网络拥塞状况来动态变化的。
对于发送端来讲,需要获取接收端的接收窗口大小,也要获取拥塞窗口大小。
发送端的发送窗口 约等于 ming(接收端的接收窗口,拥塞窗口)
只要发生了超时重传,就会认为网络出现了拥塞,就会减小拥塞窗口。反之拥塞窗口会增大。
而拥塞窗口的变化,主要是依赖四个算法:
慢启动
拥塞避免
拥塞发生
快速恢复
慢启动
慢启动就是TCP刚建立连接时,在其实拥塞窗口的基础上,慢慢增大拥塞窗口的意思,其增大规则是:
发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
例如,当前窗口,发送方可以发送10个TCP报文,然后收到了10个确认应答报文。此时发送方的拥塞窗口增大到可以发送20个TCP报文。
慢启动算法是指数性增长。
慢启动门限
慢启动门限, ssthresh (slow start threshold),一般是65535 字节。
当 拥塞窗口 < ssthresh 时,使用慢启动算法。
当 拥塞窗口 >= ssthresh 时,就会使用「拥塞避免算法」。
拥塞避免
拥塞避免阶段,每当收到一个 ACK 时,cwnd 增加 1/cwnd。
此时是线性增长。
虽然拥塞避免阶段,拥塞窗口增长的速率放慢了,但是最终可能还是会进入拥堵,开始出现丢包。一旦出现丢包,进入拥塞发生阶段。
拥塞发生
在发生丢包后,会进入超时重传,或者快速重传。
不同的重传方式,对应了不同的拥塞发生算法。
超时重传- 拥塞发生算法
此时,慢启动门限ssthresh 设为 cwnd/2,同时cwnd 重置为 初始化值,然后再进行慢启动。
门启动门限ssthresh初始值
Linux 针对每一个 TCP 连接的 cwnd 初始化值是 10,也就是 10 个 MSS,我们可以用 ss -nli 命令查看每一个 TCP 连接的 cwnd 初始化值。
小林Coding - 拥塞发生
快速重传- 拥塞发生算法
此时认为网络情况没有那么糟糕。
拥塞窗口 =拥塞窗口/2 ,也就是设置为原来的一半,这里和上面一样。
但是慢启动门限设置为和拥塞窗口一样大。然后,进入快速恢复算法。
快速恢复
快速恢复和 拥塞发生-快速重传 算法搭配使用,此时认为网络拥塞没有那么糟。
本部分内容引用自:小林Coding - 快速恢复
在 拥塞发生-快速重传的基础上,拥塞窗口 = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
重传丢失的数据包;
如果再收到重复的 ACK,那么 cwnd 增加 1;
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;