
上一篇我们介绍了TCP的报头,但是很多可靠性的策略是在报头里体现不出来的!!比如说重传、流量控制......
一、超时重传机制
情况1:主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;

情况2:主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;

所以主机A发出去的时候,在没收到应答期间,并不知道这个数据究竟是丢了没被首收到,还是收到了应答丢了! 但是我总不能一直等着吧! 所以我们规定了在没收到回应期间,我们就约定了一个特定的时间间隔,如果没有应答的话,那么我的主机A就判定这个数据已经丢失了,然后就会进行重发!!
问题1:这个重传时间应该设置为多少比较好呢??
------>**最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".但是这个时间的长短, 随着网络环境的不同, 是有差异的.。**当网络比较好的时候,间隔时间不能太长,因为此时信息发得很快,如果出现少量丢包的话等待会影响整体的重传效率。而当网络比较差的时候,也不能设置得太短,这样光传就要花很多时间了,非常有可能出现大量超时补发的情况!!因此我们的重传时间应该是动态的,和网络状况强相关的!!当然平时我们用户并不关心这些,具体细节是由内核来做的!!
问题2:如果是因为情况2(应答丢了),主机B收到很多重复数据怎么办???
------>所以这要求TCP协议需要能够识别出哪些包时重复的,然后把重复的包给丢掉。就可以很容易做到去重的效果. 这时候我们可以利用前面提到的序列号来判断!!
问题3:TCP需要保证任何环境下较高性能的通信,他是如何动态计算这个最大超时时间的??如果一直重传都没有得到应答该怎么办??
------>
(1)Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍.
(2)如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
(3)如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
(4)累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
二、链接管理机制
2.1 TCP三次握手和四次挥手
TCP通信时基于链接的, 所以要经过三次握手建立连接, 四次挥手断开连接

服务端状态转化:
CLOSED -\> LISTEN\] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
\[LISTEN -\> SYN_RCVD\] 一旦监听到发送SYN确认报文.
\[SYN_RCVD -\> ESTABLISHED\] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行 读写数据了.
\[ESTABLISHED -\> CLOSE_WAIT\] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器 返回确认报文段并进入CLOSE_WAIT;
\[CLOSE_WAIT -\> LAST_ACK\] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当 服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
\[LAST_ACK -\> CLOSED\] 服务器收到了对FIN的ACK, 彻底关闭连接.
**客户端状态转化:**
\[CLOSED -\> SYN_SENT\] 客户端调用connect, 发送同步报文段;
\[SYN_SENT -\> ESTABLISHED\] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
\[ESTABLISHED -\> FIN_WAIT_1\] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入
FIN_WAIT_1;
\[FIN_WAIT_1 -\> FIN_WAIT_2\] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服 务器的结束报文段;
\[FIN_WAIT_2 -\> TIME_WAIT\] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
\[TIME_WAIT -\> CLOSED\] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会 进入CLOSED状态.
**下图是TCP状态转换的一个汇总:**

较粗的虚线表示服务端的状态变化情况; ;
较粗的实线表示客户端的状态变化情况
CLOSED是一个假想的起始点, 不是真实状态;
关于 "半关闭" , 男女朋友分手例子
关于CLOSING状态. 可以网上查一下.
问题1:关于connect和accept的本质
------\>**其实connect只是发动了三次握手(注意握手的时候发的都是裸报头,没有数据,只不过标记位有区别),但其实三次握手的细节是由双方OS自动完成的,此时他只不过是在哪里阻塞等待结果!!**
**而链接成功和accpet也没有关系,他只是把建立好的连接直接拿过去使用(此时双方的一些准备工作都已经做好了) 然后就可以直接进行通信了!**!
问题2:为什么是三次握手和四次挥手??
------\>**其实按道理来说应该是四次握手,三次是因为我们的服务端在发动链接请求的时候扫带应答了,所以中间被压缩成一层了(这是必然的,因为服务器一般都是会统一的,所以本质上是在技术上压缩成了三次)**
**而之所以是四次挥手,是因为断开连接双方都需要协商(因为双方都有可能给对方发数据),当我确保没有数据给你发的时候,我告诉你我要断开连接了,然后你也得确认你没有数据给我发了,然后你也要向我申请断开连接(必须可靠地得知对方不想发消息的意愿)但其实如果我们客户端断开的时候恰好服务度也想断开,那么也有可能中间两层会被压缩成一层(少数情况,得有巧合性 因为客户端不想发的时候服务端可能还想发,全双工的特性所以客户端的收端并不会关闭)**
**所以他们的本质其实就是基于双方一来一回的一种可靠性,双方都至少可靠地给对方发了一次消息,并收到了一次应答,那么双方就是可靠的(****验证了全双工通路正常****)**
问题3:既然服务器基本上都会统一客户端的连接,那为什么一次握手不行呢??
------\>**因为如果客户端一次握手默认服务器必须同意,但由于服务器管理链接是需要消耗资源的,所以如果同时大量的客户端都来建立连接请求,那么其实服务器是需要基于自身内存去考虑的,否则很容易出现服务器内存被打满的情况**!
问题4:为什么不是两次握手呢??
------\>**(1)情况1:如果是两次握手,那么必然要求服务端必须要先于客户端一步建立连接,可是这个要求的前提是客户端必须是好的,万一客户端突然崩掉了,对ACK不做处理,那么会使得服务端的连接会被长时间的维持。因为服务器是一对多的情况,所以如果一定要出现这种情况的话,应该让客户端去做资源的让步,而不是应该所有的异常情况负担都让服务端来兜底,这样他的压力会非常大!!**
**(2)情况2:其实我们会发现,我们三次握手最怕的就是丢的就是第三个,因为第三个是没有回应的,那么如果是两次握手的话,我们的第二个一旦丢了(此时服务端已经默认建立好链接了),那么客户端没收到ACK会以为自己链接失败了,然后会再重新发送请求,又要建立新链接,这样又会把异常的负担全部都压在服务器上!!**
**所以奇数次握手可以确保一般情况握手失败的话链接的成本是在client身上的!**!
问题5:为什么三次握手是最好的呢??
------\>**因为三次握手如果是第三次丢了,大不了就重传一次,此时建立连接失败的代价是在客户端身上的(谁发送的请求让谁来承担失败后果),这样可以保证了服务端的稳定性,所以我们三次握手是性价比最高的!**
问题6:总结建立连接时做的事情
------\>
**(1)可靠地验证了全双工**
**(2)奇数次握手确保了握手失败的成本是在client端**
**(3)双方可以在报文里通过窗口大小告知对方自己的接收能力,为后面传输数据做准备(和滑动窗口有关)**
**(4)双方通过报文里的序号协商起始序号(一般以较小的为主,这是为了避免不同报文的序号冲突)**
问题7:三次握手就绝对可靠了吗???
------\>**虽然三次握手成本是最低的,并且把握手失败嫁接到了客户端,但是你要知道如果连接成功了那么双方是会消耗同样的资源的,所以如果同一时间有多个客户端恶意发起了链接,那么也是会造成服务端崩溃的!!**
情况1:被动成为肉鸡 比如说几台电脑中了病毒,然后在他们后台开启了一个不知名的程序,这个程序会在特定的时间点接收黑客电脑发送过来的任务,然后要求在同一时间一起向某个服务器发起请求,此时TCP解决不了这个问题!!所以大公司可能会需要有一些限流的策略或者是防火墙的策略,确保服务器不会因为攻击而挂掉,所以虽然三次握手已经很好的,但是这种情况也是难以避免的!!
情况2:自愿形成肉鸡 由一群人自愿形成一个比较大的社团,然后在同一时间用一个目标网址不断发送情况如何不断刷新(攻击外网资源),消耗对方的服务器资源,造成服务器宕机、崩溃!
### 2.2 测试三次握手
验证三次握手的状态,我们的tcp代码只需要保留链接的代码就可以了 IO部分可以不需要

我们把accept也去掉,证明三次握手和他没有关系 我们先把listen的第二个参数设为1
用netstat -ntp可以看到他的状态
 如果我们建立了两个链接,客户端机器随机分配的端口号会被区分出来!
 当我们建立第三个链接的时候,我们会发现客户端认为第三次连接成功了,但是服务端认为第三次连接没有建立成功,并且往后建立的链接都会失败

问题1:为什么会这样呢??
------\>**客户端状态正常, 但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态 这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:**
**1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)**
**2. 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求) 而全连接队列的长度会受到 listen 第二个参数的影响.**
**全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.**
**这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.**

问题2:既然队列都满了,那为啥客户端是连接成功的状态??
------\>**因为TCP设计的时候,一般都会让服务端同意客户端的请求,也就是说我允许你前两次握手成功,但是你第三次ACK回来给我的时候我会去检测我的队列是否已经满了,如果已经满了我就会把你的ACK给丢掉!**!
问题3:关于SYN_RCVD
------\>**他表现的是一种半连接状态(全连接的前置状态),当然这是暂时的,半连接并不会被长期维护(他的队列长度跟listen的第二个参数也有关系,但是是有相关算法的,是内核区决定的),所以时间太长他也会被丢弃(就是如果因为全连接队列满了导致ACK一直被丢弃)**
**所以无论是因为我们发的SYN+ACK对方没收到,还是因为他收到了但是ACK被我们丢弃,我们服务端都会评判这次链接是失败的!!所以不会放到长连接队列里供accept使用**!
问题4:关于SYN洪水
------\>**因为进入长连接必须要先经历端链接,所以如果同一时间大量SYN涌入就会导致半连接被快速打满(因为全连接的数量有限),所以也会大量消耗服务端的资源!**!
问题5:你的网络为何时好时坏呢??
------\>**因为在网络连接拥堵的时候,有时候可能你的连接恰好从半连接拿到了长连接队列里,但是也有可能因为长时间无法进入长连接队列而被服务端给丢弃!**!
问题6:全连接队列为什么不能太长呢??
------\>**因为没有必要,如果我的上层很忙,需要很多资源,我明明没有很多时间来处理这些连接,那么你的这些大量链接还需要被长时间维护起来,此时因为太忙,所以这些资源其实是没有价值的!!还可能影响上层的处理速度!**!
问题7:那为什么不能没有全连接队列呢??
------\>讲个故事,比如说海底捞因为客人太多,所以他会在门口摆一些桌子还有一些小零食,告诉那些客人我们这里暂时没有位置了,可不可以等一等,此时用户可以选择等,那么如果正好有一桌此时吃完了就离开,那么就可以立马有人可以补进来,不至于餐桌闲置(因为如果我满了的时候就告诉后面的客人我们满了,那么一旦有人吃完离开了就可能没有人立马进来补位,而桌子被闲置就会导致我们的钱赚的少了!!)
所以**如果我们没有全连接队列,你来我就处理你不来我就不处理,那万一我突然从很忙变得不忙的时候,我有能力处理很多链接凑巧又没有什么链接,那么就会造成资源的闲置,我无法将服务器的资源进行充分利用!所以我们有了listen提供的这个全连接队列,就是为了能够在服务端不忙的时候可以快速提供连接,让服务端去做新的服务!**!
### 2.3 TIME_WAIT
**要验证断开连接的状态,代码里就不要有close 然后要把accept解开(因为如果你上层没有用accpet把链接拿过去用,那么也就不存在挥手的过程)**
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是:

这是因为,虽然**server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口**.
我们用netstat命令查看一下:

此时我们会发现一个问题就是:主动断开连接的一方,在四次挥手完成之后会进入time_wait的状态,等待若干时长之后再自己自动释放
问题1:为什么会这样呢??
------\>**TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)****的时间后才能回到CLOSED状态.**
**我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口; MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;**
**可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;**

**规定TIME_WAIT的时间请参考UNP 2.7节;**
问题2:TIME_WAIT导致TCP没有完成断开是server无法立刻监听合理吗??
------\>其实很多情况下并不合理
**服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求 一般是HTTP1.0版本的短连接).**
**这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.**
**由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,端口, 协议). (TCP端口数量上限是65535)而且还未被系统回收,就会出现无法向服务端创建新的socket连接的情况,此时系统几乎停转,任何链接都不能建立:`address already in use : connect`异常**
问题2:怎么解决server因为断开后的TIME_WAIT状态而不能再次监听端口的问题呢??
------\>**使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符**(就是如果你之前这个端口号因为TIME_WAIT不让我用了,那么现在请立刻让我使用!!)

问题3:为什么断开的一般都是客户端,但是客户端一般不会出现这样的问题呢?
------\>**因为客户端使用的是随机端口,不受这个特点的影响,而服务器是必须绑定的同一个端口!**
问题4:为什么要等??为什么是TIME_WAIT的时间是2MSL?
------\>**MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(消失其实就是为了丢掉他),否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的。**
**同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);最大程度确保四次握手能够完成。**
问题5:如果历史数据消散得不彻底怎么办??
------\>**其实因为我们在握手的时候会去协商起始序号,加上一个随机值,一方面是为了预防黑客去才我们的序号发送恶意报文,另一方面也是为了最大限度规避历史报文对我们的通信造成的影响!**
问题6:可以我们一般发送消息不也是ms级的吗,为什么MSL会这么长呢??
------\>**因为ms级是正常报文,此时我们考虑的是最大传送时长,但是在网络异常的状态下可能出现拥堵,所以MSL表明的是报文的最大存活时长!!具体是多少不同OS实现不同(一般是60-120) 当然如果我们去配置文件修改的话也可以改!**!
### 2.4 CLOSE_WAIT
```cpp
#pragma once
#include