上篇文章:
目录
1 TCP协议特点与格式
TCP协议的特点是有连接、可靠传输(核心)、面向字节流、有接收缓冲区和发送缓冲区、全双工。
注意:面向字节流和面向数据报是从应用层角度来谈,而无论UDP还是TCP发送的数据都是一份一份的报文,只是接收的方式不同。UDP一次必须从接受缓冲区接收一份完整的数据报,而TCP采用字节数组,接收的数据长度以字节数组长度为准,因此才讲TCP是面向字节流的。
TCP协议格式如下:
32位序号:TCP协议会将数据按报文段段进行编号,编号内容是本报文段第一个字节的序号。
32位确认序号:希望接收的下一个报文段的第一个字节的编号。
4位首部长度:单位4字节,4位比特位最大表示1111=15,即TCP首部长度最大60字节(15*4=60),除了20字节固定首部长度,余下40字节是选项的最大长度。
保留6位:预留空间,目前无用处,全为0 。
6个标志位(每个标志1比特,0表示不是该类型,1表示是该类型):URG紧急指针是否有效、ACK确认号是否有效、PSH提示接收端应用程序立刻从TCP缓冲区把数据读走、RST对方要求重新建立连接(携带RST标识的称为复位报文段)、SYN请求建立连接(携带SYN标识的称为同步报文段)、FIN通知对方本端要关闭(携带FIN标识的为结束报文段)。
16位窗口大小:表示发送该TCP报文的接收窗口还可以接收多少字节的数据量,用于TCP的流量控制(在ACK报文生效,16位不一定只能表示64kb,实际上选项中有窗口扩大因子,实际表示的窗口大小是64kb*窗口扩大因子)。
16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的校验和不光包含TCP首部,也包含TCP数据部分。
16位紧急指针:标识哪部分数据是紧急数据,即使接收方窗口大小为0,也可以发送紧急数据,因为紧急数据无须缓存。
2 确认应答机制(保证可靠传输)
可靠传输并不是指安全传输(这需要加密机制的保障),而是指数据发送方可以知道传输的数据是否被接收方收到。
因此TCP为了确保可靠传输,为数据进行编号,即32位序号seq,而接收方如果收到TCP报文段,就会发送确认报文ack,确认号ack是希望下一次收到的编号。
如上图,假设报文段长度1000,第一个报文段的序号seq=1,则该报文段的最后一个字节的序号是1000,因此ack是下一次希望收到的序号1001(1000+1)。而第二个报文段的第一个字节的序号seq=1001,因此如果接收方顺利收到,就应该发送ack=2001。
通过一问一答的方式,发送方如果收到接收方的确认报文,就知道数据已经顺利被收到了。
3 超时重传机制(保证可靠传输)
因为网络环境的复杂,所有类型的报文都可能会丢失,因此如果确认应答机制出现差错,比如发送的TCP报文段丢失,那么接收端就不会收到报文段也就无法发送ack,于是超时重传机制便可以保证确认应答机制的可靠。
当TCP报文段丢失,发送端隔上特定时间段没有收到ack,就会进行超时重传。
当ack报文丢失时,发送端也会进行超时重传,但是此时接收端就会收到重复的数据,由于数据都进行了编号,因此接收端就会丢弃先前重复的数据。
注意:超时重传的时间段如何确定?如果第一次超时重传后,又发生了丢包,一般网络丢包的概率很小,连续多次丢包的概率更小,概率更小的时间都能发生,说明此时网络已经非常拥堵了,频繁的超时重传无意义,于是第二次超时重传的间隔时间段一定要比第一次更长。Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传。如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
4 连接管理机制(保证可靠传输)
TCP协议是通过“三报文握手四报文挥手”来管理连接的。三报文握手是建立连接的过程,主动发起建立连接请求的是客户端,被动建立连接的是服务端,服务端持续运行处于监听建立连接申请的状态。
1.建立连接:客户端发送SYN报文,随之进入SYN_SENT状态;服务端接收到SYN后,发送ACK;同时服务端发送SYN,进入SYN_RCVD状态;客户端接收到SYN后,发送ACK,并进入ESTABLISHED状态;服务端接收到ACK后也进入ESTABLISHED状态。至此连接建立完成。
由于封装分用操作开销大,因此可以把服务端发送ACK和发送SYN合并成一个TCP报文段,因此建立连接的过程有3个报文段的交互,故称为三次握手建立连接。
注意:为什么不能是两次握手和四次握手?1.可以是四次握手,但是没必要,见上述合并报文的含义。2.不可以是两次握手,防止已经失效的SYN报文到达服务端,从而维护已经失效的连接(当客户端发送的SYN因为网络问题滞留在某个节点,而客户端触发超时重传,又发送SYN报文建立了新的连接后,滞留失效的SYN到达服务端导致服务端无法识别是否要继续建立连接),实际上第一次第二次握手验证了客户端的数据发送能力和接收能力没有问题,第二次第三次握手验证了服务端数据发送能力和接收能力没有问题。
2.数据交互:数据交互会重复多次请求和响应。而数据交互期间起始数据的序号不一定是从1开始,而是双方进行约定,因此三次握手还有一个重要的作用就是协商重要参数配置。
3.断开连接:客户端希望断开连接,发送FIN报文,进入FIN_WAIT_1状态;服务端接收到FIN报文,发送ACK报文,进入CLOSE_WAIT状态;客户端接收ACK报文,进入FIN_WAIT_2状态;经过一段时间后,服务端发送FIN报文,进入LAST_ACK状态;客户端接收到FIN报文,发送ACK报文,经过2MSL时间后进入CLOSED状态;服务端接收到ACK报文,进入CLOSED状态。至此断开连接。
注意1:这里必须是四次挥手,第二次和第三次不能合并!接收端接收到报文后发送ACK是内核行为,即立即就发送;而发送FIN是用户代码行为,只有显式调用了close()方法才会发送FIN报文。通常服务端发送ACK后(第二次挥手),此时可能还有数据未处理完(缓冲区还有数据未读取),因此服务端会隔一段时间待数据处理结束后再调用close()进行第三次挥手(在try-catch-finally语句的finally中调用close())。
注意2:为什么客户端必须等2MSL再关闭连接?MSL是报文最大生存时间,即报文从一端到另一端的最长时间,超过这个时间就会被丢弃。1.当客户端发送的ACK丢失时,此时服务端将进行超时重传,2MSL保证客户端一定会等到重传的FIN并进行处理(想象极端情况,ACK在即将到达服务端丢了,再加上超时重传的FIN的最大时间刚好是2MSL),客户端等到重传的FIN会重启计时器,重新计时2MSL。2.2MSL保证了TCP连接中所有的报文都会消失,不会出现下次TCP连接中出现上一层的旧报文段,导致数据错误。
5 滑动窗口机制(保证效率)
采用一问一答的确认应答机制,虽然可靠性得到保证,但是这种方式效率很低,滑动窗口机制就是来提高效率的。
具体来讲,窗口大小是指能一次性不等ACK就发送的数据量的最大值。比如采用窗口大小为4000,一次性就可以发送4个TCP报文段。
当收到窗口内seq=1的ACK(下次传输1001)时,窗口向后移动一个报文段长度,发送下一个报文段即4001-5000,以此类推。但是滑动窗口机制下,如果出现超时重传,如何做?
情况1:ACK丢失,假设ack=1001丢失,那么后续发送端收到ack=2001,也就知道1-1000的报文段已经被接收端收到,因此后续ack也会确认数据的收到,故ACK丢失不影响可靠性。
情况2:TCP报文段丢失,假设seq=1的报文段丢失(1-1000的数据),窗口内的所有数据都会发送,当接收端收到seq=1001-4000的三个报文段时,把这些报文段放在接收端缓冲区,发送ack=1表示希望收到1-1000的数据。发送端连续收到3个重复确认号的报文段,就知道数据1-1000已经丢失了,触发重传(由于这种重传不是计时器超时了,因此不是超时重传,而是快重传,表示快速重发丢失的报文段)。
注意1:快重传时只重传丢失的报文段,其他报文段已经被存储在接收端的缓冲区中,故不需要重新发送。
注意2:当TCP报文段丢失时,如果窗口中的数据全部发送完,再未收到丢失报文段的ACK时,窗口处于冻结状态,即使收到其他数据的ACK(只是确认号都是一样),窗口也不会向后移动。
6 流量控制机制(保证可靠传输)
网络传输的速度不止由发送端的发送速度决定,也取决于接收端的数据处理速度。如果发送端发送过快,接收端数据缓冲区很快就慢了,再来的数据就会被丢失,因此导致丢包(丢包后发送端超时重传,引起一系列的连锁反应)的不可靠现象。
因此,流量控制机制会根据接收端的数据处理能力来动态调整发送端的窗口大小。具体而言,在TCP协议格式中有窗口大小字段,只有ACK报文段才会生效,填写接收端剩余空间的大小。
发送端收到ACK后根据窗口大小字段调整自己的窗口,如果窗口大小是0,此时接收端缓冲区已经满了,因此发送端窗口大小为0不再发送数据,超过超时重传的时间时,发送端发送窗口探测报文段(没有数据载荷),接收端接收窗口探测报文段自动触发ACK发送,一旦接收端缓冲区有空间了会主动发送ACK报文段(窗口更新通知),进而下一步发送数据。
7 拥塞控制机制(保证可靠传输)
除了发送端发送速率和接收端处理速率,网络传输的可靠性还由传输过程的网络节点转发保证。当中间节点比较拥堵时,如果发送端发送大量数据就很容易导致丢包,因此需要衡量中间节点的网络状况就需要进行“实验”:
实验是指发送方窗口大小通过试探的方式得到合适的大小,比如刚开始以小窗口发送数据,此时能顺利完成数据交互,就可以扩大窗口大小。当出现网络拥塞(丢包)时,就减小窗口大小,从而实现根据网络拥塞状况动态调整窗口大小。
具体而言有如下策略:
1.慢开始:初始选择小窗口,以指数速率增加窗口大小(慢开始的含义仅指窗口初始值很小,不代表窗口增加的速率)。
2.拥塞避免:为了不让窗口值扩大速率很快,设置阈值ssthresh,一旦窗口超过ssthresh值,窗口大小增加速率变成线性增加(加法增加,依次增加1个单位)。
3.乘法减小:当出现网络拥塞时,窗口大小恢复初始值,重新慢开始。同时ssthresh值减少为原窗口值的一半(出现网络拥塞,说明短期内不宜发送过多的数据,因此控制窗口增加速率不要那么快)。
注意:这里的窗口和流量控制的窗口取哪个?拥塞控制机制的窗口称为拥塞窗口cwnd,流量控制的窗口称为流量窗口。发送方选择的窗口大小=min(拥塞窗口,流量窗口)。拥塞窗口大于流量窗口,说明此时接收端数据处理压力大,因此应该减少数据的发送,故选择窗口为流量窗口;流量窗口大于拥塞窗口,说明此时网络状况较差,发送数据可能会产生丢包,因此选择拥塞窗口。
8 延迟应答机制(保证效率)
接收端实际上可能处理缓冲区的速度比较快,因此如果接收端在收到报文段后立即发送ACK,此时窗口大小可能比较小。而延迟发送ACK,由于接收端处理了部分数据,此时窗口大小就可以增加。增加窗口大小意味着网络传输的吞吐量增大,于是效率就得到提高(保证流量控制机制不会抑制传输速率太狠)。
对于延迟应答,不是所有的报文段都可以延迟发送ACK,否则可能导致发送端的超时重传。如果超过最大时间限制就应该及时应答,如果连续收到N个报文段,也应该及时应答(此时应答意味着同时确认了多个报文段)。
9 捎带应答机制(保证效率)
在延迟应答机制的基础上,如果延迟的时间刚好接收端也有数据需要发送(实际上客户端和服务器分别发送请求和响应都包含数据载荷),那么发送的包含数据的普通报文段也可以捎带ACK一起发送。
注意:在这个基础上,四报文挥手断开连接在特殊情况下,四报文挥手可能变成三报文,如果第二次挥手(服务端发送ACK)延迟应答前,而第三次挥手(服务器端发送FIN)立即发送,此时第二次和第三次挥手就会合并为FIN+ACK,但是我们仍然成为四报文挥手。
10 粘包问题
粘包问题粘的是应用层的数据包,由于TCP协议只有首部长度,没有数据长度字段,而TCP协议面向字节流,接收端缓冲区中如果存在多个报文段,在应用层看来是一大串字节,不知道报文段的起始和结束,看起来好像所有的数据粘在一起了,这就是粘包问题。
解决方案就是明确数据的边界:1.约定数据长度2.使用分隔符。在应用层使用TCP的Socket编码时,使用println()就是使用分隔符传输数据的方式,否则就会导致next()读取数据区分不了边界从而阻塞等待。
11 TCP连接异常情况
1.主机关机:杀死进程,释放进程PCB,释放文件描述符(相当于调用close()),因此会正常触发FIN报文段的发送,即四次挥手流程。当四次挥手还未执行结束就关机了,此时服务端会超时重传若干次FIN,都没有反应就自动关闭连接。
2.程序崩溃:这种情况也是进程终止,即同主机关机(连接管理由内核负责,即使没有进程也可以进行)。
3.机器掉电:如果接收方掉电,发送方尝试发送数据,发现没有ACK,连续多次超时重传也没有响应,就尝试重新建立连接,重新建立连接不成功就会主动放弃。如果发送方掉点,接收方长时间没有接收到数据,就会定期给发送方发送“心跳包”(发送方发送ping报文,接收方回复pong报文,具有周期性和检测对方是否存活的作用)。
4.网线断开:同机器掉电。
12 常见问题
1.UDP本身是无连接,不可靠,面向数据报的协议,如果要基于传输层UDP协议,来实现一个可靠传输,应该如何设计?
2. UDP大小是受限的,如果要基于传输层UDP协议,传输超过64K的数据,应该如何设计?
问题1和问题2答案类似,都是采用TCP的思路来解决UDP的问题,比如设计类似TCP可靠传输机制的UDP协议,引入序列号、确认应答、超时重传等等。
3.什么时候使用TCP,什么时候使用UDP?
TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
UDP用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等。另外UDP可以用于广播。
下篇文章: