发送方在发送一次数据后就开启一个定时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,在达到一定次数还没有成功的话就放弃重传并发送一个复位信号。其中超时时间的计算是超时的核心,而定时时间的确定往往需要进行适当的权衡,因为当定时时间过长会造成网络利用率不高,定时时间过短会造成多次重传,使得网络阻塞。在TCP连接的过程中,会参考当前的网络状况从而找到一个合适的超时时间。
Client 最大 TCP 连接数
client 在每次发起 TCP 连接请求时,如果自己并不指定端口的话,系统会随机选择一个本地端口(local port),该端口是独占的,不能和其他 TCP 连接共享。TCP 端口的数据类型是 unsigned short,因此本地端口个数最大只有 65536,除了端口 0不能使用外,其他端口在空闲时都可以正常使用,这样可用端口最多有 65535 个。
Server最大 TCP 连接数
server 通常固定在某个本地端口上监听,等待 client 的连接请求。不考虑地址重用(Unix 的 SO_REUSEADDR 选项)的情况下,即使 server 端有多个 IP,本地监听端口也是独占的,因此 server 端 TCP 连接 4 元组中只有客户端的 IP 地址和端口号是可变的,因此最大 TCP 连接为客户端 IP 数 × 客户端 port 数,对 IPV4,在不考虑 IP 地址分类的情况下,最大 TCP 连接数约为 2 的 32 次方(IP 数)× 2 的 16 次方(port 数),也就是 server 端单机最大 TCP 连接数约为 2 的 48 次方。
流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。如果接收方来不及接收发送方发送的数据,那么就会有分组丢失。在TCP中利用可变长的滑动窗口机制可以很方便低在TCP连接上实现对发送方的流量控制。 实现方式是接收端返回的ACK中会包含自己的接收端的滑动窗口大小,以控制发送方此次发送的数据量大小。
如果接收方的滑动窗口满了,基于TCP流量控制的滑动窗口协议,接收方返回给发送方的接收窗口大小为 0,此时发送方会等待接收方发送的窗口大小直到变为非 0 为止,然而,接收方回应的 ACK 包是存在丢失的可能的,为了防止双方一直等待而出现死锁情况,此时就需要坚持计时器来辅助发送方周期性地向接收方查询,以便发现窗口是否变大。
拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况。常用的解决方法有:慢开始和拥塞避免、快重传和快恢复。
但是,为了防止拥塞窗口增长过大而引起网络拥塞,另外设置了一个慢开始门限 ssthresh。
① 当 cwnd < ssthresh 时,使用上述的慢开始算法;
② 当 cwnd > ssthresh 时,停止使用慢开始,转而使用拥塞避免算法;
③ 当 cwnd == ssthresh 时,两者均可。
拥塞控制是为了让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT (往返时间定义为发送方发送数据到收到确认报文所经历的时间)就把发送方的 cwnd 值加 1,通过让 cwnd 线性增长,防止很快就遇到网络拥塞状态。
当网络拥塞发生时,让新的慢开始门限值变为发生拥塞时候的值的一半,并将拥塞窗口置为 1 ,然后再次重复两种算法(慢开始和拥塞避免),这时一瞬间会将网络中的数据量大量降低。
快重传
快重传算法要求接收方每收到一个失序的报文就立即发送重复确认,而不要等到自己发送数据时才捎带进行确认,假定发送方发送了 Msg 1 ~ Msg 4 这 4 个报文,已知接收方收到了 Msg 1,Msg 3 和 Msg 4 报文,此时因为接收到收到了失序的数据包,按照快重传的约定,接收方应立即向发送方发送 Msg 1 的重复确认。 于是在接收方收到 Msg 4 报文的时候,向发送方发送的仍然是 Msg 1 的重复确认。这样,发送方就收到了 3 次 Msg 1 的重复确认,于是立即重传对方未收到的 Msg 报文。由于发送方尽早重传未被确认的报文段,因此,快重传算法可以提高网络的吞吐量。
快恢复
快恢复算法是和快重传算法配合使用的,该算法主要有以下两个要点:
① 当发送方连续收到三个重复确认,执行乘法减小,慢开始门限 ssthresh 值减半;
② 由于发送方可能认为网络现在没有拥塞,因此与慢开始不同,把 cwnd 值设置为 ssthresh 减半之后的值,然后执行拥塞避免算法,线性增大 cwnd。
为什么会出现TCP粘包和拆包?
解决方案:
第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;
第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。
一方面,如果只有第二次握手,服务端发给客服端的包丢了之后,服务端就直接建立了连接,然后一直傻等,从而造成服务器系统调用超时返回。
另一个方面,TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发送方(比如客户端)的初始序号可以得到确认,而另一方的初始序号则得不到确认。
四次挥手可以被抽象为如下过程:
考虑以下服务器遭遇的场景,
先同意对方关闭连接,对方无法传输数据;(第二次挥手)
自己若还有数据未发送完,接着发送直至全部发送完毕;
请求自身关闭连接;(第三次挥手)
客户端同意(第四次挥手)
如果说服务器在客户端请求关闭连接的一瞬间已经没有任何数据需要发送了,那么三次挥手应该也是可以的,但是在实际生产环境中这样的情况几乎没有,所以需要服务器先把自己的所有数据发送完毕,因此四次挥手更加稳妥。
或者也可以这么回答:
释放 TCP 连接时之所以需要四次挥手,是因为 FIN 释放连接报文和 ACK 确认接收报文是分别在两次握手中传输的。 当主动方在数据传送结束后发出连接释放的通知,由于被动方可能还有必要的数据要处理,所以会先返回 ACK 确认收到报文。当被动方也没有数据再发送的时候,则发出连接释放通知,对方确认后才完全关闭TCP连接。
ACK是为了告诉客户端发来的数据接收无误,传回SYN是为了把自己的初始序列号同步到客户端。
CLOSE- WAIT发生在第二次挥手请求发送之后。
在服务器收到了客户端关闭连接的请求并且告诉客户端自己已经成功收到了该请求之后,服务器进入了CLOSE- WAIT状态,然而此时有可能服务端还有一些数据还没有传输完成,CLOSE- WAIT状态就是用来保证服务器在关闭连接之前能够将待发送的所有数据发送完成,因此不能立即关闭连接。
TIME-WAIT发生在第四次挥手的时候,当客户端向服务端发送ACK确认报文后进入该状态。作用有两个
当客户端发出最后的ACK确认报文时,并不能确定服务器能够接收到该段报文。所以客户端在发送完ACK确认报文之后,会设置一个时长为2MSL的计时器。
如果服务器在1MSL后仍然没有收到客户端发送的ACK确认报文,那么它会向客户端重传FIN报文,对客户端而言,从客户端发出ACK报文起,重传的FIN报文的最晚到达时间是2MSL。
若服务器在1MSL内没有收到客户端发出的ACK确认报文,再次向客户端发送FIN释放连接报文。若客户端在2MSL内收到了服务器在此发来的FIN报文,客户端将再次向服务器发出ACK确认报文,并重新开始2MSL的计时。
服务器可以设置 SO_REUSEADDR 套接字选项来通知内核,如果端口被占用,但 TCP 连接位于 TIME_WAIT 状态时可以重用端口。如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时 SO_REUSEADDR 选项就可以避免 TIME-WAIT 状态。
实际上,要建立长连接的话,就需要使用SO_REUSEADDR关键字。
一般来说,系统内不会出现很多CLOSE- WAIT(第二次挥手后服务器出现的状态)。我们可以首先检查一下是不是自己的代码出现了问题,比如查看服务端是否忘记关闭连接。其次我们可以调整系统参数,包括句柄相关参数和TCP/IP的参数,一般一个CLOSE_WAIT会维持至少两个小时。