首先明确TCP的几个特点
- 有始有终
- 有序性
- 可靠性
- 拥塞控制
- 流量控制
有始有终
即TCP的建立连接和关闭连接,也就是所谓的三次握手和四次挥手
三次握手建立连接
- 刚开始客户端和服务端都是关闭状态
- 当要建立连接时,服务端先开启监听状态
- 客户端发送SYN至服务端,同时声明自己的消息序号,进入SYN-SENT状态
- 服务端发送ACK表明自己收到客户端消息,同时发送SYN并声明自己的消息序号,此时服务端状态变为SYN-RCVD,
- 客户端收到了服务端的ACK和SYN回复针对ASCK的ACK,表明自己可以成功收到服务端消息,之后处于ESTABLISHED状态,因为它这边已经有发有收了
- 服务端收到客户端的ACK之后,服务端状态也变为EATABLISHED状态,此时三次握手完成,就可以开始数据传输了
可以看到所谓的三次握手的过程中,不管是服务端还是客户端都经历了一个消息的发送与接受的过程,即一来一回
四次握手断开连接
自古多情伤离别,断开连接相比建立连接要复杂的多,因为要处理各种状态,各种收尾工作。
假设左边是客户端,右边是服务端
- 刚开始都是EATABLISHED状态
- 客户端想断开了,就开始发送FIN给客户端,当然,少不了的也要发送消息序号,接着进入FIN-WAIT-1状态
- 服务端收到FIN消息后回复ACK并记者就进入CLOSED-WAIT状态。我们知道断开连接需要一些收尾工作,所以这里不能像建立连接一样顺带把自己关闭的FIN也发过去而是只发送ACK
- 客户端收到服务端的ACK后进入FIN-WAIT-2状态。
- 此时服务端可能做完收尾工作了,接着发送给客户端FIN,服务端进入LAST-ACK阶段
- 客户端收到服务端的FIN后回复ACK,接着进入TIME-WAIT状态,之所以进入这个状态是防止客户端的ACK没有被服务端接收到
- 服务端收到服务端的ACK后就进入了关闭状态
- 客户端超过TIME-WAIT状态后也进入关闭状态
其实四次挥手实质上还是客户端和服务端的一来一回
发送端数据结构
接收端数据结构
有序性和可靠性
前边建立连接时我们说过了会确定消息序号,有序性正是跟这些序号有关,按照序号一一发送,接收端再对每个包一一应答。但是考虑到网络传输中出现的各种问题就不是这么一句话说的清了。
发送端和接收端都有对应的缓存记录发送和接受的包
先看发送端,
- 1,2,3 发送成功并收到了ACK
- 4,5,6,7,8,9 已经发送但是还没收到ACK
- 10,11,12 可以发送但是还没发送
- 13,14,15不可发送
再看接收端
- 1,2,3,4,5 接受并且已发送ACK确认
- 6,7,10,11,12,13,14 尚未接收到(等待接收)
- 8,9 已经接受到但是还没发送ACK确认
- 15 不能接受
可以看到8,9 已经收到而6,7还没收到,出现了乱序,可能6,7是发生了丢包。此时为了保证一致性,8,9就只能先缓存着不能发送ACK.
进一步的,如果4的ACK到了而5的ACK丢包了,此时的情况变为5的ACK包丢了,6,7的数据包丢了,此时该怎么办?
发送端会采用超时重试的方法。针对每一个包都会有一个定时器,如果定时期限内没有收到ACK就会重试。定时器的时长是TCP采样每次往返时间的,随着网络波动而自适应变化
OK,假设现在已经超时开始重传5,6,7,接收方就发现 5 原来接受过,于是抛弃5.假设接收方收到6,开始发送6的ACK,若是7再次丢包且再次超时,此时的重传策略需要注意
超时间隔加倍,即每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传机制,即当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段
比如当前接收方收到6,8,9,按理说6之后是7,结果收到8,说明7的丢包了,此时就会给发送三个6的ACK。客户端收到3个,就会发现7的确又丢了,不等超时,马上重发。
快速重传避免了超时间隔加倍带来的周期过长的问题。
还有一个累计确认机制,即如果发送端收到了编号为9的ACK,哪怕它还没收到编号为7的ACK,也认为7已经被接收端接收到了,因为接收端是挨个儿发送ACK,能发送9的ACK必然证明7的ACK已经发送,只是可能ACK发生了丢包而已。
流量控制和拥塞控制
滑动窗口rwnd是怕发送方把接收方缓存塞满,而拥塞窗口cwnd是怕把网络塞满。
流量控制:
接收端处理数据包,发送端窗口不断右移,这是一个正常的过程。若是接收端一直耗着不处理数据,发送端窗口就会不断变小甚至变为0。
拥塞控制:
网络杜塞可能会造成包丢失和超时问题。一旦出现了这些现象就说明,发送速度太快了,要慢一点。
一条TCP连接开始,cwnd设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认cwnd加一,两个确认cwnd加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认cwnd加一,四个确认cwnd加四,于是一次能够发送八个。可以看出这是指数性的增长。
增长到一个值sshresh=65535字节时候,每收到一个确认后,cwnd增加1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加1/8,八个确认一共cwnd增加1,于是一次能够发送九个,变成了线性增长。
一直增长下去会出现问题,一旦出现阻塞或者丢包,需要重传。这个时候,将sshresh设为cwnd/2,将cwnd设为1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。
对应前边说过的快速重传算法,当接收端发现丢了一个中间包的时候,发送三次前一个包的ACK,于是发送端就会快速的重传,不必等待超时再重传。cwnd减半为cwnd/2,然后sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。