目录
前言
一、传输控制协议(TCP)
二、TCP报头字段详解
1、16位源端口与16位目的端口
2、4位首部长度
3、32位序号与32位确认序号
(1)确认应答机制
(2)序号引入
4、保留字段
5、六个控制位
6、窗口大小
7、校验和
8、紧急指针
9、选项
三、详解TCP可靠性
1、校验和
2、序号
3、确认应答机制
4、超时重传机制
5、去重机制
6、连接管理机制
(1)三次握手
(2)四次挥手
(3)理解TIME_WAIT状态
7、流量控制机制
(1)PSH控制位
(2)窗口探测与窗口更新通知
8、滑动窗口机制
9、拥塞控制机制
10、延迟应答机制
11、捎带应答机制
四、粘包问题
五、基于TCP常见的应用层协议
本章主要对TCP协议原理进行层层剖析,从理解报文首部字段到TCP是如何提供传输策略进行一 一详解。
首先,我们要清楚的是我们常说的TCP全程为Transmission Control Protocol,中文翻译也就是传输控制协议,该协议为传输层中的协议,所谓传输层主要负责的工作是为报文传输提供策略,也就是我们如何进行传输?什么时候进行传输?一次传输多大?传输时出错了怎么办?这都是我们传输层所要处理的问题。而我们前面所学习的UDP面对上面问题采取的措施是,当上层应用给我们数据时,我们将数据进行封装,也就是加上报头,接着直接交付下层,关于传输过程中若出现错误,我们统一选择抛弃;这也是UDP的不可靠性(中性词);而我们今天所学习的TCP时一种可靠的、有链接的、全双工的、面向字节流的一种协议;
首先,我们粗略的查看一下TCP报文格式,具体如下图所示;
可能第一次看这个报文觉得很复杂,确实如此,相对UDP什么也不管,TCP为了实现可靠性必然需要付出相应的代价;下面小编带着大家一起来理解TCP报文中的这些字段;
这两个字段与UDP中的源端口与目的端口作用相同,为了实现分用功能,用端口来区分应该将数据交付给上层的哪个用户进程;它们各占2个字节,也就是16个比特位;
对于传输层的数据段,我们同样也要考虑两个问题,如何将应用层托付的数据进行封装,托付给下一层(网络层);如何将当前带报头的数据进行解包,交付给上层(应用层);
补充:对于不同层的数据包,我们有不同的称为,在传输层时,我们称其为数据段,在网络层时,我们称其为数据报,在数据链路层时,我们称其为数据帧;
关于数据段的封装与解包,我们通过报文中的4位首部长度来进行的,这个4位首部长度,记录的是TCP报文中首部的长度,也就是最开始那张图中前六行的内容,就是除去数据的部分,其中前5行位固定首部,第六行为选项,可有可无,最多为40字节;
该字段的取值范围是0101~1111,如上图所示,这里的数字是二进制,由于只有四个比特位,因此最高值是1111,也就是15; 这时可能就有小伙伴疑惑了,可是根据上面的图固定首部就有20字节了,而这四个比特位最多只能到15,实际上,这个首部长度是有单位,单位为4字节,也就是4*15=60字节,这就对的上了,其中最小的值为0101,也就是5,5*4=20字节,也就是我们固定首部的大小;
如何封装与解包?
该问题不就迎刃而解了吗?我们能获取首部的长度,不就可以将数据与首部分离了吗?一旦能提取首部,剩余的不就是数据部分吗?封装不就更简单了吗?我们填写首部的字段,然后再加上数据部分,不就进行封装了吗?
我们又如何获取数据长度呢?
前面UDP中,首部字段中,有一个UDP长度,该长度包括数据部分长度,故我们无需担心无法获取数据长度,可这里我们并不知道数据长度,那我们如何提取数据部分呢?实际上,这不是TCP应该关心的问题了,这也是TCP面向字节流的特点,关于提取完整数据,得交给我们应用层来解决,这也就是TCP中的粘包问题的解决;
关于32位序号与32位确认序号涉及TCP的可靠性,TCP为了保证可靠性采用了确认应答的机制;这也是保证TCP可靠性之一的机制;具体如下图;
对于网络上的每一个报文我们无法确定对方是否收到的,但是如果我们规定一旦对方收到我们的报文就要进行回应,此时,若对方回应了,我们一定可以确认对方收到了,如上图所示;就好像两个距离很远的人在聊天,比如两个人相距50米聊天,此时一个叫client的人对一个叫server人喊了一句,"server,你吃饭了吗?",此时,client并不能保证server听到了信息,因此他两做了规定,听到了就要恢复一句,"收到!";此时这个叫server的人也遵守了规则,并进行回应说收到,此时client听到了server说了收到,就知道了server一定听到了自己说的话,因此这个client接着又问了一句,"吃了啥?",接着这个叫server的人听到后,接着又说了一句,“收到”,此时client知道对方一定听到了自己说的话,此时server也给client说了一句,“吃的水饺”,此时我们的client听到了对方说的话,也回复了一句,“收到”;这就是确认应答的粗略理解;
使用TCP的双方都要有两个缓冲区,一个为发送缓冲区,一个为接收缓冲区;而我们也会对这两个缓冲区的每个字节进行编号,这个编号的范围也就是一个整型的范围,0~2^32-1(4294967296),当我们调用系统接口read/write/recv/send时,实际上是将数据在用户自定义缓冲区与TCP缓冲区之间的来回拷贝,而我们所谓的发送也就是将一端发送缓冲区的数据拷贝到另一端的接收缓冲区中;而上述我们发送一个TCP报文到网络时,我们需要给该TCP报文一个序号,该序号为我们缓冲区中发送数据块的起始位置的编号;
当我们对端收到数据后,会给对端一个回应,这个回应也有一个序号,这个序号我们称之为确认序号,这个确认序号为期待对方收到对方下一个报文段的第一个数据字节的序号,而也就是说,从这个序号起前面的数据我们都已经收到了,下一个数据从确认序号这个开始发送;在上图中,我们发送的数据为“Hi”,序号应该为1,而我们期待下一次收到报文端的序号应该为3,因此,我们在发送回应是应该携带确认序号3;
注意:信息的小伙伴们可能发现了上图中,发送缓冲区与对端接收缓冲区是使用一条虚线连接的,这时因为在逻辑上我们认为双方缓冲区是直接进行拷贝交互的,实际上我们要经过下层,以及中间还有若干中间路由才可以到达对端主机,然后再向上交付给其传输层;
由于TCP是全双工通信,因此,两端都可以同时收发,因此,两端都应该有自己各自的序号;
注意:通常这种图都有一个隐性条件,就是时间轴!上图中的数据长度指的时发送端发送报文数据长度;
该字段占6位,保留为今后使用,目前应置为0;
保留位后面接着又是六个控制位,该控制位暂不要求大家全部理解,其中部分牵扯后面知识点,这里暂时只需理解ACK这个控制位即可;
URG:该比特位置为1表示首部中的紧急指针字段有效;
ACK:该比特位置为1表示该报文属于一个应答(回应)报文段,其中确认序号字段有效;
PSH:该比特位置为1表示催促对端赶紧取走对端接收缓冲区中的数据;
RST:该比特位置为1表示该报文属于一个复位报文段,要求对端重新建立连接;
SYN:该比特位置为1表示该报文属于一个同步报文段,请求建立连接;
FIN:该比特位置为1表示该报文属于一个结束报文段,通知对方,本端要关闭了;
有了上述知识,想必大家应该很快能理解ACK控制位的作用,在确认应答机制中,对端发送数据过来时,我们需要进行应答,此时我们发送一个应答报文段给对方,该应答报文段中,ACK必须置为1,表示确认序号有效;
但是这也让很多小伙伴就有了一个问题,TCP报文首部字段中既有序号,也有确认序号,我们用一个不就可以了吗?还可以让我们首部大小变小一些,这样不就可以节约网络资源吗?
有这个问题实际上也合理,我们需要注意的是,我们的TCP协议采用的是一种全双工通信的方式,在双方通信时,你可以给我发送数据,我也可以给你发送数据,因此我们的ACK报文可能不仅仅是ACK报文,其中也可以携带数据,就像我们两进行交流,你问我“吃了吗?”,我可能会回答“吃了,你呢?”;再给对方应答的同时,也会给对方发送一段数据;这也是我们后面所提到的捎带应答机制;
在基于TCP协议的网络通信过程中,是否可能出现当我发送数据到对端后,发现对端的接收缓冲区为满,此时我们无法继续存储数据,选择丢弃;这显然也是不合理的,我们的数据资源经过网络是需要消耗网络资源的,这样白白消耗网络资源却最终被丢弃这种做法显然很不合适,因此窗口大小就是解决这一事情的方案,每次我们给对方发送确认报文时,表示从该确认序号起,该端还可以接收窗口大小值的数据;
与UDP的校验和一样,该字段用来检测数据与首部是否发生错误,若发生错误,则直接丢弃,具体校验方法可去网上搜索;
在某些情况下,我们可能需要传送一个紧急数据,而不想像普通数据一样在接收缓冲区中排队,因此,我们会将URG控制位置为1,并且将紧急数据的最后一个比特位的位置放在紧急指针字段下;这个字段用的不是很多,有兴趣可去网上了解详细用法;
该字段一般设置一些特殊参数,该字段最多有40字节,并不是本文主要讲解内容,所以略过;
我们通过校验和的方式,不仅保证首部的正确性,也保证了数据的正确性,这样的方式保证我们数据在网络传输过程中没有出现差错;
我们使用序列号的方式保证按序到达,同时也保证了可靠性;可能有小伙伴这里也疑惑了,我们发送数据时,不是TCP数据段不是一个一个得发的吗?为什么接收的时候不是按顺序到呢?打个比方,比如平时我们在网购时,我们也是一个一个的下订单,那么我们的订单也能够按照我们买的顺序依次到达吗?也不到一定吧;网络的情况也是如此,非常复杂,因此数据也不一定会按照顺序到达,而序列号可以保证数据的按序到达;
为什么要按序到达?
比如我们实现一个计算的服务,我们给服务端发送计算请求,请求中包含计算参数,我们同时发送一批计算请求,我们希望计算结果与计算结果一一对应,而我们发送请求时若没有按序到达,最终答案顺序与我们的计算方程式也不会匹配;
如何保证按序到达的?
我们可以通过序号来保证,我们发送出去的方程式也会按序到达,因为序号有先后,所以我们的数据段也有先后,最终的次序也就不会乱;
前面我们也介绍过确认应答机制,这里也简略回顾一下,确认应答机制保证了我们以往发送数据段的可靠性,只有最后一个数据段,我们没有应答,不能保证其可靠性;但是我们有接下来的超时重传机制来保证这最后一个数据段的可靠性;
同时这里补充一下,我们前面所学习的确认应答机制是我们发送一个数据报,我们接着等待接收应答后,再发送下一个,实际上,网络中并不会这样传输报文,因此这样串行发送数据太慢了,严重影响数据传输速率,通常我们很有可能是发送一批数据段,接着接收一批应答;如下图所示;
由于有了确认序号,我们也不担心无法区分哪一个应答对应哪一个发送数据段;上图中的应答与确认信号是一 一对应的,而我们前面也学习过确认序号的概念,表示该确认序号前的所有报文都已经收到了,因此,当我们第一个数据段的应答丢失时,而后面两个所对应应答都收到时,我们并不担心,因为确认信号的概念;
在上述报文交换过程中,是否存在报文在网络中丢失的情况,不用思考也知道,这种情况是肯定有可能存在的;可能由于网络繁忙,也可能由于硬件损坏等问题;那么出现如上情况有应当如何处理呢?
这里有两种情况,如下图所示;
其中,情况一为发送数据段丢失,此时根本就不存在应答,此时我们的client端并不知道对方是否收到报文;情况二为应答丢失,此时我们的client端并没有收到应答,也不知道对端是否收到;所以这两种情况都是不可靠的;
而我们TCP采用的策略是超时重传,当我们发送端过了一定的时间后,仍然没有收到对端的应答,此时,我们的发送端将再次发送这个数据;如下图所示;
通常,这个超时重传的时间在Linux下为500ms,若第一次重传后,仍然没有收到对方应答,这个重传时间会乘以2,也就是1000ms,若第二次重传后,仍没有收到应答,接着乘以2,以此类推,累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接
;
上面重传机制为我们保证了可靠性,同时也诞生了一个新的问题,如上述情况二,仅仅只是丢失了接收方发送的应答,我们却仍然重传,此时若我们接收方再次收到这份数据段时,会发现有两份相同的数据段,这显然也是不合理的,此时TCP协议规定,会根据序号对数据段进行去重,这样也保证了TCP的可靠性;
我们前面提过,TCP协议是有连接的协议,所谓连接,是在双方正式通信前,建立一个信道,双方通过该信道进行正常通信,当双方不需要这条信道时,需关闭这条信道;信道建立的过程,就是三次握手的过程,如下图所示;
起初,客户端与服务端都是关闭状态,当我们服务端调用listen系统调用时,此时服务端进入了监听状态(LISTEN),此时便进入到一个可以获取连接的状态(若有才获取);而当我们的客户端调用connect系统调用时,则表示发起连接请求,即三次握手的请求;
第一次握手,客户端向服务端发送SYN请求报文,报文的确认序号为x,这个SYN就是前面控制字段种提到的SYN,也就是将报文SYN对应比特位置为1,通常这个报文不准携带数据,发送完这个报文后,客户端进入SYN_SENT状态,此时第一次握手完成;
第二次握手,当服务端收到客户端的SYN请求后,立刻会像客户端发送一个既携带SYN、也携带ACK的报文,序号为y,确认序号为x+1,这个报文的SYN与ACK控制标志都会被设置成1,发送完这个报文后,服务端会进入SYN_RCVD状态,同时这个OS会为我们维护一个半连接队列,此时这条连接就放在了半连接队列,到这第二次握手完成;
第三次握手,当客户端收到第二次握手的报文时,随后会发送一条ACK报文,这条报文的序号为x+1,确认序号为y+1,此时客户端认为连接建立成功,进入ESTABLISHED状态,当服务端收到ACK报文时,会将这个连接放入一个全连接队列,我们通过accept系统调用可获取全连接队列中的一条连接,到这,服务端获取连接后也视作连接建立成功,第三次握手完成;
信道建立完成后,双方便可以正常通信,这里仍存在很多问题,我们将一 一解答;
如何理解连接?
在OS上,我们可能存在这大量连接,因为一个服务器程序不可能只有一个连接,肯定有大量的用户进行连接,等待服务,那么我们的OS是否需要将这些连接管理起来呢?答案也当然是肯定的,那我们应该如何管理这些连接呢?还是之前我们理解操作系统的六字口诀,“先描述,再组织”;我们使用一个结构体将每个连接描述起来,然后用一种数据结构将这些连接存储起来,方便后续管理,同时我们不难推导出,维护这些连接肯定时需要消耗系统资源的,因此维护连接是有成本的;
三次握手一定能成功吗?
不一定,我们在三次握手的过程中,也是进行网络通信的过程,既然是网络通信,那么一定存在丢包的可能,既然存在丢包的可能,那么三次握手也不一定百分百成功;
为什么一定是三次握手,一次、两次、四次、五次不可以吗?
首先分析一次的情况,一次握手,当我们发送一个SYN报文后,对方就视为信道建立成功,而若我们客户端不管这个连接,我们可以通过单机发送大量的SYN报文给服务端,而客户端不对这些连接做管理,而连接的管理是需要成本的,而我们服务端充满着大量的连接,我们单机就可以让服务端崩溃,这种攻击方式我们称之为SYN洪水;
在分析两次连接,两次连接也是同理,当我们服务端发送第二个SYN+ACK请求时,服务端就算建立连接成功,而我们的客户端需要收到第二个报文后才算连接建立成功,此时我们同样可以采用SYN洪水攻击;
四次、六次、八次等都是同样原理,偶数次,必定时服务端先建立好连接,客户端后建立好连接;
至于五次,为什么不可以,因此三次就足够了,我们为什么要用五次呢?
为了三次握手就一定可以建立好连接?
首先,三次连接是确认双方具有全双工通信的最少次数,也就是双方都至少进行了一次收发;其次,三次握手是我们客户端先建立好连接,而服务端后建立好连接,我们客户端承受着与服务端同样维护连接的成本,而我们服务器性能肯定远远高于客户端,因此我们不害怕单机使用SYN洪水就能把我们的服务器搞崩溃;
在我们通信完成后,我们需要关闭这个信道,而关闭这个信道的过程就是四次挥手的过程,具体如下图所示;
起初,两端已经都建立好了连接,此时若客户端突然向关闭连接,需要经过四次挥手;
第一次挥手,当我们客户端已经没有什么报文需要发送给服务端时,客户端向服务端发送一个FIN报文,其中序号为x,所谓FIN报文就是将FIN控制位置为1,当客户端发送完报文后进入FIN_WAIT1状态,此时第一次挥手完成;
第二次挥手,当服务端收到第一次挥手后,随后发送一个ACK报文,此时确认序号为x+1,发送完毕后,服务端进入CLOSE_WAIT状态,当客户端收到这个ACK报文后会进入FIN_WAIT2状态,此时第二次挥手完成;
第三次挥手,接着若我们服务端也没有什么报文要发送给客户端时,服务端也会发送一个FIN报文,该报文的序号为y,发送完毕后,我们的服务端进入LAST_ACK状态,此时第三次挥手也完成;
第四次挥手,当我们客户端收到第三次挥手时,我们的客户端会发送ACK报文,此时序号为x+1,确认序号为y+1,此时客户端进入TIME_WAIT状态,并再2个MSL过后进入CLOSED状态,而我们的服务端会在收到第四次挥手后直接进入CLOSED状态,此时第四次挥手完成;
补充:MSL是Maximum Segment Lifetime英文的缩写,中文意思是报文最大生存时间;
在学习完上述内容后,同样,我们仍然有如下问题需要回答;
四次挥手一定成功吗?
不一定,与三次握手相同,四次挥手本质就是进行网络通信,同样存在丢包情况;
如果我们发现服务器中存在大量处于CLOSE_WAIT状态的连接,最有可能的原因是什么?
服务端的未关闭socket描述符,关闭socket描述符的本质实际上就是发送FIN报文的过程;而若我们客户端关闭了socket描述符,而我们客户端未关闭,此时服务端会进入CLOSE_WAIT状态;我们可以做如下实验进行测试;
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 创建套接字
int listenSock = socket(AF_INET, SOCK_STREAM, 0);
if(listenSock < 0)
{
printf("create socket fail, errno:%d, errstr:%s\n", errno, strerror(errno));
exit(1);
}
// 绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr("0.0.0.0");
local.sin_port = htons(8888);
int n = bind(listenSock, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
printf("bind sock fail, errno:%d, errstr:%s\n", errno, strerror(errno));
exit(2);
}
printf("bind socket success!\n");
// 监听
n = listen(listenSock, 32);
if(n < 0)
{
printf("listen sock fail, errno:%d, errstr:%s\n", errno, strerror(errno));
exit(3);
}
printf("listen socket success!\n");
// 死循环
int sockfd;
while (true)
{
struct sockaddr_in client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
sockfd = accept(listenSock, (struct sockaddr*)&client, &len);
}
return 0;
}
我们启动上述服务器程序,然后我们使用telnet连接服务器,注意,这里我们服务端并没有关闭套接字,也就是没有发送FIN报文;
接着我们打开了三个telnet服务来连接我们这个服务程序,我们果不其然发现了三条连接已经建立好了,由于客户端和服务端都是一台主机,所以维护连接的成本都在我这一台主机上,所以有六条连接,三条客户端,三条服务端,如上图所示;
接着我们关闭两个telnet服务,接着就只剩下一个telnet服务维护的连接了,我们再看看我们服务端维护的连接,发现有两个服务端的连接是处于CLOSE_WAIT状态,我们再将剩余的那个telnet客户端关闭;
我们发现所有的telnet程序的连接都已经关闭了,只剩下我们服务端维护的连接了,且一直处于CLOSE_WAIT状态;
不难得出,如果我们在服务端中不关闭socket描述符,服务端的机器上将出现大量的失效socket描述符,这时由于我们服务端未关闭socket描述符,所以也没有给对方发送FIN报文,而对方过一段时间自动关闭了;
我们之前在写TCP套接字代码时,当我们启动一个服务器后又马上退出,然后当我们再次启动这个服务器时,经常会给我们弹出绑定失败的消息;如下所示;
上面显示我们这个地址正在被使用;可我们明明已经退出了啊,之前的端口应该也没有使用了啊;为什么会出现这样的现象呢?实际上这就是由于TIME_WAIT状态所维持的那2MSL所导致的,我们看上去我们已经退出服务程序了,实际上,他还会存在2MSL的时间,维持一个TIME_WAIT状态,下图为我们使用netstat查询结果;
在Linux下,我们可以通过下面这条指令查询MSL的值;
cat /proc/sys/net/ipv4/tcp_fin_timeout
假设在我们淘宝或京东的双十一时间上,若我们的服务器突然挂掉,若重启也需要花2分钟,这得造成多大损失,那么我们应该如何解决这种问题呢?让我们得服务器在挂掉后能立即重启;
我们可以使用如下接口进行设置,让我们端口和IP能重复使用;
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
// 设置套接字可以重复绑定
int opt = 1;
setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
我们重新编译代码,运行结果如下;
确实也不会绑定失败了,我们在使用netstat查看连接状态;
确实存在一个处于TIME_WAIT状态的连接,也有一对我们刚使用telnet进行连接的连接;
前面我们也对这一机制稍有提及,在我们TCP报头首部,有一个字段叫做窗口大小,这个字段记录自己接收缓冲区的大小,也就是告诉对方自己的接受能力,一旦对方发送的太快,我们快来不急接收对方的数据时,我们通过更新窗口大小告诉对方,我快来不及接收了,你发慢一点,且这种数据更新会跟随每一次对方发送数据,我发送ACK告诉对方我的接收能力;这种机制我们便称为流量控制机制,由于TCP是全双工协议,因此这种流量控制也是双向的,我可以通过ACK告诉你我的接收能力,你可以通过ACK告诉我你的接收能力;
可有没有可能一方迟迟不讲自己的数据从接收缓冲区中取走,而另一方却想发送给对方数据,却无法发送的情况呢?这时我们前面提及的一个PSH字段就派上作用了,它会督促对方赶紧从接收缓冲区中取走数据;如下所示;
当client收到对方rwnd为0的ACK后,client又想给server继续发送,因此会发送一个PSH标志位为1的报文,督促对方赶紧取走缓冲区数据;
当对端接收缓冲区满了以后,我们如何得知对方接收缓冲区的数据已经被取走一部分,可以继续放数据了呢?
这时,本端回向对端发送窗口探测报文,该报文不携带任何数据,只是为了得知对方窗口大小;对方收到我们的窗口探测报文后会向本段发送一个ACK,告诉我们对端窗口大小;
对端也有可能会向本端发送一个窗口更新通知,该通知会告诉我们对端的接收缓冲区已经更新了,并将窗口大小告诉本端;
滑动窗口机制主要是解决TCP效率的问题,前面我们提过超时重传机制,以及我们的TCP不是发一条报文,收到ACK后再发送另一条报文,而是发送一批报文,然后可能会收到一批ACK;而重传机制意味着我们发送过的报文不能就此丢掉,而是选择暂时保存起来,那么这一切是如何实现的呢?这就要介绍我们的滑动窗口机制了;
前面我们也提过我们会将缓冲区按比特位进行编号,从0到2^32-1,因此我们可以将我们的接收缓冲区看成一个很大的字符数组,我们将这个数组分为五个部分,如下所示;
其中,滑动窗口的区域就是红色方框的区域,该窗口的大小正是对方给我们更新的窗口大小,我们可以用两个变量来控制这个滑动窗口,分别为win_start与win_end;这么介绍滑动窗口肯定还是不够,我们必须更深一层次来探究滑动窗口的奥秘,我们用接下来几个问题来解决滑动窗口相关问题;
滑动窗口一定会向右移动吗?
不一定,若对端应用层并没有接收数据,而只是给我们发送一个ACK报文,告诉我们对端收到这条报文了,并携带这一个确认序号,确认序号告诉我们下条报文该从这个序号开始发送数据了,我们就将win_start = 确认序号;然后通过win_end = win_start+min(对端发送ACK中窗口大小字段,拥塞窗口大小)来更新滑动窗口大小;关于这个拥塞窗口大小可暂不理会,后面会介绍拥塞窗口的概念;若对端应用层没有取走数据,此时对端发送过来的窗口大小就会变小,然后win_end就不会向右移动,而对端一旦收到了本端发送的数据报文,就一定会将win_start向右移动;
滑动窗口大小可以为0吗?
当对端接收窗口大小为0时,此时我们的win_start与win_end会重合,此时窗口大小便为0;
如果滑动窗口一直向右滑动会越界吗?
不会的,我们可以将接收缓冲区的数组理解成要给环形数组,就像前面学过的环形队列一样,我们通过取模操作来实现;
如果我们发送三个报文,大小都为1000,序号分别为1001,2001,3001,此时若第一个报文丢了怎么办?第二个、第三个分别丢了又怎么办?
假设第一个报文丢了,对端会收到2001,3001,发现没有1001,因此会发送一个确认序号为1001的应答报文,待超时重传时间到后,会进行重传,后面两个报文丢了也是如此,我们使用确认序号的方案实现重传指定报文;
补充:
这里补充TCP的另一个机制,快重传;若本端收到对端连续三个及以上重复的ACK报文,则会对指定报文进行补发,而无需等待超时重传,如下图所示;
快重传 VS 超时重传
两者之间并不矛盾,而是一种互补关系,两者快重传虽然快,但是有一定的条件,而超时重传不需要任何条件;
前面我们介绍了多种机制保障可靠性,而我们始终是以单机的视角看待问题,而互联网中,存在成千上万台主机,我们就不得不考虑网络问题了;当在网络拥堵时,我们仍然继续发送大量的数据,这样只会加重网络的拥塞,TCP也提供了对于该种情况的应对策略;分别为慢开始、拥塞避免、快重传以及快恢复,如下图所示;
首先,当网络发生拥塞时,拥塞窗口大小被设置成1,接着执行慢开始算法,所谓慢开始增长速率并不慢,是以指数级增加的,第一次发送一个报文,若收到ACK则继续发送两个报文,若均受到ACK则继续发送4个,接着8个,类似增长,如上图,一旦增长到某个阈值(ssthresh)后,则使用拥塞避免算法,呈线性增长,也就是一个一个增长,如上图到16后开始执行拥塞避免算法,而使用拥塞避免算法窗口大小到24后又发生了网络拥塞,此时拥塞窗口被设置成1,阈值除以2,接着继续执行慢开始算法,直到大于等于阈值后,再次执行拥塞避免算法,接着到拥塞窗口大小到16后,突然我们收到了3个ACK,也就是发生了快重传,此时我们执行快恢复算法,我们此时不会将拥塞窗口大小设置为1,而是首先将阈值除以2,接着将拥塞窗口大小调整为阈值的大小,接着执行拥塞避免算法;
每当对端收到一个报文后,为了尽可能返回更大的窗口大小,可能不会马上做出应答,而是等待一会,说不定这段时间应用层会从接收缓冲区中取走一些数据,更新出更大的窗口大小返回给对方,这样可以提高网络通信的吞吐量;
由于TCP是一种全双工的协议,因此每次本端收到对端发送的数据时,当我们做出应答时,一般不会做出一个纯应答报文,其中也会夹带我们要给对端发送的数据,这样可以提高网络通信的效率,这样的机制我们称之为捎带应答;
首先明确粘包中的包指的是报文中的数据包,由于TCP时面向字节流的,因此会出现粘包问题,我们必须才上层(应用层)采取一定的协议规定,如\r\n作为分隔符等,将TCP的数据包进行分离,而UDP是采用面向数据报的,不存在所谓的粘包问题;
HTTP
HTTPS
SSH
Telnet
FTP
SMTP