朋友们、伙计们,我们又见面了,本期来给大家带来TCP协议相关的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. TCP协议
TCP是传输层协议,TCP全称为 "传输控制协议(Transmission Control Protocol")。人如其名,要对数据的传输进行一个详细的控制;
1.1 TCP报文格式
通过TCP报文的格式可以看出相比较于UDP报文就复杂太多了,但是对于前面两个字段我们并不陌生,源端口和目的端口;
另外对于报头中的选项,他是可选字段;
对于其他字段后面依次慢慢了解;
1.2 4位首部长度
我们在学习一个协议的时候,需要搞清楚两个问题:
① 报头与有效载荷如何分离;
② 有效载荷如何向上交付;
对于TCP报文来说,TCP的报头如果不算上选项,那么总共就是20字节大小,也叫TCP做标准报头大小;但是因为选项字段是可选字段,所以就导致了TCP报头的大小不固定,因此我们就需要通过4位首部长度来辨别,4位首部长度就是TCP报头字段的大小(包含选项)。
① 4位首部长度从字面上看只有四位,初步估算取值只有0~15字节,但是标准的TCP报头长度都要20字节,所以就规定了基本单位是4字节,所以4位首部长度的取值是0~60字节;
② 所以也可以说TCP报头的长度一定是4的倍数;
③ 如果没有数据的情况下,选项的长度最长就是40字节;
④ 那么有了4位首部长度,如何进行报头与有效载荷的分离呢?
- 当拿到一个TCP报文时,先按照标准TCP报头大小读取20字节,拿到4位首部长度,根据4位首部长度的大小减去标准报头大小20字节,拿到的就是选项的长度,有了标准报头长度,有了选项长度,那么剩下的就只有有效载荷了。
TCP这里对于有效载荷的向上交付问题与UDP一样,都是通过源端口和目的端口来向上交付的;
关于TCP报文的封装过程和UDP是一样的,都是先创建描述TCP报头的结构体字段,然后填充,OS内部也有管理TCP报文的机制,通过拷贝数据等方法对TCP报文进行封装,具体的过程可以参考UDP报文的封装过程;
1.3 流量控制
TCP具有发送和接受缓冲区,它是一种全双工的通信协议,通信双方的地位是对等的;
当我们调用send/write发送数据时,其实并不是将数据发送到网络,而是将数据拷贝到OS内部;
那么此时就存在一种情况,发送方发送的太快,一直send/write,然而接收方来不接接受呢?
那么简单粗暴的做法也就是将来不及接收的报文丢弃,其实丢弃也没问题,因为接收方已经来不及接收了,但是不合理,发送方发送的报文千里迢迢的从网络中跑过来直接被丢弃吗?当然是不可以,太不合理了;那么正确的方法就是:如果接收方来不及接受了,那么发送方你也就不要发送了,先等一等,等接收方又能力接受了,你再发送,这种控制手段就叫做流量控制。
流量控制是由发送方的TCP协议做的,也就是OS中的TCP模块做的,用户不用关心,用户只需要将数据拷贝到缓冲区,完全不需要担心什么时候发,发多少,出错了怎么办;
其实这里就很像我们的文件系统,我们调用文件接口时,向文件写入数据时,也不就是像这样子嘛,我们只调用接口传入数据,并不管什么时候把数据写入磁盘,写多少,出错了怎么办,这些都是由OS去做的事情;
1.3.1 如何进行流量控制
上面说到流量控制是当对方没有能力接收了,发送方就不要发送了;
所以如何做到流量控制,其实就是发送方需要知道接收方的接收能力;
那么该如何知道呢?接下来先粗略的了解一下TCP的确认应答机制;
当有一方给另一方发送消息之后,另一方收到了消息还要返回给对方一个确认收到的信息;
需要注意的是,在这个过程中,双方发送不只是简单的数据,而是一个完整的TCP报文,数据就是报文的有效载荷;TCP协议中把应答的字段叫做ACK;
其中TCP报头中的16位窗口大小就是用来进行流量控制的;
1.3.2 16位窗口大小
16位窗口大小字段是用来进行流量控制的;
表示的就是接收缓冲区剩余空间的大小;
16位窗口大小也用于后面的所要讲的滑动窗口;
1.4 确认应答
因为TCP有确认应答机制,举个例子:
发送方A向接收方B发送一个消息,B都要进行确认,并返回给A我已经收到了你的消息了,那么B发的这个消息A是否收到B不知道,只有当A向B发送了确认消息,B才知道A收到了,但是A还是不知道自己最后的确认消息B收到了没有呀?B又要发,这样子就陷入了一个死循环,目前根据我们对TCP协议的了解,因为最新的一条消息没有应答,所以并不知道消息是否可靠的发送给对方,但是已经收到应答了,那么就保证之前的消息绝对可靠的发送给了对方:
所以只要能收到应答,就能保证历史上最近发送的一条数据是100%被对方收到了,这就是TCP协议的确认应答的机制。
1.5 TCP发送数据模式
① 串行发送:
就比如客户端要和服务器进行通信,客户端先给服务器发送一个TCP报文(包含数据),然后在下次发送数据之前,必须要收到客户端的应答才能继续发下一条消息,这样子就保证了客户端到服务器通信的可靠性;
反过来服务器向客户端发送消息,必须要等客户端进行应答之后才能发送下一条消息,这样子就保证了从服务器到客户端通信的可靠性;
串行发送消息存在效率问题,发一条消息,接收一条应答,效率太慢了。
② 并行发送:
如果通信方式变成并行,将发送时间进行重叠,那么当客户端向服务器发送消息时,一次性发送好多条,同样的,服务器应答时也要应答,客户端需要同时接收到多条应答,这样子既解决了效率问题,又保证了从客户端到服务器通信的可靠性;
反过来,服务器向客户端发送消息时也是同时发送多条消息,接收应答时也是同时接收多条应答,既提高了效率,也保证了从服务器到客户端通信的可靠性。
但是这样做存在一个问题,报文收到的顺序和发送的顺序完全一致吗?当时是不一致;
所以就需要用到TCP报头中的32位序号和32位确认序号字段;
1.6 32位序号和32位确认序号
通过上面的TCP发送数据的模式了解到了TCP常用的发送数据方式是并行发送,重叠了多条消息的发送时间来提高效率,但是存在报文发送和接收顺序不一致的问题,所以就需要通过32位序号和32位确认序号来解决这个问题。
① 32位序号:
首先需要了解顺序不一致会导致什么问题?
在进行通信的时候发送的报文不一定只有数据,如果是数据和应答或者其他报文掺杂在一块一起发送,那么接收方接收到的报文顺序不一致,那么就会导致数据异常,就比如在发送的时候先发送的是一个完整报文的前一段,然后发送的后一段,那么在接收方就会乱序,前后位置如果颠倒就导致接收方收到一个异常数据,所以乱序是一种不可靠的通信;
所以在TCP报头中的32位序号就可以将发送时的数据进行编号排序,那么接受方收到多个TCP报文时,对其中的报头字段中的序号进行解析,然后对收到的报文进行排序就可以实现一种有序切可靠的通信了;所以32位序号是为了保证报文的有序到达!
② 32位确认序号:
32位序号解决了数据报文的按序到达,那么接收方对于数据的应答需不需要在发送方按序到达呢?肯定是需要的,如果应答没有顺序,其中有一个应答丢失了,我们如何知道是哪个报文丢失了呢?所以32位确认序号就是保证应答报文的按序到达;
所以在TCP报头中的32位确认序号用于对应答进行排序,假设数据报文的序号是100,那么在应答报文中的确认序号就是101,这样子对于数据和应答就一一对应起来了,实际上确认序号的含义是:之前所有的报文都已经收到了,就比如确认序号是301,那么就表示前面的报文序号为100、200、300的报文都已经收到了,这样做的目的就是可以允许少量的应答丢失;
1.6.1 为什么要有一对序号?
为什么在TCP报头中要有一对序号(序号和确认序号),为什么不直接将两个合并为一个,发的是消息就是序号,如果是应答就是确认序号呢?
因为在上面演示的过程中只是一方向另一方发送,一方接收,那么如果双方同时进行通信的话,就意味着序号和确认序号要被同时使用,所以一个报头中需要有一对序号(序号和确认序号);
1.6.2 捎带应答
紧接着上面的话题,双方同时进行通信,所以需要同时使用序号和确认序号;
因为在TCP这里双方通信发送的都是TCP报文,是包含完整的报头字段的,所以在有一方向另一方发送消息时,可能还需要对另一方进行应答,所以干脆把这两件事情合并在一起,即向对方发送消息,又向对方应答,这种机制就叫做TCP的捎带应答;其实就是在向对方发送消息的时候,顺手就进行应答了(应答就是将标志位的ACK置为1);这也是TCP用来提高效率的一种方式,因此:大部分通信,既是数据又是对历史报文的确认;
是数据就要又序号,是应答就要有确认序号,所以需要一对序号;
序号是表示自己发送的报文的序号,是自己的;
确认序号是用来给对方的报文确认的,所以是别人的;
TCP不仅会保证可靠性,还会进行各种提高效率的机制!就比如现在已经了解到的捎带应答;上面这些工作都是由OS内核完成的(因为TCP在内核中),不需要用户去管;
1.7 6个标记位
6个标记位其实就是6个比特位,前面的确认应答机制就是将标志位ACK置1,如果只是单纯的应答,那么就只有报头字段,其中标记位是ACK;
在普遍的场景中,同时可能存在多个客户端要和服务器进行通信,所以也面临着服务器会收到许多许多的TCP报文,那么这么多的报文怎么区分呢?就是通过标记位来区分,所以标记位就可以代表报文的类型。
在这6个标记位中,其中ACK就是表示的是应答报文,SYN是TCP三次握手时建立连接的报文,FIN是TCP四次挥手时要断开连接的报文,这三个字段比较好理解,后面再说;
这里先简单的理解,后面会具体细数三次握手和四次挥手;
1.7.1 URG标记位和16位紧急指针
通过序号可以让报文按序到达,按序处理,所以就需要排队处理,那么如果对方发送了一个紧急报文呢?那么还需要排队吗?肯定是不能进行排队的,需要提前处理,进行插队;
所以就有了URG标记位,当报文的URG标记位被置1,那么就表示该报文需要进行优先处理,那么报文中需要紧急处理的数据在哪里呢?
所以还需要16位紧急指针,16位紧急指针用来保存紧急数据在有效载荷中的偏移量(只有一个字节);
紧急任务就比如终止上传或者暂停上传等等;
1.7.2 PSH标记位
TCP具有流量控制,所以当接收方的接受能力为0时,根据16位窗口大小发送方也就知道了,就不能发送数据了,需要等待,那么在等待的过程中,发送发就会定期给对方发送询问报文,TCP需要保证可靠性,所以询问报文也需要有应答,应答报文的报头字段的窗口大小就表示了接收方的接收缓冲区剩余空间大小,如果不为0,那么就可以发送数据了,为0就继续等待;
另外,如果接收方的接收缓冲区有了空间了,接收方也会给发送方发送更新窗口大小的报文;
PSH标记位表示的是:告诉对方,需要尽快的把数据向上交付,把缓冲区赶快腾出空间;
回到上面的发送询问报文:在发送方发送询问报文时,询问报文中的PSH标记位被置1,就表示告诉接收方,赶快把你的缓冲区的数据向应用层交付;
PSH标记位适用的场景不知上面这种情况,在所有数据需要被尽快交付的场景都适用;
1.7.3 RST标记位
TCP通信之前必须进行三次握手,TCP是保证可靠性的,三次握手必须成功吗?肯定是不一定成功,也可能失败,TCP的可靠性不是保证数据100%的发送到,而是对方需要知道,发送出错了也需要知道,所以三次握手可能存在失败的情况;
三次握手其实是不担心前两个报文丢失,因为前两个报文都有应答,没有收到应答就表示丢失了,收到应答就表示成功了,但是最后一次的报文没有应答;
如果客户端向服务器通信时,前两次报文都成功了,第三次报文对于客户端来说只要客户端发送成功客户端就认为连接建立好了,对于服务器来说只有当第三次报文收到之后才认为连接建立好了,那么如果第三次报文丢失了呢(发送过程中丢失)?
但是客户端已经成功发出去了,已经认为连接建立好了,但是由于在发送过程中丢失了,服务器没有收到,服务器认为连接还没有建立好,导致连接建立不一致,客户端认为建立成功,所以当客户端给服务器发送数据时,服务器就会给客户端返回一个RST的报文,表示的就是让客户端重置连接;
TCP三次握手其实就是为了在赌最后一个报文对方可以成功收到,即使收不到也有RST报文对连接进行重置;
1.8 序号和缓冲区
在报头中的序号和确认序号都是怎么规定的呢?
结合发送缓冲区,我们可以把发送缓冲区当做一个char类型的数组,既然是数组,就表示了数组可以使用下标进行访问,假设发送方要发送4个字节,那么下标从0~3,所以序号就是3,接收方接收到了数据在给发送方应答时的确认序号就是3 + 1 = 4,所以就表示了发送方下次发送时从下标为4的位置开始发,所以序号可以从数组下标中得来;
1.8.1 面向字节流的理解
既然发送缓冲区可以当做char类型的数组,接收缓冲区也可以当做char类型的数组,因为是数组,可以通过下标访问,所以在上层来读取的时候看到的就是一个一个的字节,这就是面向字节;
TCP报文在被对方拿到之后,对方只会将报头和有效载荷分离,然后把有效载荷拷贝到接收缓冲区,在接收缓冲区中可能存在历史上的多个有效载荷,这些数据是没有边界的,需要用户自己去区分,这个接收缓冲区其实就是一个生产者消费者模型,有人拿就有人放,体现的就是一种流动的状态,所以这就叫做面向字节流;
每一对TCP都有发送和接收缓冲区!
1.9 超时重传
超时重传:在报文丢了之后,会在特定的时间间隔进行重新发送;
超时重传可能存在两种情况:
①
- 主机A给主机B发送数据,可能由于网络堵塞等原因,数据无法到达B主机;
- 主机A在特定的时间间隔没有收到B主机的应答,就会对数据进行重发。
②
- 主机A给主机B发送数据,B主机也收到了来自A主机的数据;
- 但是主机B给主机A的应答报文丢失了,所以主机A还是要对数据进行重发。
对于主机A来说,上面的两种情况都需要进行重发,所以也就意味着主机B会收到多个重复的数据,所以就需要对收到的数据进行去重,此时就可以用到之前的32位序号来进行去重;
超时的时间如何确定:
因为网络状态是变化的,发消息到收到消息有一定的延迟,所以超时重发的这个超时时间是随网络浮动的,如果超时时间太久则会影响效率,超时时间太短则过于频繁,另外如果超时重传达到了一定次数对方还没有应答,那么就会关闭连接。
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间:
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍;
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传;
- 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增;
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
对于发送方来说,发送方一旦把数据发送出去之后,一段时间内,已经发送的数据是不能被移走的,而是要暂时保存起来,为了超时重传;那么保存在哪里呢?其实是保存在滑动窗口中的(后面再说)。
1.10 连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接;
服务器一定允许多个客户端与他建立连接,也就意味着服务器上有多个已经完成三次握手的连接,所以OS也要把这些连接管理起来,那么先描述、再组织;
在建立连接或者是在断开连接的过程中,连接状态的变化其实就是对应的字段被各种赋值;
1.10.1 三次握手
三次握手是TCP在进行通信之前必须要做的事情,目的就是为了确保通信双方接收和发送数据的可靠性:
在三次握手的过程中,双方发送的都是完整的不带数据的TCP报文,在三次握手的时候会对初始随机序号进行交互,还会对双方的窗口大小进行交换,另外以及对滑动窗口的确认;
发送的SYN、ACK其实就是TCP报头字段中的标记位被置为1;
1.10.1.1 为什么是三次握手
① 假设是一次握手,那么就意味着有任何一方向对象只发起SYN即可完成连接建立,因为在建立连接时,有一方发起连接,另一方就得无条件答应建立连接,在上面我们说到过,建立好的连接是需要被OS维护的,维护是需要成本的(时间 + 空间)如果有一个恶意用户一直发起SYN,一直建立连接,这就导致OS需要维护好多的连接,不断的占用OS的资源,这种情况叫做“SYN洪水”;
② 如果只有两次握手,其实也存在SYN洪水的问题,但其实主要的问题在于,两次握手是对发送方的通信可靠性的保证,既可以发数据,也可以收数据,但是对于接收方呢?接收方只能通过第一次握手验证可以收数据,那么发出去的数据没有应答,到底能不能发数据呢?因为TCP是全双工的,所以需要保证通信双方的接收能力;
一次和两次握手会存在一些单机程序恶意向服务器发起连接,消耗服务器资源;
③ 三次握手其实也存在SYN洪水的问题,但是三次握手可以确保通信双方的接收能力,也就是以最小的成本来验证全双工,前两次握手验证了发送方的发送和接收能力,后两次验证了接收方的发送和接收能力;
④ 如果三次握手上面的不好理解,那么其实可以将三次握手转化为四次握手来理解:
把第二次握手的ACK和SYN拆分成两次,那么前两次握手保证的是发送方的通信能力,后两次握手保证的是接收方的通信能力;
所以三次握手本质上是四次握手,其中三次握手中的第二次握手本质上就是一次捎带应答,四次握手可以100%确认双方都已经收到了建立连接的请求,另外也可以100%确认对方没有收到,从而也就保证了可靠性;
总结:
- ① 三次握手为了保证通信信道是否通畅,以最小的成本验证全双工,双方既可以发送也可以接收;
- ② 奇数次握手,肯定是发送方先把连接建立好,接收方才建立连接,双方维护连接都是需要成本的;
- ③ 三次握手本质上是四次握手 + 一次捎带应答,四次握手可以100%确认双方都收到了建立连接的请求。
1.10.2 四次挥手
四次挥手的目的就是为了断开连接,断开连接与建立不同,断开连接时是需要双方都同意,因为TCP中双方的地位是对等的;
关于四次挥手中进行协商的这些报头中的标志位前面已经提到了,在四次挥手这里主要来了解四次挥手过程中双方的状态变化;
前面说过三次握手其实就是四次握手加一次捎带应答,那么断开连接时为什么是四次挥手呢?将FIN和ACK合并在一起进行捎带应答可以吗?
当然是可以的,但是大多数情况并不这么做,因为在三次握手建立连接的时候,双方还没有进行数据的交互,但是连接已经建立好了,肯定是要进行数据的交互的,那么当客户端向服务器断开连接时,发送断开连接的FIN请求,这里表示的是客户端给服务器不再发消息了,但是别忘了服务器很有可能还要给客户端发消息,如果采用的是捎带应答的情况,服务器给客户端的消息还没有响应完,那么双方连接直接关闭,这不就出问题了嘛;所以在双方断开连接的时候,大多数情况都是四次挥手;
1.10.2.1 四次挥手过程中状态变化
被动断开连接的一方在确认对方要断开连接之后,自己进入了CLOSE_WAIT状态;
主动断开连接的一方最终要进入TIME_WAIT状态,一段时间之后进入CLOSED状态。
当客户端在两次挥手之后已经关闭连接了,此时服务器给客户端发的消息客户端怎么应答呢?不是都已经把连接关了吗?
因为TCP在OS内核,所以这些工作由操作系统自动帮我们做了,应用层完全不用担心!
在之前我们所写的TCP套接字那里,当我们将TCP服务器启动之后,用客户端连接之后,然后直接将服务器终止掉,再次重新启动服务器时,就会发现启动失败,然后我们在绑定之前添加了这样两行代码:
int opt = 1; setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
这里也就表示了当主动断开连接的一方,会进入TIME_WAIT状态,表示的就是此时他的资源还没有完全释放,所使用的套接字地址和端口仍然在被使用,当在重新启动的时候就会绑定失败;
所以我们添加了上面这两行代码,表示的就是为了对地址和端口进行复用;
1.10.2.2 CLOSE_WAIT状态
当客户端把连接关闭之后,服务器一直不关闭连接,连接服务器的用户越来越多,就会导致在网络应用的时候服务器越来越卡,系统中会存在大量的CLOSE_WAIT状态的连接;这样做就会导致在一个TCP通信中,四次挥手没有完成,连接一直存在,所以在客户端关闭连接之后,服务器处理完消息也要将连接关闭,完成四次挥手的整个过程。
1.10.2.3 TIME_WAIT状态
存在的意义:
- 双方在进行通信的时候,不是一方把数据只要发送对方就立马收到了,是需要经过网络中多个路由器进行转发的,所以是有延迟的;
- 那么,当主动断开连接的一方没有进入TIME_WAIT状态,而知直接进入CLOSED状态,那么此时网络中很有可能还有对方给他发送的报文(历史报文),所以这些报文就丢失了,另外还有一种情况,主动断开连接的一方断开之后立马重连,因为网络中还存在历史报文,那么就会导致上一次通信的历史报文在新的一次连接中传递过来了,这就会造成数据混乱的现象;
- 所以在主动断开连接的一方需要进入TIME_WAIT状态,等待历史报文从网络中消散,再进入CLOSED状态;
这里就存在一个问题,假设上面的情况发生,新的通信拿到了旧历史报文,那么数据混乱怎么办?所以TCP在序号这里采用的是随机序号,那么关于序号的理解其实就是在随机序号上继续加上下标即可,那么对方怎么知道你的序号呢?所以在TCP三次握手的时候,发送的SYN和ACK不仅仅是个字段,而已是一个完整的TCP报头,报头字段中的序号就可以给双方知道,所以TCP在三次握手的时候,随机起始序号需要通知给对方,这就叫做起始序号的协商。
1.10.2.4 TIME_WAIT时间
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
- MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK)
- 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值:
- 注意这里的MSL时间不是超时重传的时间;
1.11 滑动窗口
在前面我们说过TCP发送数据的模式:假如是串行,发一个数据收到一个应答再发下一个数据,这样做也可以,但是效率太慢了;
所以就有了并行发送数据的方式,同时发送一批数据,对应的也要同时收到一批应答,这种方式大大提高了效率;
其实这种并行的发送数据方法就是使用了滑动窗口;
所以滑动窗口就是TCP发送大量暂时不要应答的报文,从而提高发送效率的一种解决方案;
1.11.1 滑动窗口在哪里
TCP通信双方都有接收缓冲区和发送缓冲区,滑动窗口其实就是TCP发送缓冲区中的一个区域,通过滑动窗口把发送缓冲区划分为了三个部分,滑动窗口左边的是已发送、已确认的数据,滑动窗口里面的是可以直接发送的数据且尚未确认的数据,滑动窗口右边的是未发生数据或者直接没有数据;
1.11.2 理解滑动窗口
前面说到过可以把发送缓冲区看做一个char类型的数组,滑动窗口既然在发送缓冲区中,所以可以用下标来控制滑动窗口的大小以及移动;
所以滑动窗口的大小可以用两个下标来控制一段区域:起始下标win_start,结束下标win_end;所谓的窗口的滑动其实就是下标的移动;
滑动窗口的大小由谁来决定呢?目前来看其实是由对方的接收缓冲区接收能力来决定(剩余空间大小);
因为对方的接收能力可能随时在变化,那么滑动窗口的大小怎么更新呢?
根据对方的应答报头中的32位确认序号和16位窗口大小来更新:
那么在最开始还没有发数据的时候,滑动窗口大小该怎么确定呢?
不要忘记了,在通信之前需要三次握手,三次握手过程中就会对滑动窗口大小进行协商,所以三次握手完成了滑动窗口的大小就已经协商好了;
1.11.3 滑动窗口大小变化
① 滑动窗口变小:
比如发送方一直发送数据,接收方一直接收,但是上层一直不拿数据,所以接收方的缓冲区会变小,所以给发送方的应答报头中的16位窗口大小就变小了,所以发送方会根据16位窗口变小,从而调小滑动窗口大小;
② 滑动窗口变为0:
当对方的接收缓冲区已经完全接收不来了,所以应答报文的16位窗口大小也就是0,所以根据更新滑动窗口的机制,win_start和win_end重合,所以滑动窗口的大小就变成0了;
③ 滑动窗口变大:
当对方上层把接收缓冲区的数据拿走的时候,空间腾出来了,所以发送方根据应答报文的16位窗口大小也会把滑动窗口的大小变大;
所以滑动窗口也是一种流量控制的手段,对方接收能力强,就多发点,接收能力弱,就少发点;
1.11.4 滑动窗口的移动方向
- 根据滑动窗口把发送缓冲区的划分,滑动窗口左边的区域是已经发送的数据,并且也已经收到了应答,所以这就意味着滑动窗口只能向右滑动,不能倒着向左滑;
- 那么也可能存在滑动窗口越界的问题,就是从缓冲区滑出去了,其实TCP在这里做了好多控制的方法,我们只要将发送缓冲区想象成一个环形结构即可;
1.11.5 丢包了如何处理
把丢包可以分为三种:
- ① 滑动窗口最左边的丢失
- ② 滑动窗口中间的丢失
- ③ 滑动窗口最右边的丢失
① 最左边的报文丢失:
假设客户端给服务器发送了5个报文,序号分别是2000,3000,4000,5000,6000,其中序号为2000的报文丢失了,其余的报文都收到了,此时根据确认序号的定义(该序号之前的数据全部都收到了),所以对于后面几个报文的应答中的确认序号肯定是1001,那么当客户端连续收到后面几个报文的应答之后,发现确认序号都是1001,所以就会立即对对应的报文进行补发,那么当发送方连续收到三个以上相同的确认序号时就会出发快重传机制对报文立即补发;
② 中间的报文丢失:
由于滑动窗口是可以滑动的,当数据发送且确认之后,滑动窗口就会滑走,所以如果中间报文丢失了,那么就证明前面的报文都收到了并且也有了应答,所以滑动窗口可以向右滑动,从而也就转化成了最左边报文丢失的情况了;
③ 最右边的报文丢失:
当最右边的报文丢失,也就意味着前面的报文发送且有应答,所以窗口滑动,同样的也可以转化为最左边报文丢失的情况。
④ 应答丢失
因为确认序号的定义,所以应答丢失了可以通过确认序号来知道是哪个应答丢失了;正因为确认序号的存在,所以也允许少量应答可以丢失;
所以这也就解释了为什么发送方历史发出去的数据不能移除,得先保存起来,所以历史的数据就保存在了滑动窗口中,如果没有收到应答,还可以进行快重传或者超时重传,只有收到了应答,滑动窗口滑动,删除指定的报文即可;
1.11.6 快重传和超时重传
快重传既然速度快,那么为什么还要有超时重传呢?
- 首先,这二者并不干扰彼此,另外快重传是有前提条件的;
- 只有当接收方连续收到三个以上同样的确认序号时,才会触发快重传;
- 超时重传是兜底的作用,快重传是提高效率的策略,二者相互配合。
1.11.7 补充流量控制
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。
16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M 位;
1.12 拥塞控制
TCP保证了可靠性,另外还有一些提高传输效率的策略,其实TCP的可靠性还考虑了网络的情况;
当我们正常通信的时候,如果发送的报文少量的丢包了,可以进行超时重传,但是如果出现大面积丢包了,就应该考虑相关网络出现了问题了,此时就不要再超时重传了,这是为什么呢?
因为如果出现大面积的丢包,此时的网络情况肯定不容乐观,网络中出现了大量的拥塞情况,如果继续重传,那么网络中不止一台主机,存在很多台主机,这么多的主机同时向本就拥塞的网络中继续重传,那只能是雪上加霜;
1.12.1 慢启动机制
TCP不仅考虑了接收方的接收能力、发送方的发送策略,还考虑了网络健康状态,当出现大面积丢包的时候,就表示已经出现了网络拥塞,所以已经网络拥塞时,采用慢启动机制、执行拥塞避免算法;
先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
拥塞避免的算法是以指数级增长模式:
1.12.2 拥塞窗口
- 前面学习的滑动窗口只考虑了接收方的接收能力;
- 但是数据要能到达对方,首先得发送至网络,再由网络进行转发,所以网络也
是决定发送数据量的多少的重要因素;
- 拥塞窗口其实是发送方定义的一个数字,数据量超过拥塞窗口大小时,有很大概率就会导致网络拥塞,所以发送方不仅要考虑接收方的接收能力,还要考虑网络情况;
- 滑动窗口是决定单词发送数据量的多少,所以滑动窗口的大小需要由网络和对方接收能力共同决定;
- 滑动窗口的大小 = min(拥塞窗口大小,对方窗口大小);
- win_start = 32位确认序号; win_end = min(拥塞窗口大小,对方窗口大小);
- 所以我们不用担心按照
一直增长时对方来不及接收。
指数级增长的方式是:前期慢,后期快(
),这种也符合我们的拥塞避免算法,刚开始发少量的报文去探测,中后期尽快进行正常的通信;
1.12.3 慢启动阈值
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍;
此处引入一个叫做慢启动的阈值;
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
我们假设接收方的接收能力无限,当发生网络拥塞时,进行拥塞避免的算法,刚开始先发少量的,然后随着发送次数的增长,拥塞窗口以
进行增长,当增长到了慢启动阈值(16)的时候,就从指数级增长变成线性增长,一次加一个,那么直到增加到出现网络拥塞的最大拥塞窗口时(24),然后更新新的慢启动阈值(发生网络拥塞的拥塞窗口大小的一半 -> 12)然后将拥塞窗口置为1,重新进行网络探测,依次类推,周期性的对网络健康状态进行探测。
- 拥塞控制是通过拥塞窗口和慢启动阈值来搭配控制的;
- 因为网络状态一直在变化,所以拥塞窗口大小要一直变,周期性的进行探测;
- 少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
- 当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
- 拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
1.13 延迟应答
延迟应答也是TCP提高效率的一种策略;
当接收方接收到数据时,先不着急应答,先等一会,那么在这个等的过程中,接收方上层就有概率把数据拿走,这样子就可以给发送方应答一个更大的窗口大小,对方可以发送更多的数据。
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
延迟的策略:
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间(200ms)就应答一次;
1.14 缓冲区
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时,数据会先写入发送缓冲区中;
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配:
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;
1.15 粘包问题
首先说明,这里的粘包不是TCP的包,因为TCP发送是有完整的报头字段,可以将报头与有效载荷分离,这里的包指的是应用层拿到的数据;
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中;
- 站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包;
- 因为TCP是面向字节流的,所以出现了数据粘包的问题;
如何避免粘包问题呢?就是要明确两个包之间的边界:
- ① 定长的包:保证每次读取都按照固定的长度进行读取;
- ② 变长的包:可以在头部位置约定一个数据包的总长度,从而就知道包的结束位置;
- ③ 变长的包:还可以在包与包之间添加一些特殊字符进行分割(自己定义,只要保证包与包之间不冲突)。
在之前的自定义协议的文章中已经做过类似的处理了:https://blog.csdn.net/Yikefore/article/details/146280807?spm=1001.2014.3001.5501
UDP会出现这中粘包问题吗?
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在;同时,UDP是一个一个把数据交付给应用层;本身就有很明确的数据边界。
- 站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收; 不会出现"半个"的情况。
1.16 TCP连接异常情况
- ① 进程终止了:进程启动是有自己的文件描述符表,套接字本质上也是文件描述符,进程异常终止了,所维护的文件描述符表也就释放了,所以本质上还是关闭连接,只不过由双方的OS自动断开连接;
- ② 机器重启:大部分情况我们关闭机器时都会有提示,提示用户还有未关闭的进程,是否先要关闭进程再重启,所以也可以正常关闭连接;
- ③ 机器断网:因为接收方不知道对方断网了,所以接收方还认为连接存在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,重置连接;即使没有写入操作,TCP自己也内置了一个保活定时器,定期询问对方是否还在,如果对方不在,OS也会把连接释放掉;
- ④ 另外,应用层的某些协议,也有一些这样的检测机制,例如HTTP长连接中,也会定期检测对方的状态,在应用层这种检测机制叫做心跳机制。
2. TCP总结
TCP是面向字节流的,不管怎么发送,一次发多少,什么时候发,只管把数据交给对方;
TCP在通信前需要建立连接,三次握手,在断开连接时需要四次挥手;
TCP有序号、确认序号、确认应答、超时重传、流量控制、拥塞控制来保证可靠性;
TCP有滑动窗口、快重传、捎带应答、延迟应答来提高效率。
2.1 基于TCP的应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
另外还有我们自己基于TCP实现应用层的自定义协议;
3. listen的第二个参数
在当时写TCP套接字时,是直接使用的,也没有说为什么这么用,其实他是TCP特有的一个全连接队列;
当我们将这个参数设置为1,那么如果我们用三个机器连接我们的服务器时,前两个机器都可以成功连接,进入ESTABLISHED状态,但是第三个机器只会等待,不会连接成功,进入SYN_RECV状态;
这是因为Linux内核协议栈为一个TCP连接管理使用两个队列:
1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
2. 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响;
全连接队列满了的时候,就无法继续让当前连接的状态进入 established 状态了;
这个队列的长度是 listen 的第二个参数 + 1
全连接队列不能太长;
太长所需要维护的成本太高,不值得;