传输层协议UDP和TCP

传输层

负责数据能够从发送端传输接收端.

再谈端⼝号

端⼝号(Port)标识了⼀个主机上进⾏通信的不同的应⽤程序;

在TCP/IP协议中, ⽤ "源IP", "源端⼝号", "⽬的IP", "⽬的端⼝号", "协议号" 这样⼀个五元组来标识⼀个通信(可以通过netstat -n查看);

端⼝号范围划分

0 - 1023: 知名端⼝号, HTTP, FTP, SSH等这些⼴为使⽤的应⽤层协议, 他们的端⼝号都是固定的.

1024 - 65535: 操作系统动态分配的端⼝号. 客⼾端程序的端⼝号, 就是由操作系统从这个范围分配

的.

认识知名端⼝号(Well-Know Port Number)

有些服务器是⾮常常⽤的, 为了使⽤⽅便, ⼈们约定⼀些常⽤的服务器, 都是⽤以下这些固定的端⼝号:

ssh服务器, 使⽤22端⼝

ftp服务器, 使⽤21端⼝

telnet服务器, 使⽤23端⼝

http服务器, 使⽤80端⼝

https服务器, 使⽤443

cat /etc/services通过这个命令也能查我们的知名的端口号。

两个问题

  1. ⼀个进程是否可以bind多个端⼝号?
    可以的。
  2. ⼀个端⼝号是否可以被多个进程bind?
    不可以,端口号都是唯一的。

UDP协议

UDP协议端格式

协议报头,协议就是结构化的字段。

这就是我们内核中的结构化字段。

报头和有效载荷分离。

报头(Header)管「规则、控制、说明」 有效载荷(Body / 载荷)管「真实数据、内容本体」 强行分开,是为了:灵活、通用、易解析、好扩展、不乱套。

可以使用上个博客的理解,就是我们的报头就是我们空白行上面的内容,有效载荷就是我们的响应正文,发送给我们客户端的有用的信息。

我们UDP如何分离呢??

因为我们的报头长度是固定八个字节。

分用问题:我们如何交给上层如何返回呢??

我们不是有目的端口号吗,就可以交给上层,返回就有源端口号就可以返回了,为什么UDP是面对数据报的呢??

udp长度是发送方udp层填写的,报文的总长度,如果你读取的报文的总长度不够八个字节,你这个就是错误的,如果超过八个字节,你减去8就表示有效载荷的长度了,udp不需要考虑粘报问题。

⾯向数据报

应⽤层交给UDP多⻓的报⽂, UDP原样发送, 既不会拆分, 也不会合并;

⽤UDP传输100个字节的数据:

如果发送端调⽤⼀次sendto, 发送100个字节, 那么接收端也必须调⽤对应的⼀次recvfrom, 接收

100个字节; ⽽不能循环调⽤10次recvfrom, 每次接收10个字节;

发送次数和接受次数一样叫做数据报。

UDP的缓冲区

UDP没有真正意义上的 发送缓冲区. 调⽤sendto会直接交给内核, 由内核将数据传给⽹络层协议进

⾏后续的传输动作;因为自己一次全部发送过去了,又不保证可靠性,数据丢不丢和我没关系,我没必要建立一个缓冲区专门存放这块数据。

UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序⼀

致; 此时来不及处理就先放到缓冲区中;如果缓冲区满了, 再到达的UDP数据就会被丢弃;

UDP的socket既能读, 也能写, 这个概念叫做 全双⼯

但是TCP是需要的,因为TCP是面向字节流的,你一次可能发不完,所以需要多次发送,又因为要保证可靠性,一次发送失败重发即可。

UDP使⽤注意事项

我们注意到, UDP协议⾸部中有⼀个16位的最⼤⻓度. 也就是说⼀个UDP能传输的数据最⼤⻓度是

64K(包含UDP⾸部).

然⽽64K在当今的互联⽹环境下, 是⼀个⾮常⼩的数字.

如果我们需要传输的数据超过64K, 就需要在应⽤层⼿动的分包, 多次发送, 并在接收端⼿动拼装;

基于UDP的应⽤层协议

NFS: ⽹络⽂件系统

TFTP: 简单⽂件传输协议

DHCP: 动态主机配置协议

BOOTP: 启动协议(⽤于⽆盘设备启动)

DNS: 域名解析协议

当然, 也包括你⾃⼰写UDP程序时⾃定义的应⽤层协议;

UDP是很简单的。

我们把报文重新详细认识一下。

我们每层都存在很多的报文,我们不同的层一定会存在多个报文,那么有这么多的报文要不要管理啊,所以报文的结构体一定存在,报文还有自己的内存空间,那报文的结构体和报头有什么关系呢??

我么网络报文的空间布局,首先我们看head和end指针就是指向空间的开始和结束,不用看, 我们主要看这个data和tail这两个指针,第一个指针开始是指向我们的数据区的开头的,tail是指向数据区的结尾的,但是当我们上层往下发送报文的时候,我们怎么拿到下层的报头信息呢??

只需要让data指针-报头数据的大小,此时就为我们的报头开辟了一部分空间,继续向下继续减去报头,因为我们最高层对应的是低地址吗?我们只需要让data指针-我们的对应层的报头,此时我们指针的值就变小了,往上移动,数据区不变大了吗,不就可以填充报头了吗,此时不就能为我们的报头开辟位置了吗??

我们解包的时候,只需要再加上报头大小即可让指针下移即可解包了。

我们sk_buff指针的向上移动就是封装给我们添加报头的过程。

socket和文件系统的关系

大概就是这个样子,我们从fd_array中我们拿到fd,此时这个fd就是我们的下标,我们其中的内容是指针指向了这个file结构体,这个file结构体中的private指针指向我们的socket结构体,我们的这个结构体中的sk指针指向了sock结构体其中存在两个变量就是我们的接受和发送缓冲区了,但是我们的udp一般不用发送缓冲区,我们的缓冲区就指向了我们的sk_buff_head结构体了,其中的sk_buff就是我们刚才讲的那几个指针,此时就能指向我们的存放报文的那部分内存了。所以我们就能拿fd就能对我们的报文操作了。因为我们的报文也是对应一个fd。报文就是我们在网络中打包好的一些数据。因为你的网络这个报文要写给我的内核,所以我就要给你一个fd也就是套接字。

这个fd是属于承载这些报文的容器。所以我们之前调用socket的时候,直接让 Linux 内核,在内核内存里,新建一个「网络专用缓冲区容器」,就是这个作用。

我们发送数据的本质就是先通过fd找到我们的内存中的这块空间,然后通过指针-sizeof本层报头的大小,然后封装,通过网络传输发给对方的接受缓冲区,然后对方再通过指针+sizeof本层报头的大小解包拿到数据。

TCP

下面我们来讲一下TCP。

TCP报头的标准长度是20个字节。

这是我们TCP的结构体,类型+名称:+数字就代表位段。

里面的宏代表大小端。

我么为什么还存在4位首部长度是干嘛的啊?

它代表的数值是[0,15]呀,都不能代表前20位,但是我们这个有一个内部规定,就是我的单位是4个字节,所以需要乘以4,这就是一个规定,此时就是[0,60]了,前二十位是报头长度,后面的四十位就可以代表tcp的选项了,我们标准长度是x单位是4字节,我们报头20位,x*4=20,x=5,5是0101,此时就代表了前二十位。

URG:紧急指针是否有效。

比如我们TCP是可靠的,是有序的,按序到达的,我们的接收缓冲区字节流式的接受队列,按序接受的,但是如果我们有数据比较紧急需要优先处理呢??

比如我要取消上传这个文件,上传错了,那你说你要把前面的全部上传完再取消我是不是就太晚了,因为我上传错了要取消,你不先执行我这个如果我上传上传进去影响你其他报文怎么办呢??怎么实现的呢?就是有一个紧急指针能先被上层拿到,看见了就取消你即可。

  • 客户端正在疯狂发大文件(一堆数据堵在 TCP 发送缓冲区、网络链路、接收缓冲区)
  • 用户点 取消上传,想立刻停

如果没有URG的话,此时你无法立即停止必须等前面的上传完才能。

这个紧急指针本身是在报文有效载荷里特定的偏移量处,有紧急数据,紧急数据只有一个字节。

TCP可靠性问题

思考两个问题,C发送报文,能知道自己的报文丢失了吗??

答案是不知道的。

存不存在百分之百可靠的协议??

最新消息,永远没有办法确认对方是否收到!!

比如你给离你很远的地方的人喊话,你说了一句话对方回应了你才确定对方听到了,但是如果他不回应你就无法确定它是否收到了你说的话,所以说最新的一句话永远是无法确定对方是否收到。

只能保证上一句话的可靠性,不能确定最新一条的可靠性。

TCP常规的通信模式1:对方必须给我发送的消息应答,应答的消息不需要回答。

应答也有可能收不到,所以我们只需要保证消息的可靠性即可,应答就是一个单纯的应答,应答就算丢了,我也可以重发。发送应答方你的应答信息不重要,收到了就确定了发送消息的可靠性。

能保证历史报文被百分百确认。

这种常规模式效率很低。

解决办法就可以一次发送多个消息,虽然你还没应答,但是我还继续发消息,我如果收到了前面的应答,就可以了,收不到就会有一个超时计时器超过某个时间重发即可,不用发一个必须应答才发另一个效率太低了。

发送顺序和接受顺序是否一致呢??

比如早走的一定走收到吗???

答案时不一定的。我们发送的可能是乱序的啊,这怎么办呢??

我们就可以给消息的到达消息标个序号,发送的时候自带序号,到达对方主机的时候,一定要让大序号等小序号到了重新排序让小序号先去上层。

但是我们发送的信息可能丢失啊,我们要怎么给发送信息方作应答呢?

答案是这里会存在一个确认号,比如你发送100,200,300,400,此时你只发送过去了100和400,此时我发送会给你的400的确认号也是101就表示我只拿到了101之前的信息,中间的我不确定。

我们来回互发的是什么东西???

TCP完整报头+有效载荷。就是TCP报文。应答最少也得发报头。无非就是是否有数据的区别,发过去的有数据很好理解,应答没有数据只发回来报头。

不就是这个样子吗,一个带数据,一个只是报头,把确认号给我们即可。

还有一个机制叫做捎带应答,就是我应答的时候,你服务器可能在应答的时候,可能也想给我发信息啊,所以我也可以带上你要发送的数据,给客户端此时既可以应答也可以发送给客户端服务器要发的数据,效率很高。

为什么要两个序号呢??我返回100不行吗??为什么要返回101?

因为捎带应答的存在导致我们必须有两个,因为我也可以给你发送消息,我自己也有序号,要是只有一个的话,那么我要给你发的消息的序号怎么存放呢??

我们客户端给服务端发消息的时候我并不清楚服务器端的接收承载能力,客户端可能发送大量的数据,导致对端主机来不及接受,对方缓冲区满了,你再发数据就被丢包了,TCP可以解决丢包问题,但是这个丢包不合理啊,显然是不行的,不能乱丢包。

要想解决这个问题就要让发送发知道对端的接受能力,对端的接受能力怎么衡量??

就是对端接收缓冲区的剩余空间就是接收能力。

2.你怎么知道对端的接受能力呢??

发送数据,得到应答,应答的本质是tcp报头,tcp报头中设置一个协议字段就是我们上面的16位窗口大小。

每发送一次就给你返回对方的剩余缓冲区空间的大小。

根据win大小,调整发送数据速率的机制 -- -流量控制。

窗口大小填写的是谁的接收缓冲区剩余空间的大小??

答案是自己的,自己把自己的接收能力发送给对方。

如果对方剩余缓冲区空间特别大就大量发数据,此时效率不就很高了吗?

下面我们来谈一下标志位

标志位

什么是标志位呢??

就是结构体位段中的比特位(0或者1).

为什么要有标志位呢??

因为我们多个客户端可能发送不同的请求报文,有的是建立连接的,有的是断开连接的,有的是应答报文,所以我们的报文是存在类型的,所以要有标志位区分类型。

各自是什么??

ACK标志位是应答标志位是确认报文,因为存在捎带应答,所以我们的ACK大部分情况都是设置为1.

SYN:请求建立连接,我们把携带SYN标识的称为同步报文段。

TCP面向连接的,通信前需要先进行三次握手进行建立连接,怎么理解呢??

**就是我们打电话的时候,明明接通了但是我们还是都喂了一声,这个就是确认是否接通,三次握手就是我客户端发送给你报头我把SYN标志位置为1,对方把SYN和ACK都置为1,然后我再把ACK置为1发送回服务器,此时就建立起连接了,很抽象。**为什么要三次握手呢??

答案是是建立连接最快的方式。

FIN标志位:通知对方,本端要结束了,我们称携带FIN标识的为结束报文段。

就是我客户端要和你断开连接,就是我客户端把FIN置为1,服务器看到了,知道了你要和我断开连接啊,我服务器给你发送ACK标识我收到了,然后服务器再给我发送FIN,我客户端在发送ACK。这叫做四次挥手。

你客户端和我断开连接的时候恰恰我服务器也要和你断开连接,此时我ACK确认了并且FIN直接捎带应答给你发送FIN,然后客户端在发送ACK表示收到,此时也可以是三次挥手,这种情况太少了。

就是把中间的ACK和FIN合并了。

其实三次握手也是四次握手,只不过服务器需要无条件接受连接了,此时捎带应答把SYN标志位带过去了,如果是比较严格的服务器,你连接我,我先ACK,然后我考虑一下是否要和你连接,此时就是四次握手了。

PSH标志位,这个是处理什么情况呢??

发送方一直发给接收缓冲区,导致接收缓冲区空间为0,此时你的发送缓冲区发送这个PSH置为1此时发送发表示给接收方是你赶紧把数据交给上层,要不不给你发了,就是催接收方快点把数据交给上层。

应用程序怎么取数据,应该是应用层自己决定的吧,那么PSH到底在底层做了什么??

现在不好讲,后面再讲。本质就是唤醒进程去处理。

RST标志位:我们发送的时候会存在多个连接,我们操作系统要对这些连接管理起来啊,就是通过结构体来管理的。

建立和维护连接都是有成本的。

我们画线为什么是斜的呢??

因为我们发送和到达是需要时间的,斜的就代表是时间是不同的。

TCP建立连接时三次握手不一定是百分百建立连接成功的。

重新理解三次握手。

我们客户端发送出去ACK就是三次握手完成,还是对方收到才是完成。

当然是发出去就代表完成了,因为我们最后一个ACK是没有应答的。对方是否收到你是不知道的。

如果最后客户端发送的ACK丢失了,此时客户端认为已经建立连接了,但是服务器没有收到,此时你客户端发送信息给服务器服务器就很奇怪,我没给你建立连接你给我发什么信息啊,所以直接就给客户端发送RST标志位,我要你重新建立连接的意思。

就是处理各种连接异常的问题。

URG上面说过了。

就是如果一些报文要插队提前处理,就设置这个标志位即可。

在逻辑上我们把TCP的缓冲区看成线性的,为什么是逻辑上线性呢??

因为我们的缓冲区是一个个的sk_buff所指向的报文空间,它们是不连续的独自开辟申请的,我们通过一个struct_iobec结构体里面是vector容器,把这些空间发在这个vector容器中,此时就可以看成是线性的了。就可以把它们管理起来了。

序号到底是怎么回事呢??

TCP将每个字节的数据都进⾏了编号. 即为序列号.
每⼀个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下⼀次你从哪⾥开始发.就是把字节编号了,导致每个都存在序号了。
超时重传机制就是我发的报文丢失了我怎么知道呢??
有人说没收到应答不就知道了吗??
但是如果你的应答丢包了呢??
此时怎么办呢??
你能说我的数据丢了吗,此时我不知道我的报文是否丢了,不确定报文对方是否收到,收到应答对方百分百收到,就是不管你哪个丢包了,我只要约定时间内没收到应答,就是超过约定时间就认为他丢了。
如果报文没丢,应答丢了呢??
那你重传一次,对方不就收到重复的报文了吗??
序号去重,序号重复了就去重就行。
时间是多长??
超时时间太长太短都不行。
太长效率太低,太短就误判了,导致可能报文正在传,对方应答没传回来呢,此时就重传。

一般都是动态计算超时时间的。

OS怎么设置超时的??

是不是OS要有定时的能力----闹钟,闹钟响了就重传回调呗。

连接管理机制

结论1:连接管理机制属于TCP可靠性机制之一!!

其实上面发送的不是标志位,都是发送的都是报头或者报头+数据!!很重要的,不要被误导了。

客户端调用connect就是发起三次握手的,双方OS自主完成的。

如果我们不进行accept,那么我们会不会连接上呢(仅仅用listen)??

答案是可以的,所以说accept不参与三次握手,只获取已经建立好的连接。

TCP三次握手,为什么是三次??

我们要建立连接双方都要同意啊,所以我们都得对对方发送连接请求,保证我们网络通常。就是网络通畅的,你双发都发送消息,验证了全双工,就可以双方互发消息是通畅的,同时也建立了双方通信的共识,双方都知道,我要和你进行通信了。

为什么是四次挥手,还是就是断开连接要建立双方的同意,其实就说了两句话,这里的ACK就是一个应答,就是一个回答。

客户端给服务器发送FIN断开连接就断开了客户端给服务器发送信息的通道,服务器再给客户端发过来就是关闭服务器给客户端发送信息的通道。

有相应的函数来支持我们关闭相应的窗口。

客户端给服务器端发送FIN的时候自己是FIN_WAIT_1的状态,此时服务器端收到这个之后自动发送ACK,此时就处于CLOSE_WAIT状态了,此时客户端收到就是FIN_WAIT_2的状态了。

客户端退出,我们服务器端是不调用close(fd)的。如果我们不关闭我们的服务器端,此时我们就会出现fd泄漏的问题,后两次挥手无法完成,此时如果大量用户去访问你的服务器然后退出的话,此时就会出现服务器端占据着大量的fd不去关闭的情况。

如果关了就会变成LAST_ACK的状态,发送FIN客户端变成相应的状态就不说了。

主动断开的一方,四次挥手已经完成,自己不能立即退出,而是要再等待一段时间,让自己出去TIME_WAIT状态,需要等待2倍的报文最大生存时间,最大生存时间就是我们报文发送过去了,但是没有应答,此时就会出现超时重传,序号去重的操作,直到应答,这段这个报文的生存时间叫做最大生存时间。

如果没有这个状态,有可能路由中积压着我们历史遗留的报文,如果直接关闭,此时你客户端关闭了,但是一个新的客户端来了,正好拿到你的端口号和目的端口号,此时你积压的报文不管这些东西,直接还认为自己的客户端存在,此时你新客户端发送的报文,这些老的报文可能就去了服务器端,此时就干扰了我们新客户端的发送了。也有可能我们的客户端发送的ACK丢失了,此时服务器会一直等待。我们只要等待,即使我们客户端发送的ACK丢了,服务器会重发我们的FIN。

这种情况的概率非常低,但是还是可能存在的。

因为我们全球http请求的数量太大了。

解决办法,建立连接握手时,交换随机报文起始序号。

这是因为

  • 客户端给服务端:我的起始序号 ISN_C
  • 服务端给客户端:我的起始序号 ISN_S

此时如果交换了

TCP 接收数据,不只看四元组,还要校验:这个报文的 SEQ 序号,是否在当前连接合法的序号窗口内。

三次握手协商出了全新、完全随机的 ISN新连接的合法序号范围,和旧连接完全错开。

👉 服务端收到迟到旧报文:一看 SEQ 不在当前连接的合法区间直接丢弃、拒绝接收,不会上交应用。

客户端给服务器发送或者服务器给客户端发送数据的时候,会把数据序号发送给对方,便于校验。

第二个解决方案就是主动断开,等待2MSL时间。

防止陈旧报文对新连接产生干扰,让陈旧报文从网络中消散。

但是现在有个问题,如果服务器断开,由于谁主动断开谁进入TIME_WAIT状态,此时服务器端进入这个状态的话,就存在一个问题了,fd和端口都被占用我们在二倍的最大生存时间之内是无法重新启动服务器的,我们的服务器端无法重启,怎么办呢??

即使服务器端进入这个状态也要立即重启。

认识一个新的接口。

setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));这叫做地址复用,此时我们就算服务器进入这个TIME_WWAIT状态,我们也能直接立即重启服务器。

通过我们的这个指令能查我们当 TCP 连接处于 FIN_WAIT_2 状态时,内核等待对方(被动关闭方)发送 FIN 包的超时时间

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送 端继续发送, 就会造成丢包, 继⽽引起丢包重传等等⼀系列连锁反应.

因此TCP⽀持根据接收端的处理能⼒, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow

Control);

接收端将⾃⼰可以接收的缓冲区剩余空间⼤⼩放⼊ TCP ⾸部中的 "窗⼝⼤⼩" 字段, 通过ACK端通

知发送端;

窗⼝⼤⼩字段越⼤, 说明⽹络的吞吐量越⾼;

接收端⼀旦发现⾃⼰的缓冲区快满了, 就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;

发送端接受到这个窗⼝之后, 就会减慢⾃⼰的发送速度;•

如果接收端缓冲区满了, 就会将窗⼝置为0; 这时发送⽅不再发送数据, 但是需要定期发送⼀个窗⼝

探测数据段, 使接收端把窗⼝⼤⼩告诉发送端.

我们可以看一下这个图,就是我们不断给服务器发送数据,服务器也通过应答给我这个窗口剩余空间大小的信息,此时到达0之后我们就不再发送是数据了,因为我知道对方也处理不了了。

此时主机A就发送窗口探测就是1 字节的无效数据 + 正常 TCP 头(SEQ 序号),让对方再给你发送这个信息就是发送方继续获取对方剩余空间的大小,对方有空闲空间也会告诉主机A。

可是第一次的时候,主机A,如何得知主机B的窗口大小呢??

我B只能接收500字节,你A给我发送了1000字节,此时不就直接丢包了吗??

在通信之前,双方是经历过三次握手的!!

前两次握手,一定不携带任何数据,因为没有连接成功,不能发数据,但是双方在前两次是交换过报文的!!所以,通信之前,就已经交换过双方窗口的大小了。

细节2:因为窗口的大小是65536个字节,所以是不是就是说我们的接受和发送缓冲区最大接受65536个字节呢??

所以真是缓冲区的大小是不确定的,是可以扩大的。

服务器主动FIN了,每个连接对应的服务器端的状态就是TIME_WAIT,此时服务器内会出现由服务器主动退出导致的大量的TIME_WAIT状态了。

滑动窗口

刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个⽐较⼤的缺点, 就是性能较差. 尤其是数据往返的时间较⻓的时候。
所以我们的解决办法是一次发送多个。

既然这样⼀发⼀收的⽅式性能较低, 那么我们⼀次发送多条数据, 就可以⼤ 的提⾼性能(其实是将多个段的等待时间重叠在⼀起了).

窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值. 上图的窗⼝⼤⼩就是4000个字

节(四个段).

发送前四个段的时候, 不需要等待任何ACK, 直接发送;只是暂时不需要ACK。

收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推;

操作系统内核为了维护这个滑动窗⼝, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只

有确认应答过的数据, 才能从缓冲区删掉;

滑动窗口在哪里??

我们的滑动窗口是存在在哪里呢??

答案是存在在我们的发送缓冲区中,属于发送缓冲区的一部分,我们也知道我们要把发送缓冲区看成线性的,所以表示一部分就是表示数组的一部分我们可以使用两个指针表示数组的一部分。

滑动窗口的存在,他把发送缓冲区切分成为了三部分:a.直接发,暂时不要应答。

滑动窗口左侧部分叫做已经发送和已经确认的了,滑动窗口的是直接发的,暂时不要应答的,右侧是待发送的或者空的位置。

窗口滑动的方向是从左向右滑动的。

滑动的本质就是下标在增大,就是滑动,从左向右就是增加的。

滑动窗口的大小由谁来确定??

对方ACK报文中的win窗口大小决定!!(就是对方的接受缓冲区的能力)

流量控制是由滑动窗口实现的。

它是怎么确定滑动窗口大小的呢??

我们用start指针表示滑动窗口的最左侧的点,end表示右侧,此时end-start不就是滑动窗口的大小了吗??

但是我们的这两个如何确定呢??

对方ACK发送的时候会有确定序号,start=确定序号,end = start + win(对方接受缓冲区的接受能力)。

每次都这样既不会超出对方缓冲区的接受能力,start也能拿到我们新的要发送的数据的确认号,每次更新就是类似滑动了。

思考一下这几个问题。

能向左滑动吗??答案是不能的,因为左边的都是已发送已确认的数据了,未发送的在后面。

大小会变吗??

一定会变的因为还要考虑对方的接收能力的。

什么时候变小。

比如我开始start是2000,win是4000,我们的滑动窗口就是2000-6000了,此时我们如果对方上层没有取走数据,我们又发送过去1000个此时对方给我确认了1000个序号,返回报文中的win是3000了,但是此时我们的start就拿到返回的3001了,此时左边是3001,但是右边还是我们的start+win(3000),此时左边向右滑动,但是右边不变,此时不就变小了吗??

什么是不变呢??

就是我发送过去1000个序号,对方正好交给上层1000个序号,对方的win不变,但是我的start变大了,为3001了,此时end=4000+3001,大小还是4000,这就是不变的情况。

什么时候变大呢??

就是你发送过去1000个序号,此时对方上层取走了缓冲区2000个序号的数据,此时win大小就是5000,此时你的start是3001,但是end就变成3001+5000了,此时滑动窗口就变大了。

滑动窗口大小可以变成0吗??

答案是可以的,只需要我们此时滑动窗口把数据全部发送过去把对方接收缓冲区打满了,此时win就是0了,此时把ACK确认序号发过来了,此时你的start就等于end了。

start能大于end吗??

答案是不可能的。

滑动窗口发送的时候,丢报了怎么办呢??

这种情况没事,就是你前面的应答丢了无所谓只要我最后一个应答了我就知道前面的全部发送过去了,即使最后一个应答丢了也没事,无非就是超时重传,序号去重就可以了。

但是如果发送的时候丢了呢??

如图所示,下面ACK全部确认序号是1001了。

此时如果我们收到了三次ACK都是同一个结果的话,我们就能百分百确认某个报文一定丢了,此时立马开启快重传机制,此时就重传1000-2000。

但是还有一个问题,此时你只重传了我们的1000-2000,但是我如果2001-3000也丢了呢??

此时你只根据下面的ACK确认1000-2000丢了,重传了,我2000-3000怎么办呢??

答案是我们补发了之后,对方会对我们后面的报文重新发送ACK此时你后面的报文全都拿到2001,你此时就能知道我2001-3000也丢了啊,此时就是这个机制。

那你存在了快重传了,那我超时重传是不是没意义了呢??

答案是不是的,因为我们快重传是存在触发条件的,收到三个以上的相同ACK的,但是如果你后面只存在两个呢??你无法触发,此时你就必须使用超时重传了。

丢报问题无非就是只有这三种基本情况的自由组合,基本情况只有这三种。

我们通过这几种情况更好理解滑动窗口的妙处。

你比如我们1001-2001丢了,此时后面的ACK都会返回1001导致我们左侧滑动窗口一定是不变的,右边可能变,因为win可能改变。

为了支持重传,我们发出的数据不能立即删除,而应该被暂时保存起来,以便我们后面确认或者重传。

滑动窗口没有移动不就表示我们的报文没有丢失吗??

此时不就相当于我们发出的数据我们把它暂存起来了吗??

我们必须收到ACK才能移动我们左侧端口,此时不就是没有立即删除我们的报文吗??

中间报文丢失了,左边收到了,此时左边会移动,此时我们这个丢失的报文不就变成滑动窗口的左侧吗。

所以不管哪种情况丢失都是最左侧丢失问题。

滑出去了怎么办呢??越界了怎么办??

就是如果我们发送的数据超过了我们定义的这个发送缓冲区的大小了呢??

意思就是你旧数据发送完之后,前面的序号不就无法用了吗,我们上面说过无法向左滑动的,但是如果此时把发送缓冲区的序号全部用完,就是越界了怎么办呢??

越界是啥情况呢?

我们的发送缓冲区是一个个sk_buff指针指向的报文的内存地址组成的,我们的序号是由32位整数编址的,最大值位2的32次方-1,你每次运行完,因为只能向右滑动,所以我们的旧序号得不到使用,我们的旧数据在确认之后就被释放的,但是它的序号我们无法使用,所谓的越界就是我们不断的加入报文,往后编址,导致我们32位的序号用完了,此时就是说越界了。

答案是类似于环形数组的形式解决的,你可以这样子理解,但是底层我们也知道发送缓冲区不是连续的,而是一个个sk_buff指针指向的空间,真正的作法是通过算法重置到我们的首位置。

就是让我们的序号从最开始继续往后编序号。

为什么我们的报文

为什么不直接1001-5001直接发过去,我为什么要分为四个段发过去呢??

这和底层有关,现在没法讲,链路层的时候再讲。

简单总结一下。

拥塞控制

网络拥塞程度也要考虑,比如如果我们发送十个报文1个丢了,就说明这个报文运气不好,被丢包了,但是如果十个报文9个丢报了,此时就应该考虑这个网络的问题了。

网络瘫痪,软件崩溃OS异常,TCP都无法解决,需要人工介入。

我们大量丢报了,网络出现问题怎么办呢??

等一等或者减少数据量发送。

为什么不敢直接重传呢??

因为网络我们不止自己在用的,好多人都在用,大家都丢报了,大家都重传不还是丢报吗??

一直丢报一直传不过去就出现问题了。

我们必须TCP构建全网内所有主机面对拥塞问题的共识,就是让每个主机的报文一部分等一等,少量发送过去,此时不就很好的解决这个问题了吗。

TCP引⼊ 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的⽹络拥堵状态, 再决定按照多⼤的速度传输数据;
这个怎么理解呢??

先发一个看看行不行行了再发两个,可以再多发一点点慢慢发送,就叫做慢发送。

此处引⼊⼀个概念称为拥塞窗⼝

发送开始的时候, 定义拥塞窗⼝⼤⼩为1;

每次收到⼀个ACK应答, 拥塞窗⼝加1;

每次发送数据包的时候, 将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较, 取较⼩的值作为实际发

送的窗⼝;

发送数据此时你就要考虑两个问题了,不止要考虑对方的接收能力,还要考虑网络的拥塞情况了。

真正的滑动窗口的计算就是i这样子了,就是谁小我选谁,此时就能避免网络拥塞了。

发送方如何控制自己的发送量是1个报文的呢??

通过改变拥塞窗口=1就行了,此时一次就发送一个报文了。

慢启动就是通过改变拥塞窗口的大小来完成的。

慢启动为什么是指数增长的呢??

' 第一次1,2,4,8,16,32这种形式增长,为什么叫慢启动呢??

这里的慢表示的是前期增长的慢,增长快的特点是用来尽快恢复正常通信,前期慢就是为了试探网络拥堵的情况,如果发现不怎么拥堵,就以指数的情况快速的恢复正常通信。

也不能让它一直增长吧??

指数一直增长是没有意义的,此时怎么办呢?

为了不增⻓的那么快, 因此不能使拥塞窗⼝单纯的加倍.

此处引⼊⼀个叫做慢启动的阈值

当拥塞窗⼝超过这个阈值的时候, 不再按照指数⽅式增⻓, ⽽是按照线性⽅式增⻓

我们的这个过程就是我们会设置一个初始的阈值,超过这个阈值就不进行指数增长,而是去线性增长,如果我们探测发现到达某个拥塞窗口的值,网络又开始拥堵了,所以此时就满开始,从0开始重新探测,此时的阈值设为网络阻塞窗口的阈值的一半。

我们拥塞窗口的大小我们是不知道的,我们必须根据上面探测出来。

一直增长就是来拿到我们最真实的网络拥塞窗口的值。因为这个值根据网络情况一直在变。

以便我们滑动窗口使用。

我们拥塞窗口会一直增长吗??

理论上是一直增长。但实际上不能一直增长和我们的带宽和RTT有关。

当TCP开始启动的时候, 慢启动阈值等于窗⼝最⼤值(这个最大值一般都是默认值);

在每次超时重发的时候, 慢启动阈值会变成原来的⼀半, 同时拥塞窗⼝置回1;

少量的丢包, 我们仅仅是触发超时重传; ⼤量的丢包, 我们就认为⽹络拥塞;

当TCP通信开始后, ⽹络吞吐量会逐渐上升; 随着⽹络发⽣拥堵, 吞吐量会⽴刻下降;

拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对⽅, 但是⼜要避免给⽹络造成太⼤压⼒的折中⽅案.

延迟应答

这个就是我们发送方给了接收方,然后接收方等一会儿再应答,这个的作用就是等这一会儿,此时我们上层就可以把我们接收缓冲区拿走数据,此时就能返回更大的win,此时滑动窗口就可能更大,此时效率就会提高。

那么所有的包都可以延迟应答么? 肯定也不是;

数量限制: 每隔N个包就应答⼀次;

时间限制: 超过最⼤延迟时间就应答⼀次;

等一等的策略。

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客⼾端服务器在应⽤层也是 "⼀发⼀收" 的. 意味着客⼾端 给服务器说了 "How are you", 服务器也会给客⼾端回⼀个 "Fine, thank you";

那么这个时候ACK就可以搭顺⻛⻋, 和服务器回应的 "Fine, thank you" ⼀起回给客⼾端

只有一个注意的点:

就是只有我们第三次客户端给服务器应答的时候可以带数据,前两次是不能带数据的,怎么理解呢??

就是我第一次给你发送连接请求,我不知道服务器是否同意啊,我肯定不能发数据呀,第二次服务器ACK了代表的就是我开辟了一个客户端到服务器端的单向通道,此时是服务器允许客户端给自己发消息了,但是你服务端发送的SYN握手请求我客户端没有接受啊,你肯定不能给我发,客户端没有ACK可以理解位服务器端到客户端的单向通信通道还没打开,此时第三次已经存在从客户端到服务器端发送数据的通道了,此时就可以发送数据了。

面向字节流

由于缓冲区的存在, TCP程序的读和写不需要⼀ 匹配, 例如:

写100个字节数据时, 可以调⽤⼀次write写100个字节, 也可以调⽤100次write, 每次写⼀个字节;

读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以

⼀次read⼀个字节, 重复100次;

就是写的次数和读的次数不一样就是面向字节流的,恰好和我们面向数据报的相反,面向数据报是读的次数和写的次数一样。

粘包问题

⾸先要明确, 粘包问题中的 "包" , 是指的应⽤层的数据包.

在TCP的协议头中, 没有如同UDP⼀样的 "报⽂⻓度" 这样的字段, 但是有⼀个序号这样的字段.

站在传输层的⻆度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.

站在应⽤层的⻆度, 看到的只是⼀串连续的字节数据.

那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的

应⽤层数据包.

那么如何避免粘包问题呢? 归根结底就是⼀句话, 明确两个包之间的边界.

对于定⻓的包, 保证每次都按固定⼤⼩读取即可; 例如上⾯的Request结构, 是固定⼤⼩的, 那么就

从缓冲区从头开始按sizeof(Request)依次读取即可;

对于变⻓的包, 可以在包头的位置, 约定⼀个包总⻓度的字段, 从⽽就知道了包的结束位置;

对于变⻓的包, 还可以在包和包之间使⽤明确的分隔符(应⽤层协议, 是程序猿⾃⼰来定的, 只要保

证分隔符不和正⽂冲突即可);

思考: 对于UDP协议来说, 是否也存在 "粘包问题" 呢?

对于UDP, 如果还没有上层交付数据, UDP的报⽂⻓度仍然在. 同时, UDP是⼀个⼀个把数据交付给

应⽤层. 就有很明确的数据边界.•

站在应⽤层的站在应⽤层的⻆度, 使⽤UDP的时候, 要么收到完整的UDP报⽂, 要么不收. 不会出

现"半个"的情况.

TCP异常情况

进程终⽌: 进程终⽌会释放⽂件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.

机器重启: 和进程终⽌的情况相同.

机器掉电/⽹线断开: 接收端认为连接还在, ⼀旦接收端有写⼊操作, 接收端发现连接已经不在了, 就会进 ⾏reset. 即使没有写⼊操作, TCP⾃⼰也内置了⼀个保活定时器, 会定期询问对⽅是否还在. 如果对⽅不在, 也会把连接释放.

另外, 应⽤层的某些协议, 也有⼀些这样的检测机制. 例如HTTP⻓连接中, 也会定期检测对⽅的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接。

进程打开的文件和文件描述符只有这一个进程打开,此时这个文件的生命周期随着进程了。

等同于上层调用close,此时就四次挥手。

机器重启需要先杀掉我们的进程,就是情况一。

比如你登录的软件多了证明你连接上对方的服务器了,此时你重启机器,此时就会很慢,这是因为进程和服务器端进行四次挥手呢,所以会慢一点。

网卡出现问题你OS怎么知道我网卡出现问题了呢??

硬件中断!!客户端就自己释放连接,此时服务器保活,过段时间也会断开。

就是如果我们给服务器端发送数据,然后我们客户端直接断开然后重连,此时你服务器端恰好吧处理好的数据发送回来,此时客户端很疑惑,还没三次握手呢,你给我发什么东西,此时就会给服务器发送RST,重置连接,此时你发送回数据就相当于write此时你向一个不存在的连接中写入,此时可能会给你这个进程发送SIGPIPE信号会把你的服务器进程杀掉,所以我们在写守护进程的时候为什么要忽略这个信号了。

TCP/UDP对⽐

我们说了TCP是可靠连接, 那么是不是TCP⼀定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进⾏⽐较

TCP⽤于可靠传输的情况, 应⽤于⽂件传输, 重要状态更新等场景;

UDP⽤于对⾼速传输和实时性要求较⾼的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以

⽤于⼴播;

归根结底, TCP和UDP都是程序员的⼯具, 什么时机⽤, 具体怎么⽤, 还是要根据具体的需求场景去判定

相关推荐
molihuan1 小时前
最新VMware Ubuntu 1分钟极速安装 植物人教程
linux·ubuntu
sdm0704272 小时前
深刻理解进程信号
linux·运维·服务器
Simonhans2 小时前
Linux安装Bun
linux·bun
70asunflower2 小时前
Ubuntu `tree` 命令完全指南:让目录结构一目了然
linux·数据库·ubuntu
老四啊laosi2 小时前
【Linux系统】16. 进程程序替换
linux·exec·程序替换
奇妙之二进制2 小时前
zmq源码分析之消息可读通知机制
服务器·网络
techdashen2 小时前
不开端口,不配 DNS,用树莓派在家搭一个公网可访问的 Web 服务
前端·网络·智能路由器
笨熊呆呆瓜2 小时前
【可靠性配置】华为M-LAG防环机制
网络
郑寿昌2 小时前
虚幻引擎UE5 Lumen兼容PBR材质全解析
服务器·网络·材质