文章目录
- 1、滑动窗口
- 2、拥塞控制
- 3、延迟应答
- 4、捎带应答
- 5、总结TCP可靠性和性能提高
- 6、面向字节流
- 7、粘包问题
- 8、异常情况
- 9、全连接、半连接
上一篇是传输层TCP协议(1)。本篇默认读者已经清楚TCP报头各个部分、可靠性和握手挥手的含义。
有时候会把客户端写作C,服务端写作S。
1、滑动窗口
C和S之间的交互有串行的方案,一收一发;多发多收,时间会有重叠,效率更高,这是TCP通信的主流方案。对于TCP,可靠性确实重要,但效率也是其考虑的重点。发不能超过收的上限,在能收的前提下再发。一次最大发送量取决于对方窗口大小。发送方和接收方可以互换角色。
窗口大小在发送方的发送缓冲区内,滑动窗口是发送缓冲区的一部分,这部分可以不用收到应答,直接发送,当然也得对方能收得了。这样可以把发送缓冲区分成三部分,左边部分是已经确认发送了的,这部分可以被覆盖;中间是滑动窗口部分,可以直接发送但尚未收到应答;右边部分是尚未发送的数据。中间部分一旦有数据收到应答,窗口就滑动,向右滑动。滑动窗口有多大取决于对方发送的应答报文中的窗口大小。当应用层有数据写入时,比如调用write函数,就会把数据放到右边的部分。
假设把发送缓冲区当作一个char数组,因为char能访问1字节的数据,数组分出多个块。发送数据,缓冲区接收,这也就是流的概念。每个进来的数据都有下标作为序号。滑动窗口就是两个下标之间的区间,假设下标是start和end,当创窗口开始移动时,就是前后下标+±-。
滑动窗口不能向左滑动,因为左边是已经发送且确认的,右边是没发送的。滑动窗口取决于对方的接收能力,所以可以变大变小,变大就是end += …,变小就是start += …,变0就是对方接收缓冲区满了,两个指针指向同一个位置。
发送要按序,应答也会按序。应答报文中有两个重要字段,序号和窗口大小。序号表示这个序号之前的报文都收到了,所以start = 序号,应答报文的窗口大小win,则对应着end = start + win。应答报文不断发送,start就不断更新,以win变化或者不变化,end都向右移动,这样窗口就滑动了。
假设发送了100,200,300,400报文,如果第一个丢失,怎么办?丢失有两种情况,一个是发送出去了,而对方的ACK,也就是应答丢失了,这个没有关系,因为后续收到201报文,其实就知道100报文丢了,但能根据201报文调整窗口;另一个情况是还没发送出去,数据丢了,而不是应答丢失,比如中间的200报文没发送过去,100,300,400都收到了,这时候服务器即使收到了后面几个报文,返回的几个应答中的序号也都会写101,客户端得到重复的101后,TCP就知道丢包了,此时客户端就等待超时重传,重新发200报文,这个机制叫做高速重发或者快重传机制。而为了超时重传,为了数据没丢,服务器确认收到之前,报文都必须先保存,这里就对应上窗口根本没动,数据保存在滑动窗口,加上之前收到300,400报文,200重发后,服务器就会发送序号为401的应答,客户端就从400报文往后继续发。
无论窗口内哪个位置的报文丢包了,都不需要担心,办法就是上段所说。
窗口一直滑动,也不会越界,因为发送缓冲区可以设计成环状结构,就可以做到循环使用。这也就是为什么数据可以被覆盖。所以这里就用到了环形队列。
快重传和超时重传不冲突,共存,快重传决定重传的上限,超时重传决定重传下限。
2、拥塞控制
客户端C和服务端S之间的交互,C会发送数据到网络中,S会从网络中拿到数据,C和S不仅有各种机制,网络也有机制。网络相比于C和S复杂得多。网络里有大量发送方和接收方,网络中所有的主机或设备都在遵守TCP/IP协议。
出现错误,出现的次数多和出现的次数少的原因是不同的,比如丢包,丢的少可能是发送方问题,丢的多可能就是网络问题。当出现大量丢包时,这个问题就是网络拥塞,此时重传会加重网络负担,因为网络内所有主机都会被影响,主机都会判定网络出现拥塞,然后在短时间内大量重发数据,就会加重网络负担,加重拥塞问题。所以重传的机制都不能采用。
拥塞的原因有很多,TCP只是网络策略,没办法彻底解决网络的大部分问题。网络拥塞的解决也不能让所有的主机都等待处理,这样效率有点低。在网络拥塞有起色的情况下,要尽快恢复网络通信。
虽然有流量控制,可以尽量减少网络拥塞,但总归还是有,比如最一开始发送数据的时候。当出现网络拥塞后,拥塞控制就开始生效。TCP会先使用慢启动机制,属于拥塞控制机制,发送少量的数据探路,摸清当前拥塞状况,再决定以多少速度来传输数据。比如发一个报文,有应答就发送2个报文,有应答就发送4个报文,以此类推,发送到那个报文丢包了,就知道当前拥塞程度;如果1个报文就丢了,那就说明拥塞很严重。TCP丢包后会超时重传,还是发送丢包前发送的微量报文,等待时间就是为了等网络拥塞好转。
为了衡量网络健康状态,即拥塞状况,用拥塞窗口来当作指标。网络的状态是变化的,那么拥塞窗口大小一定也是变化的。主机要了解网络健康状态只能尝试、探测,也就是得到当前网络的拥塞窗口。发送开始的时候,定义拥塞窗口大小为1,每次收到一个ACK应答,拥塞窗口大小就加1。上面所写的滑动窗口,不仅要对端主机的接收能力,还要看网络的拥塞窗口,所以实际上滑动窗口的上限等于这两个的较小值,用min函数,对应到start和end变量,start还是序号,end则是序号 + 窗口大小,和拥塞窗口的较小值。
拥塞窗口的增长速度是指数级别的,也就是上面所说的发1个,2个,4个,8个。指数增长前期慢,后期飞速增长,这就对应着,试探网络情况时要发少量的报文,后面网络好转了就要尽快恢复正常发送。但指数增长不能一直增长下去,这里就用一个叫做慢启动,也叫ssthreah的阈值,超过阈值就变成线性增长。指数增长到线性增长的整个过程,一直在探测拥塞窗口。当增长超过一次拥塞窗口时,就直接从1开始,重新指数增长,而这一次的阈值变为上一次拥塞窗口的一半,之后不断进行同样的做法。这样的循环会一直循环下去。
3、延迟应答
为了让发送和接收的效率提高,接收方可以给发送方更大的窗口大小。虽然滑动窗口大小还有拥塞窗口在制衡,但如果不给更大的,发送方就不可能在同一时间发送更多的数据,给更大的窗口大小才有这个概率。接收方要给,就得自身接收缓冲区剩余空间更大才行,接收缓冲区的清理取决于上层应用层,要想让应用层更快地读取数据,可以不需要立马应答,发送方还有超时重传机制,在未超时前,接收方等待一下,给应用层更多时间,让应用层多读取一些数据再给应答,这就是延迟应答。
延迟应答可以利用时间等待来应答,也可以每隔几个包再应答一次。一般每隔2个包,或者等待不超过200ms。
4、捎带应答
这个在之前的博客中写过,也就是一次性多发几个报文,不占用效率反而提高效率。有些报文不需要单独发一次,所以就在发送一个报文时,顺便带上这些报文。
5、总结TCP可靠性和性能提高
可靠性:
检验和(校验和),序列号,确认应答,超时重发,连接管理,流量控制,拥塞控制
提高性能:
滑动窗口,快速重传,延迟应答,捎带应答
TCP还有定时器,比如超时重传定时器,保活定时器,TIME_WAIT定时器等。
6、面向字节流
TCP有发送缓冲区和接收缓冲区,应用层发给传输层的数据,是以字节为单位发送的,数据可以拆分成几个字节为一组。数据统统发给TCP后,TCP存在发送缓冲区右边部分,通过滑动窗口,一块块地发送到对端主机的接收缓冲区内。这也就是面向字节流,发送接收都已字节为单位。无论上层怎样读取发送,TCP都以字节为单位来相互拷贝。
下层如何发送接收,失败成功,上层并不关心,每层做每层的事,每层只关心这一层,这也就是流。
7、粘包问题
TCP是被上层使用套接字进行访问的,TCP使用的数据都是上层发送过来的,不过因为面向字节流,TCP并不知道这些数据,而上层知道,那么TCP如何保证读到的是完整报文?读一个完整的或者读多个完整的,但每一个都分开,这种读取完整报文就是粘包问题,也叫做粘包问题。在之前的代码实现中,可以循环读取,每次读取都做解析,分成各个成员。
计算机解决粘包问题有很多办法,比如定长包,分割符,报头加相关信息等,不过本质上这个问题的解决就是明确两个包之间的边界。UDP没有这个问题,因为UDP报文的长度都能够在报头中知道,且因为UDP报文需要处理。TCP中没有有效载荷长度,两个TCP报文之间的分离,是由应用层自主完成的,TCP报文不做处理,就是存储在缓冲区内。
8、异常情况
一个进程运行时申请了很多资源,在还没处理好资源,没有关闭像malloc,fd这样的时候,进程终止,这时候内存泄漏不存在,因为进程挂了,就由bash管理所有资源,页表,地址空间等等,也就是被回收了。网络这里也是这样,建立的连接都是OS做的,无论是否异常断开,OS都会进行挥手再断开,由OS决定。异常断开,比如拔网线,拔了之后,服务器不知道客户端断了连接,以为还有连接,服务器也有保活策略,隔一段时间发去报文询问,如果没有应答服务器就断掉连接。如果刚拔又插上,可能服务器只发过一次保活信息,客户端因为断开过连接,但是它自己又不知道,接到保活信息才知道自己以前的某个连接异常,就会发送RST报文让服务端重置连接。
TCP的连接长短是由应用层做的。活跃连接是经常交互的连接,这个好管理;如果建立一个长连接,但不怎么交互,但却要一直挂着连接,不能断,这样的连接维护起来成本比较高点,如果这时候TCP插手关掉连接,那么对应用层就有不好的影响,但长时间挂着TCP又得不断询问。所以实际上,连接的维护是由应用层来做的,保活一般以小时为单位。
9、全连接、半连接
三次握手,客户端第一次发送过去SYN,服务器发送SYN + ACK应答后服务器处于SYN_RECV状态,这是半连接状态。TCP协议需要在底层维护全连接队列,队列长度就是listen接口第二个参数 + 1。假设第二个参数为1,此时只能由两个连接成功,成为全连接状态,其它再发送请求时,服务器只能处于SYN_RECV状态,这些半连接状态的连接就会被维护在半连接队列中,维护的时间很短,这个时间就是在等待上层把全连接的连接取走。全连接是指已经连接成功,但还没被应用层读取的连接。半连接队列不能太长,效率低了且不如增大-长全连接队列或者增加服务器吞吐量,会过多消耗OS本身的资源,客户端也不愿意等。
结束。