传输层协议------TCP协议

协议段格式

1.首先我们先看TCP协议段格式的样子为:
在这里插入图片描述
大致包含的内容如上图所示。

其中,字段中的每个不同的内容都有着不同的作用。

  • 16位源端端口号/对端端口号:描述这个数据从哪个进程来,到哪个进程去。
  • 32位序号和确认序号:实现tcp的包序管理和确认应答机制。(在下面会体现出这两个的作用)
  • 4位tcp报头长度:以4字节为单位,描述tcp报头长度(就是tcp头部有多少个32bit);对于tcp的头部字段,它的固定长度是20字节,但是此时由4个比特位去表示,那么以4字节位单位,最大可有60个字节(4比特位最大表示15,然后15*4 = 60字节)所以对于其他40个字节的来源就是上图中的选项数据和应用层交付下来的数据。(①:所以应用层交付下来的数据就等于tcp报头长度减去20再减去选项数据后剩下的字节;②:而固定的20字节就是上面表格中除了选项数据部分和应用层要交付数据的大小以外的固定大小)
  • 6位保留位:暂时还没有使用,为长远的以后做打算。
  • 6位标志位:用于识别报文类型:
    URG:紧急指针是否有效。
    ACK:确认号是否有效。(也就是确认应答)
    PSH:提示接收端应用程序立即从TCP缓冲区中将数据拿走。
    RST:对方要求重新建立连接。(我们把携带RST的报文段称为复位报文段)
    SYN:请求建立连接。(我们把携带SYN的报文段称为同步报文段)
    FIN:请求断开连接。(通知对方,本端要关闭了,我们把携带FIN的报文段称为结束报文段)
    对于上面的标志位,一般都是当其等于1时,就要对它们要作用进行处理。
  • 16位窗口大小:用于实现滑动窗口机制。(下面会说到滑动窗口)
  • 16位校验和:发送端填充,CRC校验。接收端对校验计算,如果校验失败,那么数据就有问题。(此处的检验和不光包含tcp首部部分,也包含tcp数据部分)
  • 16位紧急指针:标识哪部分数据是紧急数据(优先处理的数据)。(也就是标识的是位置,是紧急数据的结束位置,当然紧急数据的结束位置,也就代表的是要发送的数据的起始位置)
  • 选项数据:不一定有,通常是一些协商信息或者是保存一些额外数据的作用。
    为什么不一定有捏,我们来看看tcp字段的结构体如下:
    在这里插入图片描述
    一对比,好像就是没有,但是标准的头部字段中是包含有的,所以对于tcp字段中的数据选项部分可以暂时忽略。

确认应答机制

首先,如下图:
在这里插入图片描述

由于在tcp的传输中,tcp将每个字节的数据都进行编号,称为序列号。
假设客户端和服务端已经建立好连接在发送数据。

客户端在给服务端发送数据的时候,会将自己所发送的数据对应的每个序列号发送给服务端,而服务端进行接收数据后,会将这些数据的每个序列号进行确认,当服务器接受完此次客户端发送的数据后,会给客户端发送一个确认序列的信号,而这个确认序列的信号大小就等于服务器接收数据的序列号的下一个。(例如:图中,服务端在进行接收客户端的数据,完成后,给客户端发送一个ACK,此时发送的这个ACK大小就等于此次服务端接受完客户端发送的数据最后一个序列号的下一个序列号)

每一个ACK都带有一个对应的确认序列号,接收数据的一方已经接收到了那些数据,此时发送数据的一方应该从什么位置开始发送数据

超时重传机制

1.当发送方发送给接收方的数据丢失的重传机制:
如下图:
在这里插入图片描述
如上图,客户端给服务端发送的数据在半路上丢包了,此时服务端没有接收到数据,而客户端此时正在等待服务端发送ACK确认信息,这样肯定等不到。所以,就会有一个重传机制,而这个机制就是在特定的时间内,如果发送方没有接收到接收方的对接收数据的确认信息,就会重新发送该段数据。

2.当接收方给发送方发送的确认信息丢包时的重传机制:
在这里插入图片描述
如上图,客户端在给服务端发送数据,此时服务端接收到数据,给客户端发回了确认序号,但是确认序号在半路丢失了,此时由于客户端在特定的时间间隔内还是没有收到服务端给其的确认序号,此时客户端(因为不能知道是什么情况的错误)只能重发信息,然后服务器给客户端第二次发回了确认信息,然后开始它们往后的通信。(注意:客户端再接收到重复包裹的时候,会将该报进行丢弃,然后发送对该报的确认信息)

总结:

  • 对于超时重传机制,不管是发送方发送的数据丢失,还是接收方接收到数据给发送方发送的确认信息丢失,如果在特定的时间间隔内,发送方没有接收到来自接收方的确认信息,发送方都会进行超时重传该包裹。
  • 对于接收方接收到相同的报,会对该报进行丢弃,然后给发送方发送该报的确认信息。

3.那么,超时重传机制的时间是如何确定的捏?
①:对于重传机制,我们有以下要求:

  • 在最理想的情况下,我们需要找一个最小的时间,保证确认应答一定能在这个事件内进行返回。
  • 但是这个时间的长短,是根据网络情况的不同而不同。
  • 但是如果重传时间设置的比较长,会影响整体的效率。
  • 但是如果重传时间设置的比较短,就会出现重复接收的包裹较多,也会影响效率。

也就是对于超时重传机制,时间过长或者过短,都会影响效率,所以此时基于tcp,为了保证无论在任何情况下都能比较高性能的通信,因此会动态计算这个最大超时时间。

②:tcp的超时重传机制时间:

  • 在linux中,超时时间以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
  • 如果重发一次之后,还是等不到应答就再重发一次,而这次就等于 2* 500ms,后面依次增加倍数。(每次增加为2的倍数,也就是下此等待时间为4*500ms)
  • 但是如果等到重发的次数累计到一定次数,TCP会认为网络或者对端主机出现异常了,直接强制关闭连接。

协议特性

对于TCP通信,它具有的特点是面向连接,可靠传输,面向字节流传输。
而这三个特性,也就体现TCP协议具有可靠性。

面向连接

对于tcp通信,它又有三个特点:

  • 基于连接的。
  • 全双工通信,在通信前必须保证双方都具有数据收发能力。
  • 有状态的协议,在特定状态下只能干特定的事情。(如果在特定状态下,没有得到特定的结果,那么就会可能出现一些情况,甚至可能会出现重新建立连接)

三次握手建立连接和四次挥手断开连接

1.三次握手建立连接
在这里插入图片描述
如上图所示,便是三次握手建立连接的过程。

其中:

上述图的状态描述如下:

  • CLOSE:初始状态,表示TCP连接是“关闭着的”或“未打开的”。
  • LISTEN:表示服务器端的某个SOCKET处于监听状态,可以接受客户端的连接。
  • SYN_RCVD:表示服务器接收到了来自客户端请求连接的SYN报文。在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat很难看到这种状态,除非故意写一个监测程序,将三次TCP握手过程中最后一个ACK报文不予发送。当TCP连接处于此状态时,再收到客户端的ACK报文,它就会进入到ESTABLISHED 状态。
  • SYN_SENT:这个状态与SYN_RCVD 状态相呼应,当客户端SOCKET执行connect()进行连接时,它首先发送SYN报文,然后随即进入到SYN_SENT 状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT 状态表示客户端已发送SYN报文。
  • ESTABLISHED:表示TCP连接已经成功建立; 表示两台机器正在传输数据。

其实在三次握手的时候,除了相互发送ACK和SYN报文以外,其实还有对序列化进行调整的:

①:客户端进行连接的时候,它不仅发送一个SYN=1报文,还会发送一个seq序号为自己要发送的报文序号,假设是0(从第0个序列号开始发送数据,也就是从要发送数据的第一个位置发送数据);

②:服务端接收到这个序号之后,在上面回复ACK+SYN都为1的基础上,还是会发送一个ack确认序列号为客户端发送的seq序号+1,所以对于上面的情况,服务端会给客户端再发送一个ack=1(这个是服务端准备接收的下一个数据报的序列号),和一个seq=0(假设这是服务器进程的初始序列号);

③:客户端接收到服务端发送过来的SYN+ACK+ack+seq后,客户端会给服务端发送一个ACK=1,seq=1(发送这个是对客户端要发送的报进行确认,就是自己的初始序列号+1得到的),ack=1(这个是对服务端的初始序号进行+1得到的,也是客户端对服务端的初始序列号进行确认)

对于服务端,如果此时服务端在开始的时候处于的不是LISTEN状态,而此时的客户端对服务端进行连接,那么服务端会给客户端发送的是RST。

此时,会产生一些问题:
(1)为什么TCP连接的时候是握手三次建立连接捏

答:因为对于TCP连接,它握手三次建立连接的原因就是为了防止旧的重复链接引起的连接混乱的问题。

①:TCP是一个全双工通信,它必须保证两方都处于在线状态才能进行连接,而如果进行两次握手就建立连接(由于在网络情况比较差时,客户端因会多次建立连接),那么接收方就只能选择接收连接请求,或者拒绝连接请求,但是它并不知道这个连接请求是正常的请求,还是由于网络环境问题所造成的过期请求,前者还好,如果是后者,建立连接之后也就会造成错误的连接。

②:TCP最重要的一个特点就是可靠性,所以对于数据的收发,而tcp为了在网络状态不好的情况下去构建一个稳定的数据连接,它就需要一个序列号去保证数据传输的稳定性,而这个序列号的作用就是防止数据包的重复传输,和有效的去解决数据包接收时的顺序颠倒问题。而在传输的过程中,如上图的第二次握手,服务端可以将SYN和ACK一块发送给客户端,不需要分开来发,所以进行三次连接就可以进行了,完全不需要再进行四次将它两个分开传输。

(2)三次握手,如果出现错误应该怎么办?

答:对于三次握手,它每次都有可能出现问题,而解决这些问题的办法就是上面所属的6个标志位,服务端会通过发送对应的标志位报文,然后客户端进行对应的处理,当然还有超时发送机制。

①:第一次握手时,客户端对服务端发送的SYN报文丢失,这个处理比较简单,就是客户端在发送SYN报文后,等待了一定的时间,没有得到服务端的结果,然后超时重新发送该报文。

②:第二次握手时,服务端对客户端发送的ACK+SYN报文丢失(服务器不会重传该报文),此时客户端因为在一段时间内没有接收到服务端来的该报文,此时客户端超时重发一个SYN报文给客户端,然后重新开始建立连接。

③:第三次握手时,客户端对服务端发送的ACK报文丢失,服务端等待超时,然后发送一个RST给客户端,客户端接收到该报文后,重新发起请求连接。

注意:服务端进入到SYN_RCVD状态时,只会对ACK报文进行正确的处理,如果说此时接收到了其他报文,都会给发送发发送RST报文,请求其重新建立连接。
2.四次挥手断开连接
在这里插入图片描述

如上图所示,就是tcp四次挥手断开连接的过程(其中我默认的是客户端想断开连接,然后先提出的,但是在实际情况中,由于tcp是全双工通信,所以服务器也可以进行先提出断开连接,不过操作都是一样的)。

其中,上图的状态描述如下:

  • FIN_WAIT1:等待对方的FIN报文。(表示的主动断开连接的一方,就是这一方此时的socket状态是ESTABLISHED,他想主动断开连接,所以给对方发送一个FIN,然后自己的状态置为FIN_WAIT1,而当对方回应ACK的时候,又将自己的状态置为FIN_WAIT2,通常情况下,无论对方处于什么情况下,接收到FIN后都会立即发送ACK报文,所以在一般情况下很难看到主动方处于FIN_WAIT1状态,即使是用netstsa都很难看见,而FIN_WAIT2倒有时候可以看见)
  • CLOSE_WAIT:也就是被动关闭方接收到了主动关闭方的一个FIN报文后进入的状态,这个状态主要的作用就是被动关闭方再接收到FIN报文后开始对自己进行检查,检查自己是否还有数据要去发送给对方,如果没有的话,那么close()这个SOCKET,并且发送FIN报文给主动关闭方(关闭自己到对方的这个连接),如果有的话,那么就根据程序的策略,选择删除或者发送。
  • FIN_WAIT2:等待对方的FIN报文。(上面在FIN_WAIT1中介绍到了。实际上,FIN_WAIT2状态下的socket处于半连接状态,即有一方调用close主动断开连接。但是FIN_WAIT2是没有超时的,也就是说,这种情况下,对方如果不关闭(不配合四次挥手),那么这个状态就会一直保持下去,直到系统重启,所以如果FIN_WAIT2的状态比较多,那么就会导致内核崩溃)
  • LAST_ACK:当被动关闭的一方发送FIN后,等待对方的ACK时就处于LAST_ACK状态,当接收到对方的ACK时,就处于CLOSE状态,释放套接字。
  • TIME_WAIT:当主动断开方接收到被动断开方的FIN和给对方发送ACK后,就处于TIME_WAIT状态。(这个状态会在下面详细讲解)

其实除了上面介绍的10种状态外,还有一种状态,虽然上面没用到,但是可以了解一哈:

  • CLOSING:这种情况不太常见,就是当一方要关闭给对方发送了一个FIN,而此时自己应该处于FIN_WAIT1状态,等待的是对方的ACK报文,但是对方却发送了一个FIN报文给这边。出现这种情况,只有一种可能,就是双方同时主动关闭,都给对方发送了FIN报文。这就是会出现CLOSING状态,也就是双方都主动断开连接。

此时也会产生一些问题:
为什么tcp的挥手断开连接是四次?

答:因主动关闭方发送给被动关闭方一个FIN,此时只是代表主动关闭方不在给被动关闭方发送数据了,所以被动关闭方会给主动关闭方发送一个ACK,表示自己知道主动关闭方要关闭了,所以他会看一下自身是否需要给主动关闭方发送数据,如果需要会进行发送数据,如果不需要或者发送完了,会给主动关闭方发送一个FIN,表示自己也不发送数据了,此时主动关闭方接收到FIN后给被动关闭方发送一个ACK,这个情况刚刚好是四次握手。

理解TIME_WAIT的状态

上述我们说到了TIME_WAIT状态,是主动断开连接方接收到被动断开连接方的FIN和给对方发送ACK报文后处于的状态,而这个状态在上面的图上,我们可以发现,变成这个状态后,会等待2MLS后,主动断开连接方才会进行CLOSE,去断开套接字然后释放套接字资源。

1.而上述过程,为什么要这样,我们通过如下实验进行说明。

首先,我们先运行tcp服务器,然后运行成功后使用客户端对其进行连接,连接成功后立即断开服务器,然后再使用同样的端口号去再运行服务器,此时会出现如下情况:
在这里插入图片描述
这里显示的是当前端口是被使用的,但是我们已经对其进行了关闭呀,怎么会出现这种情况。这是因为,我们在断开服务端连接的时候,虽然服务端的应用程序终止了,但是TCP协议并没有完全断开,因此此时不能监听同样的断开。

对此我们使用nestat命令去查看9000端口,发现此时9000端口还被占据着。
在这里插入图片描述
此时我们将客户端也进行断开,表名客户端也不进行数据发送了,然后此时的查看9000端口号就出现如下情况:
在这里插入图片描述
此时该端口号对应的服务端处于TIME_WAIT状态,然后我们继续进行绑定地址的时候,还是会出现这样的情况:
在这里插入图片描述
而这样的情况出现,表明了,虽然双方都主动关闭了应用端进程,但是主动关闭方还是要等待2个MLS之后才会去释放资源,关闭套接字。

从此我们就引出了以下几个特点

  • TCP协议规定,主动关闭连接请求的一方,当处于TIME_WAIT状态的时候,会等待两个MLS之后才会处于CLOSE状态。
  • 我们强制关闭了服务端的一方,所以服务端是主动关闭连接的一方,在处于TIME_WAIT状态的时候,仍然不可以监听相同的端口号。
  • 我们所说的MLS其实就是最大报文生成时间,这个时间由于系统的不同会存在差异。下面是在Centos7上可以看到的:
    在这里插入图片描述

2.都做了实验,那么TIME_WAIT状态的作用是什么?

如果没有TIME_WAIT状态的后果:假设如果没有TIME_WAIT状态,而回复主动断开连接方回复ACK后直接释放套接字资源,此时如果这个ACK丢失了,那么被动连接方在长时间没有等待到主动方发来的ACK后会重传FIN报文,而此时主动方已经释放了套接字资源,进入了CLOSE状态(端口号已经没被使用了),而此时如果有新的进程去占用了这个端口号,好家伙,这个新进程还没发送啥数据,直接来一个FIN报文,这就很不合理。还有新的这个套接字给服务发送SYN报(设服务器是被动关闭方)而服务器却处于LASK_ACK状态,服务器等待的是ACK,然后去释放套接字,直接又来个SYN请求连接,这样也不合理。

TIME_WAIT状态的作用:为了保证最后一次主动断开方发送给被动断开方的ACK的安全发送,也为了防止①种情况的出现。

为什么TIME_WAIT状态要等待2mls捏?

  • 上面我们可以看到,MLS为报文的最大生存时间。
  • 何为报文最大生存时间呢,就是这个任何TCP报文,在网络中存在的最大时长,超过这个时间,这个TCP报文就会被丢弃。
  • 而等待2MLS的作用就是,主动断开方并不知道被动断开方是否收到ACK,被动方如果没有收到ACK就会进行超时重传FIN,此时会考虑到最坏的情况,就是第四次挥手的ACK包的最大生存时间,和被动断开方重传发送的FIN包的最大生存时间,就是2个MLS。

解决TIME_WAIT状态引起bind失败的方法

上面讲到,如果说主动断开方处于TIME_WAIT状态时候,这个端口号是不可以被复用的,那么将会对以下这些情况进行处理的时候就会出现一些情况:

  • 服务器要请求非常大的客户需求。(每个连接的生存时间可能很短,但是每秒都会有很多的连接)
  • 这个时候如果由服务器请求断开连接(例如一些客户端非常不活跃,需要被清理掉),那么就会产生大量的TIME_WAIT状态。
  • 由于我们的需求量是很大的,就可能导致处于TIME_WAIT状态的进程会比较多,而每一个连接都会占用一个五元组(源端口号,对端口号,源ip地址,对ip地址,协议类型),其中服务器的ip和端口和协议是固定的,如果新的客户端的连接的ip地址和端口号和TIME_WAIT占用的连接重复了,那么就会出现问题。

所以针对上面的问题,tcp也不会坐视不管,它提供了一个函数,只要我们在进行tcp服务器的创建时,直接调用这个函数就会直接解决问题:

函数接口int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

其中,每个参数所代表的内容如下:

  • sockfd:tcp服务器所创建的套接字所返回的的描述符。
  • level:选项的级别和必须指定的选项。其中对于TCP协议,应该将该参数写为—>SOL_SOCKET。
  • optname:选项的名称。其中对于TCP协议,应该将该参数写为—>SO_REUSEADDR。(其表示允许创建端口号相同的但ip地址不同的多个套接字描述符)
  • optval:标识的是一个缓冲区,setsockopt从optval中取得选项带设置的新值,getsockopt则把已经获取的选项当前值存放在optval中。如果不提供或返回选项值,optval可以为NULL。
  • optlen:是一个值结果的参数,开始传入的时候,传入的应为optval参数类型的大小,并在返回时修改已指示返回值的实际大小。

然后我们进行操作,在套接字创建好就开始调用该函数,例如,为函数的使用:
在这里插入图片描述
然后在套接字创建好去调用该函数,此时我们使用如下端口号去创建tcp服务器,然后用客户端进行连接,连接建立好然后主动断开tcp服务器,我们发现,我们直接拿开始的哪个端口号去再重新创建tcp服务器的时候,既然是可以成功的,如下:

在这里插入图片描述
此时我们用netstat命令去查看9000端口,发现有一个是处于LISTEN状态,一个处于FIN_WAIT2状态,可以说是完成了端口的复用。
在这里插入图片描述

理解CLOSE_WAIT的状态

上面我们说到过,CLOSE_WAIT状态是被动断开连接方在接收到主动断开连接方的第一个FIN报的时候处于的状态,而这个状态就是查看自己是否还有数据发送给对方,如果由就发送,如果没有就给对方发送FIN,并将自己处于LAST_ACK状态。

那么这个状态还有什么作用呢?
那就是还可查看四次挥手有没有执行完成,因为这个状态一般情况下存在的时间比较长,我们可以通过命令很容易就可以查看到。

由于上面的情况,所以对于CLOSE_WAIT状态有了下面这个问题,当一台主机中出现了大量的CLOSE_WAIT状态的进程之后,说明出现了什么问题

答:由于CLOSE_WAIT的状态是被动接收到FIN报确认回复后进入的状态,然后等待上层进行处理,是否还有数据要进行发送。那么一台主句如果由大量的CLOSE_WAIT状态的进程,意味着有可能代码在连接断开后,没有进行
close(没有正确的关闭套接字),也就是没有进行四次挥手的下一个操作,所以会出现这个情况。而对于这个情况所应对的操作,就是在每个要断开连接的端在断开连接的时候进行close操作即可。

保活机制

1.为什么会有保活机制?

答:由于在TCP连接的时候,他是一个全双工通信,他要求的是两方都要同时在线,才能进行通信,但是如果在TCP传输过程中,突然网络中断了,并且一方已经很长时间没有给对方发送数据了,所以他就不知道此时的连接已经断了,这意味的不知道断开连接这方的套接字他还以为自己连接着,但是是没链接状态,也就是它在此次通信已经不能被使用了,但是还占用着资源,这样就有些浪费资源了。

2.保活机制:服务器在长时间没有数据通信的时候,则会每间隔一定的时间发送一个保活探测数据包给对方,如果多次没有收到探测响应,则认为对方已经断开连接了,那么就释放资源。

其中,我们通过查看保活机制的时间如下:
在这里插入图片描述

通过上面的查看我们发现,在服务器默认的长时间没有数据通信是7200s,而间隔一定时间发送的保活探测数据包的时间为75s,当9次没有收到响应,那么就断开连接。

注意:上面所示的都是默认的配置时间,如果我们感觉到这些时间有些太长了,我们可以通过setsockopt函数进行修改,上面我们介绍到这个函数,通过选项进行修改即可。

可靠传输

可靠传输也是TCP协议的一个重要的特性,是完成这个发送方发送的数据能够从发送方如何发送的,那么接收方就如何接收,不能产生半点误差的重要作用。
而可靠传输的实现也是基于以下几个方面:

  • 面向连接:全双工通信,保证双方都在线,都具有收发数据的能力。
  • 丢包检测:确认应答机制,发送方发送的每一个数据包,接收方接收后都要对其进行确认回复。
  • 超时重传:超时重传机制,等待确认应答的时候,在一定的时间内,没有得到确认应答回复,那么就对对应的报文进行重新发送。
  • 序号+确认序号字段:进行数据包序管理,保证数据有序发送,有序交付。
  • 校验和字段:二进制反码求和,保证数据是正确无误的接收。

而对于面向连接,丢包检测,超时重传,校验和字段我们在上面都已经见过了,所以说,下面主要是对完成数据报序管理,保证数据有序发送,有序交付进行理解。

滑动窗口

首先,我们先看下图:
在这里插入图片描述

如上图,简单画了一下发送方和接收方的缓冲区,然后描绘出其的滑动窗口的情况,上图只是一个简单的例子,大小都是假设出来的。

首先,先了解一下发送方滑动窗口的后沿和前沿(假设窗口的移动方向是从左向右移动)。

①后沿移动情况有两种可能:

  • 不动(没有收到新的确认,因为对于后沿来说,如上图,后沿的左边的数据必须是全部已经发送出去,并且每一个都接收到了确认才会移动)
  • 前移(上图就是右移,移动到没有收到确认的哪个位置,因为要保证后沿走过的数据必须完整的已经被接收到了)

②前沿移动情况有三种可能:

  • 通常是不断向前移动。(要维护滑动窗口的大小)
  • 不动。
    ①:没有收到新的确认,对方通知的窗口大小也不变。(这个要注意到,接收方每次给发送方发送确认信息的时候,会携带自己的滑动窗口的大小,而发送方要根据接收方的滑动窗口的大小进行调整)
    ②:收到新确认但对方通知的窗口缩小,使发送窗口前言正好不动。
  • 向后收缩。(对方通知的窗口缩小了,但是要注意,这个情况在TCP中是不支持的)

③发送窗口状态的描述:
如上图,用P1,P2,P3分别指向数据的三个位置,P1为窗口的后沿,P3为窗口的前沿,P2为窗口中目前发送的数据的位置,所以就有一下描述。

其中:

  • 小于P1的是已经发送的数据,并且已经得到了确认。
  • 大于P3的部分是在滑动窗口外面还没发送的数据。
  • P3-P1表示的滑动窗口的大小。
  • P2-P1表示的已经发送了数据但是还没有接收接收方发送的确认报文的部分。
  • P3-P2表示窗口内可以发送的数据,但是尚未发送的数据部分。

但是,数据传输的过程中,并不是一帆风顺的,总是会出现丢包,包错误,而出现的问题,而这样的问题对于窗口的滑动都有一定的影响,所以基于可靠传输的实现滑动窗口有三个协议,分别对错误的数据重发有不同的处理方法,如下:

停止等待协议(SW)

大概有以下两步:

1.发送方给接收方发送数据,发送方直接只有接收到NCK或者ACK报文后才会发送下一个包。

其中过程如下:

  • 接收方收到发送方发来的数据,然后给发送方发送ACK确认报文。
  • 发送方发送的数据半路丢失,发送方长时间没有接收到ACK或者NCK(报文错误的报文)报文,然后发送方前者是超时重发。
  • 接收方收到发送方发来的数据,但是数据有误,则给发送方发送NCK报文,发送方接收到了NCK报文后,重新发送数据。

2.接收方给发送方发送ACK或者NCK报文确认报文的情况,是接收到了还是报文有误,必须说清楚。
其中过程如下:

  • 信号成功发送,发送方根据接收到的信号,选择重发或者发下一个包。
  • 信号半路丢包,发送方长时间未收到确认就超时重发,此时可能会产生接收方收到两个包,此时如果接收到重复的包,会对其进行丢弃。(因为每个包上都有自己的标记,如果接收到的包的序号是已经出现)
  • 信号超时了(但是最后还是发过去让接收方接收到了),但是此时发送方已经超时重发了,此时发送方不会继续发送下一个包,等到接收方返回ACK或者NCK的时候,发送方就不知道这个确认信号是对于哪个包的,因此对每个包加上序号是非常重要的。
回退N帧协议(GBN)

大概有以下几步操作:

1.发送方会给自己的滑动窗口设置大小,假设我们是按照3个bit位去设置的,
3个bit能表示的大小范围会是0~7(其中7是根据2的n次方-1得到的,其中n就是我们用多少个bit位去设置,那么n就是多少)

设发送方窗口长度为Wt,那么就上面的范围,Wt的可取长度为1~7,而对于接收方的滑动窗口的大小为1。

如下图:
在这里插入图片描述

其中,Wr为接收窗口,在回退N帧协议中,接收窗口的大小为1。

2.其中移动过程是这样的,发送方每次给接收方发送Wt个包裹,而接收方接收一个往后面移动一下,并且返回一个确认包。

3.如果出现误码,则此时接收方的滑动窗口就不会确认这个包,不仅会导致这个序号的数据不会接收,并且从这个位置开始此次发送窗口发送的所有数据都不会接收,因为序号对不上。所以,发送方因为接收不到接收方发来的确认序号,会从最近一次的确认序号开始发送从该位置到发送窗口前沿的所有包裹。(如果窗口中某个序号的数据出错,那么从这个位置开始到发送窗口边沿的数据不管是否出错都要重新发送)

问题:如果发送方的窗口大小超过了2的n次方-1会如何?

答:就按上图例子来说,如果说此时发送窗口的大小为8,那么就会出现如下情况:
在这里插入图片描述

发送方一次性发送了8个数据包,而接收方全部接收了。这样还好,但是如果说接收方发送给发送方的确认信号丢失,就会出现严重的问题,那么发送方就会对这段数据进行超时重发,那么发过来的数据还是从0开始到7,就会出现新旧数据不分的结果。(为什么只发了一个确认包,其实也是为了提高效率,也就是发送个确认就代表从这个确认往前的所有包裹都已经接收到了(包含这个包裹),在下面的延迟应答中会介绍到,其实也交累计确认)

注意:对于回退N帧协议,如果发送方发送窗口的大小为1,那么就和停止等待协议一样了。

选择重传协议(SR)

选择重传协议,直接对发送方窗口大小和接收方窗口大小都进行调整。

  • Wr不再为1。
  • 为了使发送方仅仅重发出现错误的分组,接收方不再累积确认,而需要对每个正确接收到的数据分组进行注意确认。
  • Wt的大小范围为1~2的n-1次方。(其中n还是分组所需的bit位数)
  • Wr的大小为1~Wt。

主要过程是:发送方给接收方发送窗口内的所有数据,接收方接收后,往后滑动,而发送方接收到确认序号后也往后滑动。

  • 对于发送方,接收到一个分组的确认号后,才会往后滑动,如果每收到,就一直卡到这里,直到超时重发。
  • 对于接收方,接收到一个正确的分组,那么也就往后滑动,如果接收到的序号不对如果是本窗口内的,还是会接收,但是如果后沿所对的需要的分组一直没接收到,就会一直堵塞,直到接收到。

注意:如果Wt的大小是大于分组所用bit位所表示的最大数量时,那么也会出现与回退N帧协议一样的新旧数据不分的情况。

流量控制

1.一般来说,我们总是希望数据传输的能够快一些,但是如果发送方把数据发送的太快,接收方就可能来不及接收,这样就会造成数据的丢失。

2.流量控制:就是让发送方发送速率不要太快,要让接收方来得及接收数据。而流量控制的机制,就是基于上面所述的滑动窗口机制所实现的。

3.而流量控制是怎么实现的捏?如下:
首先,我们先看如下图:

在这里插入图片描述
从上图,我们可以看出来(上图中的rwnd就是接收方窗口的大小),发送方的滑动窗口大小的变化取决于接收方的接收窗口的变化,所以我们可以看出来,对于滑动窗口的介绍,我们在上面一共有三种协议,这三个协议没有谁好谁坏,都是在不同的状态下使用的,去应对不同的情况。

4.问题:如果说上面的接收方的接收窗口的大小设置为0了,那么发送方不再发送tcp报文,发送方又如何知道接收方什么时候又可以接收数据了捏?

答:这里也就使用到了我们上面在tcp连接的时候讲的保活机制,发送方每隔一段时间会发送一个探测给接收方,如果此时接收方的接收窗口还是0,那么发送方就继续重新计时这个时间,然后在发送探测给接收方,一旦接收方的接收窗口大于0,那么就开始给发送方发送数据。

注意:这里的发送方发送的探测信息如果丢失,那么会启动其的超时重传机制。

拥塞控制

1.拥塞:在某段时间,若对某一网络中的某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏。

2.拥塞控制的实现

首先,我们查看如下图:
在这里插入图片描述
其中:

  • ssthresh:表示的是一个门限值,最初值为cwnd的一半。
  • cwnd:网络开始发生拥塞的时候的窗口大小。

在tcp中,实现拥塞控制一共有四个算法,分别为:慢开始,拥塞避免,快重传和快恢复算法,而上图中用到的是慢开始和拥塞避免算法。

①:慢开始算法:这个算法就是0开始的,而ssthresh就是它的门限值,从图中可以看出来,开始的时候门限值为16,而窗口大小为1,所以在窗口大小小于ssthresh的时候,每次发送完数据,窗口的大小都会以2的指数倍的大小增长,直到窗口大小等于门限值或者大于门限值的大小后,便使用拥塞避免的算法。

②:拥塞避免算法:这个算法的窗口大小是从慢开始算法的窗口门限值开始的,使用这个算法的时候,窗口的大小是递增的(按照每次增加一个窗口的数量增加),直到增加到cwnd时。

问题:如何判断是否到了拥塞窗口最大值这个大小呢?

答:就是当窗口的大小于cwnd相等时,此时发送方发送的报文在发送过程中出现了丢失,启动了超时重传机制,此时发送方就知道了窗口的大小以及到最大值了,此时应该变小,所以就出现了图中后面的那一段。

③:后面的那一段的变化是这样的,此时它会让拥塞窗口的大小直接变为1,并设置新的门限值为发生拥塞时拥塞窗口大小的一半,然后还是按照上面所属的继续进行。

而快重传和快恢复我们会在下面进行讲解。
注意的是:慢开始算法对应的是拥塞避免算法,而快重传算法和快恢复算法是对应的。

面向字节流传输

字节流传输的特性

1.当创建一个TCP的socket,同时会在内核创建一个发送缓冲区接收缓冲区

  • 调用write时,数据首先会写入发送缓冲区。
  • 如果发送的数据太长,会被拆分为多个TCP包进行发送。
  • 如果发送的数据太短,会在发送缓冲区进行等待,等待到一定长度时,再进行发送。
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的缓冲区。
  • 然后应用程序可以调用read取读取数据。
  • 对于TCP连接,双方都有一个发送缓冲区和一个接收缓冲区,彼此都可以进行发送数据和接收数据(每一方都可以写数据也可也读数据),这也是全双工通信的概念。

2.由于缓冲区的存在,TCP程序的读和写不需要一一匹配:

  • 写100个字节数据时,可以调用一次write直接写完写100个字节,也可也调用100次write一次写一个字节。
  • 读100个字节数据时,既可以一次读取1个字节,也可也一次把所有字节读取完。

所以这样也产生了粘包问题。

粘包问题

由于字节流传输是非常灵活的,数据可用在缓冲区进行堆积,就是对传来的数据,接收方可用一个一个取数据,也可也一次性全部取完,这样也就出现了一个问题,那就是粘包问题。

1.粘包问题:有可能将多条数据当成一条数据进行处理了。

  • 首先要明确,粘包的包是指的是应用层的包。
  • 在TCP的协议头部,没有如同UDP中一样“报文长度”这样的字段,但是有一个序号这样的字段。
  • 站在传输层的角度,TCP是一个一个报文过来的,并按照好数据放在缓冲区中。
  • 站在应用层的角度,它看到的只是一连串的字节数据。
  • 那么应用层看的这些,他就完全不知道哪里是开始,哪里是结束,有可能将两段数据当成一段数据进行处理了。

总结:粘包问题的产生主要由于没有对数据边界进行划分。

2.解决方法(由于在传输层tcp对数据并不敏感,所以必须程序员在上层就对数据进行边界处理):

  • 以特殊字符作为数据的头部或者尾部。(就像http协议一样,通过使用\r\n进行分割,但是缺点就是万一原本的数据中就有该字符,就要对原本的数据进行转义操作)
  • 固定数据长度,如果传来的数据不够我们固定的长度,那么就进行补位。(缺点:就是万一传来的数据相对固定的长度非常小,那么补位的部分就很多。而传来的数据万一特别长,那么就会出现问题)
  • TLV数据格式,每个数据有个固定长度的应用层头部,头部中定义了数据中的长度,这样就可用根据该头部的内容,按照长度取出相应的数据。

注意:在UDP中是不存在粘包问题的,因为UDP在传输的时候,就是传输的是一个完整的报文,不存在粘包问题,要么整条拿走,要么就别拿。

传输性能挽救

在tcp协议中,为了实现可靠传输,用了很多方法,但是所有的一切都不是完美的,既然有了性能,那么就要损失效率,tcp协议亦然如此,但是既然不能避免,那么就应该减少伤害,所以就有了以下几种传输性能挽救的办法。

快速重传和快速恢复

快速重传就是一个传输性能挽救的方法,首先,我们先看下图:
在这里插入图片描述
假设此时两端已经建立好连接了,然后正在进行数据传输,如上图:

①:客户端给服务端发送数据的时候,会带了这个数据的seq(报文序号)还有这个报文的长度,而客户端接收到报文后,如果这个报文是正确送达的(未出现错误或者报在半路丢了的现象),都会给客户端发送一个确认序号(确认序号的大小就等于报序加上报的长度)。

②:上图所示的数据传输是这样的,客户端就给服务端发送数据,并且在没有等到服务端发送的确认信息就已经对第二个报进行了发送,然后服务端对接收的数据也会发送确认信号给客户端。

③:如果客户端出现丢包或者是报文错误的情况,客户端还是会照样按照自己的顺序继续发送报文,而此时服务端发现接收到的报文不是自己要的报文,它会先将该报文收到,然后对自己本来要接收到的报文序号发送给客户端,而客户端还是会继续按照自己的顺序去发送报文,而每次发送的时候个服务端的时候,服务端都会给客户端发送的是自己原本要接收的报文,并且把自己的接收到的报文正确接收;直到客户端收到了累计三个相同序号的报文的时候,客户端才会将该报文重发。此时服务端接收到该报文的时候,会给客户端发送的是自己已经累计接收的报文的下一个序列。

④:为什么要累计三次才会重传呢?

答:防止因为网络状态不好的原因,导致还没到的tcp报延迟到达。如果我们在接收到一次或者两次相同确认序号的时候,就去进行重传,就有可能出现接收方接收重复包裹的可能,这样的话,不仅降低了效率,还传送了重复的包裹。(主要也是为了不让发送方实现超时重传机制,因为实现了超时重传机制,那么发送方就知道了现在的网络出现了拥塞现象,此时的窗口已经是最大窗口,那么就开始重新开始慢开始算法,那么如果在没有达到拥塞的时候又开始使用慢开始算法,意味着此时的效率就又变低了)

快恢复算法:就是当发送方接收到接收方发送的三个累计重复确认的时候,发送方使用快重传算法,而不至于此时拥塞窗口的大小从1又开始,所以一旦使用了快重传算法,就要开始使用快回复算法,而不是使用慢开始算法:

  • 发送方将慢开始门限ssthresh值和拥塞窗口的cwnd值调整为当前窗口的一半,并开始执行拥塞避免算法。

注意:

  • 服务端给客户端发送的确认报文序号,是代表的自己发送的这个序号之前的报文都已经正确接收了,一旦有一个空缺,他就会一直发送的是这个空位置的序号,但是接收的不是该空缺位置的还是会照样接收。
  • 服务端接收是一个累计接收的方法:就是客户端给自己发送的报文,只要不是重复的报文,自己都会接收,不管是是否对应自己需要的这个序号。

延迟应答

这个主要是接收方对发送方发来的数据进行应答而出现问题的一个优化方法。

问题:如果接收方接收到数据就立即发送ACK应答,这时候返回的窗口就可能比较小了。

  • 假设接收端缓冲区为1M,一次收到了500k的数据,如果立即应答,那么返回窗口的大小就只有500k。
  • 但实际上,对于数据的处理其实非常的快,10ms之内就可用把500k的数据处理完成,
  • 在这种情况下,接收端的处理数据还没有达到自己的极限,即使窗口再大一些,也可也处理过来。
  • 如果接收端再等一会应答,比如等上个200ms,那么返回的窗口大小为1M。

注意:窗口越大,网络吞吐量就越大,传输效率就越高。而我们提高速率的目标就是保证网络不拥塞的情况下尽量提高传输效率。

问题:那么所有的报都可用延迟应答吗?

答:不可以,因为对于应答有数量的限制(每隔N个包就应答一次)和时间的限制(超过最大延迟时间就应答一次)。而具体的数量和时间在不同的操作系统中是不同的。一般情况下数量为2,时间为200ms。

捎带应答

在延迟应答的基础上,我们发现,在很多情况下客户端服务器在应用层上也是一发一收的,就是在应用层上对话一样,你说一句我回一句,这也对ACK确认报文就有一些优化了。

我们知道ACK确认报文其实就是一个空包头(确认序号等等),而空包头的传输会占用带宽等等一些影响传输效率的东西,如果这个时候接收方刚好要给发送方发送数据,那么就可用将其ACK确认报文一块捎带的发送过去,这也可用提高效率。

TCP异常情况

  • 进程终止:进程终止会释放文件描述符,仍然可用发送FIN包,所以和正常关闭没什么区别。
  • 机器重启:和进程终止情况相同。
  • 机器掉线/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在,就会释放套接字。(即使没有写入操作,也会有保活机制)

TCP小结

1.可靠性:

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

2.提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:定时器(重传定时器,保活定时器,TIME_WAIT定时器等等)

基于TCP应用层的协议

  • HTTP协议
  • HTTPS协议
  • SSH协议
  • Telnet协议
  • FTP协议
  • SMTP协议

当然,也包含程序员自己在应用层所实现的基于TCP的程序。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值