三次握手
三次握手协议的过程:
a.客户端 向 服务器端 发送一个 SYN 包,请求一个主动打开。该包携带客户端为这个连接请求设定的随机数A作为消息列号。
b.服务器端接收到一个SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身携带一个随机产生的序号B。
c.客户端收到SYN/ACK包后,发送一个ACK的包,该包的序号被设定为A+1,而ACK的确认码为B+1。当服务器端收到这个ACK包的时候,把请求帧从SYN队列中移出,放置ACCEPT队列中。
场景:当服务器端接收到客户端发送过来的SYN后, 回了SYN-ACK后,客户端掉线了,服务器端没有收到客户端回来的ACK,那这个连接 就 处于 一个中间状态,没成功也没失败。
但是,服务器端如果在一定时间内没有收到TCP会重新发SYN-ACK。
- 主机收到一个TCP包时,用两端的IP地址与端口号来标识这个TCP包属于哪个session。使用一张表来存储所有的session,表中的每条称作TCB(Transmit Control Block)。
- TCB结构的定义包含:连接使用的源端口, 目的端口,目的ip, 序号, 应答序号, 对方窗口大小, 已方窗口大小, tcp状态, tcp输入/输出队列, 应用层输出队列, tcp的重传有关变量等。
- 服务器端的连接数量是无限的,只受内存的限制。
tcp报文头
- 来源连接端口,16位长,识别发送连接端口
- 目的连接端口,16位长,识别接收连接端口
- 序列号(seq,32位长)
- 确认号(ack,32位长),期望收到的数据的开始序列号,也即已经收到的数据的字节长度加1
- 资料偏移(4位长),以4字节为单位计算出的数据段开始地址的偏移值。
- 保留,需置0
- ACK—为1表示确认号字段有效
- SYN—为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
- FIN—为1表示发送方没有数据要传输了,要求释放连接
- RST—为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求
- 紧急指针(16位长)—本报文段中的紧急数据的最后一个字节的序号
- 窗口(WIN,16位长)—表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控制
- 校验和(Checksum,16位长)—对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段
重传机制
方式
-
超时重传
-
概念
- 发送数据时设定一个定时器,若在指定时间内没有收到应答报文,就会重发数据
-
发生超时重传的时机
- 数据包丢失
- 确认应答丢失
-
超时时间RTO选择
- 略大于RTT
- 重传超时策略:超时时间间隔加倍
-
-
快速重传
-
概念
- 发送方可以一次发送多个数据包,若中间的某个数据包丢失了,接收方会一直回复这个丢失的数据包应答报文,接收方若收到三次这个数据包的应答报文,就知道该报文还没有被接收方收到,可以重传这个数据包
-
问题
- 因为接收方对后续的数据包也返回了丢失的那个应答报文,所以发送方不知道后续的数据包是否丢失,也就不知道应该重传丢失的数据包还是把后续的数据包都重传
-
-
SACK
-
概念
- 选择性重传,解决快速重传不知道重传哪些报文的缺点
-
实现方式
- 在tcp头部“选项”字段里加一个SACK,将缓存的地图发送给发送方,这样发送方就知道哪些数据接收方收到了,哪些数据接收方没有收到,以便重传接收方没有收到的数据
-
参数
- 要开启SACK,需要发送方和接收方都支持:net.ipv4.tcp_sack,linux2.4以后默认打开
-
-
D-SACK
-
概念
- 对SACK的扩展,通过SACK告诉发送方哪些数据被重复接收了
-
实现方式
- 若有发送方有重复发送数据包,会通过SACK告诉发送方这这个数据包已被发送过
-
好处
-
发送方能够知道是发出去的包丢了还是接收方发送的ACK丢了
- SACK若告知这个包已被发送过,那么说明是接收方发送的ACK丢了
-
发送方可以知道发出去的数据包是否被网络延迟了
- 发送方在延迟之后会重发数据包,之前的数据包若一段时间后到达了接收方,接收方返回的应答报文中能看到该数据包已经被接收了,是个重复的报文,说明被网络延迟了,而重发的数据包已被收到
-
-
序列号与确认应答(ACK)保证了tcp的可靠传输
滑动窗口
引入原因
- 每发送一个数据都需要进行确认应答,收到了再发送下一个,效率比较低。在窗口大小限制范围内,可以无需等待上一个数据包应答,就可以继续发送下一个数据包
错误处理
-
累计应答
-
发送数据包丢失
- 若发送方发送了一批数据包,中间的某个数据包丢失,那么接收方回复应答时ACK会回复丢失的那个数据包,这样接收方就知道丢失的数据包是哪一个,因此可以重新发送;
-
应答报文丢失
- 若接收方返回的这一组数据包中,某一个应答报文丢失了,只要最后一个应答报文发送成功了,接收方就知道这个数据包的其实是收到了的
-
窗口大小
-
tcp头里的字段window
- 接收方通过这个字段告诉接收方自己还有多少缓冲区可以接收数据,发送方就可以根据接收方的处理来发送数据,以免导致接收方处理不过来,因此窗口的大小是由接收方决定的
-
接收方和发送方的窗口
- 接收方和发送方的窗口大小基本相等,因为发送方的窗口大小取决于接收方,当接收方处理能力快,窗口变大,通过tcp报文中的window字段告诉接收方,若传输过程中出现了延迟,所以这时两个窗口大小不一致
拥塞控制
概念
- 流量控制是避免发送方填满接收方的缓存,但若因为其他主机之间的通信造成网络拥堵,会有超时和丢包发生,这样会导致重传 ,网络负担会更大,进入恶性循环,所以tcp不能忽略网络上发生的事,当网络发生拥堵时,它会降低数据的发送量。拥塞控制的目的就是避免发送方的数据填满整个网络
拥塞窗口cwnd
-
发送窗口swnd和接收窗口rwnd是约等于的关系,有了拥塞窗口的概念后,发送窗口swnd=min(cwnd, rwnd)
- 网络中没有出现拥塞,cwnd会增大,反之会减小
-
判断网络拥塞的方法
- 发生超时重传
-
相关算法
-
慢启动
- tcp刚建立连接完成,会有个慢启动的过程,一点一点提高发送数据包的数量
- 每接收到一个ack,cwnd大小就会加1,初始化时,cwnd大小为1,即cwnd大小按指数级增长
-
ssthreshold,slow start thresold,慢启动门限
- 当cwnd < ssthreshold时,会采用慢启动算法
- 当cwnd >= ssthreshold时,会使用拥塞避免算法
-
拥塞避免
- 每收到一个ack,cwnd增加1/cwnd。即囤积了cwnd这么多个包后,一次性发送过去。之后都会这样发送,cwnd按线性增长
- 当一直这么增长,会慢慢进入拥塞状况,于是开始出现丢包现象,这时需要对丢失的数据进行重传,当触发了重传机制也就进入了拥塞发生算法
-
拥塞发生
-
超时重传
- ssthreshold设为cwnd/2
- cwnd设为1
- 即一旦发生超时重传就重新进入慢启动
-
快速重传
- 当接收方发现丢了一个中间包时,会发送三次丢失包的ack,发送方收到后就会快速重传丢失的包
-
tcp认为这时候拥塞并不严重,只丢了一小部分包,于是
- cwnd = cwnd/2
- ssthreshold变为cwnd
- 然后进入快速恢复算法
-
-
快速恢复
- cwnd = ssthresold+3
- 重传丢失的数据包
- 如果再收到重复的ack,那么cwnd+1
- 收到新的ack后,cwnd变为ssthreshold,然后进入拥塞避免算法
-
流量控制
概念
- 流量控制是基于滑动窗口实现的,tcp通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制
问题
-
描述
- 接收方收到了太多数据,暂时不能接收数据了,于是返回了window大小为0
- 发送方发现window大小为0,于是暂时不再发送数据包了
- 接收方处理完数据,可以继续处理了,于是在之前已处理完的数据包的应答报文中更新窗口大小,但是该应答报文丢失了,导致接收方没能收到,于是就一直不发送新的数据包了,出现了死锁
-
解决方法
- tcp为每个连接设定一个持续计时器,只有发起连接的一方从对方收到零窗口通知,就启动计时器;如果持续计时器超时,就会发送窗口探测报文,对方会给出自己现在接收窗口的大小
- 窗口探测次数一般为3次,每次大约30-60秒,如果三次后窗口还是0,有的tcp实现会发起RST报文来中断连接
糊涂窗口综合症
-
发送方
- 发送方虽然知道窗口很大,但是每次都只发送很少的数据
-
接收方
- 接收方太忙,每次都只能从window中取出很少的数据,然后通知发送方,这样发送方每次也只发送很少的数据
-
问题
- 每次都只传输小包,效率低
-
解决方法
-
让接收方不通知小窗口给发送方
-
当窗口大小小于min(mss,(缓存空间/2))时,就会向发送方通知窗口为0,阻止发送方后续发送数据过来
- 注mss,Maximum Segment Size,最大报文长度,MSS是TCP报文段中的数据字段的最大长度,不包括TCP首部的长度
-
-
让发送方不发送小包
-
Nagel算法
-
发送条件
- 等到窗口大小>=mss或数据大小>=mss
- 收到之前发送数据的ack应答
- 满足发送条件的两点才会发送
-
设置关闭
- Nagel算法默认开启,若想要关闭,需要在socket设置TCP_NODELAY来关闭。没有全局参数,需要每个应用根据自己的特点来关闭
-
-
-
Silly Window Syndrome翻译成中文就是“糊涂窗口综合症”。正如你上面看到的一样,如果我们的接收方太忙了,来不及取走Receive Windows里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的window,而我们的发送方会义无反顾地发送这几个字节。
要知道,我们的TCP+IP头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。
另外,你需要知道网络上有个MTU,对于以太网来说,MTU是1500字节,除去TCP+IP头的40个字节,真正的数据传输可以有1460,这就是所谓的MSS(Max Segment Size)注意,TCP的RFC定义这个MSS的默认值是536,这是因为 RFC 791里说了任何一个IP设备都得最少接收576尺寸的大小(实际上来说576是拨号的网络的MTU,而576减去IP头的20个字节就是536)。
如果你的网络包可以塞满MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。(大于MTU的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送) 你可以想像成一个MTU就相当于一个飞机的最多可以装的人,如果这飞机里满载的话,带宽最高,如果一个飞机只运一个人的话,无疑成本增加了,也而相当二。
所以,Silly Windows Syndrome这个现像就像是你本来可以坐200人的飞机里只做了一两个人。 要解决这个问题也不难,就是避免对小的window size做出响应,直到有足够大的window size再响应,这个思路可以同时实现在sender和receiver两端。
- 如果这个问题是由Receiver端引起的,那么就会使用 David D Clark’s 方案。在receiver端,如果收到的数据导致window size小于某个值,可以直接ack(0)回sender,这样就把window给关闭了,也阻止了sender再发数据过来,等到receiver端处理了一些数据后windows size 大于等于了MSS,或者,receiver buffer有一半为空,就可以把window打开让send 发送数据过来。
- 如果这个问题是由Sender端引起的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的ack回包,他才会发数据,否则就是在攒数据。
另外,Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法(关闭Nagle算法没有全局参数,需要根据每个应用自己的特点来关闭)
另外,网上有些文章说TCP_CORK的socket option是也关闭Nagle算法,这不对。TCP_CORK其实是更新激进的Nagle算汉,完全禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送。最好不要两个选项都设置。
https://coolshell.cn/articles/11609.html
http://zdyi.com/books/unp/s1/1.3.5.html