当前位置: 首页 > news >正文

Linux网络——传输层协议UDPTCP

前言

之前我们提到过osi的标准,现在我们在回过头看一下osi的七层模型,下四层就不说了。

会话、表示、应用

会话层:通信管理。建立和断开通信连接。现在我们已经有了,目前的解决方案是用线程来统一管理连接的生命周期,由线程来获取连接访问之后创建线程操作完再断开连接。

表示层:就是我们定的协议,协议的格式、客户端和服务器进行数据交换的格式、上下层序列化反序列化的转化

应用层:具体的业务逻辑处理

现在我们就能理解为什么osi上面的三层会压缩成一层(把会话、表示、应用这三层放在应用程序里)

不同的服务器对于连接的打开和关闭的需求不一样,不同的应用层它的应用不一样,那么它定义的数据格式、序列化反序列化方式也不一样

这意味着什么,这意味着会话、表示、应用这三层因为应用的不同,他们不可能明确的写在操作系统当中。

应用层我们讲完,UDP、TCP套接字我们也用过了,具体这些套接字数据报文应该如何从客户端发送给服务器,服务器响应给客户端呢?那么我们就要来讲讲不变的部分,也就是OSI和TCP/IP相同的部分:传输层、网络层、数据链路层以及物理层

传输层协议UDP

UPD为传输层协议那么就注定了它属于操作系统,一会谈到的很多东西应该都能在操作系统中找到相关的概念。

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

再谈端口号

端口号(Port)标识了一个主机上进行通信的不同的应用程序

从应用层我们已经知道了,服务器中UDP、TCP都要显示的bind一个端口,让服务器有一个众所周知的端口,那么客户端才能够让操作系统随机式的选择对应的端口,而IP地址客户端和服务器可以根据实际情况来选择适合自己的。

主机A,这样一台服务器上可能部署了各种各样的服务,如果我知道某一种协议,那么你就必须获取连接,处理连接、根据协议进行反序列化,然后进行处理数据。底层收到的报文应该将数据传给哪个应用程序呢,就可以用端口号进行区分。

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

服务器httpd1、2、3理解为线程,客户端B用浏览器打开一个页面,客户端A打开2个浏览器画面(打开两个浏览器访问也可以),服务器收到请求后要把应答推送不同的画面到不同的客户端,那么这应该怎么做呢?服务器通过不同的ip地址区分不同的客户端,对于ip地址相同的,他们的客户端端口号不一样,那么可以通过客户端端口号来标识不同的进程画面。TCP解决的端口号的问题,而IP解决的是地址的问题。

端口号划分

0 - 1023:

知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的 端口号都是固定的

1024 - 65535:

操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.

认识知名端口号

有些服务器是非常常用的, 为了使用方便,人们约定一些常用的服务器, 都是用以下这些固定的端口号:

ssh 服务器, 使用 22 端口

ftp 服务器, 使用 21 端口

telnet 服务器, 使用 23 端口

http 服务器, 使用 80 端口 

https 服务器, 使用 443端口

执行下面的命令, 可以看到知名端口号

cat /etc/services

我们自己写一个程序使用端口号时, 要避开这些知名端口号

一个进程是否可以bind多个端口?

可以,一个进程bind8080,8082,这两端口收到的数据都会交给对应的进程。

一个端口是否可以被多个进程绑定?

也不行,会破坏端口号到进程的唯一性

UDP协议端格式

之前讲过当我们的报文从上往下交付时是要进行封装的

传输层报头(udp)

有了这个udp报头当数据传输到对方的传输层时就可以通过里面的16位目的端口号找到对应的应用程序了。

如何理解这个传输层报头呢?

所谓的协议本质是一种约定,协议就是一种结构化字段(双方操作系统都认识的结构体),传输层添加的报头就是一个结构体对象,那么对方也可以用同样的结构体来识别出源端口和目的端口

struct udphdr
{__be16 source;__be16 dest;__be16 len;__sum16 check;
}

任何一层协议都必须解决两个问题:如何分离、如何交付。

如何分离(封装)?

报文宽度(0~31),16位UDP长度标识的是整个报文的长度(包括报头+有效载荷),而数据将来是应用层的所有内容;也就是说当另一方收到报文时,可以根据16位的长度来判断有效载荷的长度然后交付给上层

这就证明UDP的总长度时能够包含我们的报头+有效载荷的长度的。而我们的报头是固定长度,所以UDP是如何做到分离/封装的呢?添加固定的8字节数据,说白了就是那个结构体对象。

如何交付?

报头中有目的端口号,可根据此端口号向上交付。

至于校验和是用来保证安全的,如果校验和出错, 就会直接丢弃

UDP面向数据报:应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并;你发什么数据块,我们上层就收到什么数据块(通过长度确定有效载荷,交给上层)

UDP 的特点

UDP 传输的过程类似于寄信.

无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;

不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP 协议层也不会给应用层返回任何错误信息;报文丢了就是丢了。

面向数据报: 不能够灵活的控制读写数据的次数和数量;

UDP 的缓冲区

UDP 没有真正意义上的发送缓冲区(不需要). 数据会调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;

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

如果缓冲区满了, 再到达的 UDP 数据就会被丢弃;

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

UDP 使用注意事项

我们注意到, UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K(包含 UDP 首部)。然而 64K 在当今的互联网环境下, 是一个非常小的数字.。如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装(很少用到)。

基于 UDP 的应用层协议

NFS: 网络文件系统

TFTP: 简单文件传输协议

DHCP: 动态主机配置协议

BOOTP: 启动协议(用于无盘设备启动)

DNS: 域名解析协议

补充:

之前写的服务(进程)必须bind指定的端口号,数据包从传输层交给应用层,本质是包数据从OS交给进程。

那么报文如何根据端口号把data交给指定的进程?

bind时候可以这么理解:操作系统内部根据端口号构建了一张hash表(不同OS的操作不一样啊),hash的键值K就是端口号,而V则是进程PCB,bind就是通过K去找V。

Linux一切皆文件,将data传到文件缓冲区,而进程通过fd(文件描述符)就可读了

传输层协议TCP

TCP 协议

TCP 全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传 输进行一个详细的控制;

TCP协议段格式,之前在写网络版本计算器的时候不是有说过序列化和反序列化吗(传输层这里怎么不讲),意思是我们UDP、TCP直接就是以结构化字段给对方发过去吗?

是的,操作系统不需要明确的做序列化和反序列化,而且它也不想做字符串转发,就是直接发结构体对象的,因为这样发出去的数据是最节省空间和网络带宽的。

之前网络版本计算器发一个结构化字段要给它转化成json串(一大串内容),确实不适合网络通信

内核当中为了减少通信成本,增加通信效率直接发送结构化字段,它其实也有序列化、反序列化,只不过它相当于直接序列化成二进制流(结构体的二进制流),这种需要解决大小端,内存对齐等问题。我们当时做不了处理,但是内核里面是做了处理的。

TCP报头在内核当中也是一个结构体

封装和解包重新理解

如何理解封装和解包呢?

应用层由用户去解释,这里我们不考虑,那么内核当中是如何进行封装和解包的。

从目前学习套接字编程的情况来看,UDP、TCP都有接收缓冲区,在收数据的同时可以进行数据的发送(全双工),应用层在处理数据时,操作系统也在不断的收消息,所以之前说过TCP、UDP本质是一个生产消费的过程(OS帮我们收消息,收到后放到接受缓冲区里,上层应用层读消息然后处理)。

操作系统内可不可以同时存在很多个  收到的  报文,这些报文还没来得及处理,甚至还在拷贝到传输层的接受缓冲区,亦或者甚至没有交给传输层而在网络层和数据链路层。

操作系统内一定会收到大量的还没来得及处理的报文。

那么又引出来另外一个问题:OS要不要对大量的暂未处理但是已经收到的报文进行管理呢?

自然是要的:怎么管理?先描述,再组织。

OS对报文的管理就变成了对链表的增删查改。

重新理解一下封装和解包。

所以封装的过程只需要进行:指针移动  +  填写报头  就行了

那么解包呢?

强转后可以直接提取其中的字段,指针向后移,报头就被解开了。

(udphdr *)head->srcport,destport;
head += sizeof(udphdr);

所以封装和解包在内核中只需要进行指针的移动即可!

依旧是这两个问题:TCP的 报头和有效载荷如何分离?如何理解分用?

分用比较简单,TCP中有16位的目的端口号,Tcp收到报文后先进行分离,将有效载荷根据目的端口号交给上层。

报头和有效载荷如何分离?

标准Tcp长度是20个字节,上图还有着选项和数据,选项部分一般是没有的,但是待会会见到

Tcp中存在着一个4位首部长度,表示的是Tcp报头的长度。tcp报头长度不是20吗,它可能有选项的,也可能没有,所以tcp报头是变长的。

4位首部长度[0,15]//全0到全1(0000,1111),你这个长度连标准的20字节都达不到。所以首部长度必须添加一个基本单位(4字节),所以首部长度[0,60]才是报头的长度范围。报头20字节也就是说选项最多可以带40字节的一个数据。如果报文中没有选项只有报头呢?首部长度为5,二进制0101。

具体如何分离的呢?首先提取首部长度,然后计算首部长度,如果计算结果就是20,报头读完剩下的就是有效载荷;如果计算结果大于20,那么就先减去20,再多出来的字节(选项长度)全部读出来,剩下的就是有效载荷。

我们把标识自身长度用来支持解包的字段称为自描述字段。

确认应答(ACK)机制

TCP保证可靠性

客户端向服务器发送消息,但是客户端怎么知道我刚发过去的消息服务器收到了呢?

两台机器的距离变长了,那么不可避免的就会出现丢包的情况,对于客户端和服务器来讲,客户端把消息发出去它并不能很清楚的知道自己发的消息服务器是否收到。

所以我们就要求当客户端发消息之后,服务器必须给出应答

服务器不清楚应答是否被客户端收到,但是客户端收到应答后就肯定知道刚才发送的消息服务器收到了,而且客户端自己也知道收到服务器的应答了。

但是服务器不知道,那么客户端又要给服务器应答,那么如果这样的话双方就要一直应答下去没完没了了,这种方法不能保证双方都确定消息被对方收到了,所以这种真正的可靠性是不存在的。

所以应答方(服务器)不用确认对方是否收到应答,这虽然没有办法保证双方都确定消息被对方收到了,但是能确定历史上(应答的上一条)的消息对方是收到了的。所谓的可靠性是对历史上消息的可靠性的保证。

这样能够保证从左向右的可靠性。那从右向左的可靠性呢?TCP不是支持全双工的吗。未来服务器向客户端发送消息,客户端也要向服务器应答,这样就保证了从右向左的可靠性了。

这种机制称为确认应答(ACK)机制。

上面是TCP发送消息的一种模式:发送数据和发送应答一般是双方操作系统自动完成的——通信细节操作系统自动解决。

而上面的模式不论是客户端还是服务器都必须等应答过来才能发送第二个消息(对应双方来讲发送消息的过程不就成串行了的吗)。

真实情况是这样的:客户端可以一次性向服务器发送多条报文(服务器也向客户端进行应答),反之也一样。

这种模式会存在一个问题:客户端(接收端)怎么判断哪一个应答对应着刚才发的哪一个报文呢?

引入序号,先发的报文序号一定小,根据序号进行排序,进行tcp 的按序到达。

还有一个确认序号(往往是收到的序号+1):该确认序号之前的数据我已经全部收到了,下次发送请从确认序号开始。

序号存在的意义:按序到达、应答和确认对应

确认序号&&序号:向对方发送数据的同时,也在做应答。

为什么要有序号和确认序号两个字段,一个字段不也可以完成吗?刚才一直客户端给服务器发消息,别忘了tcp是支持全双工的,服务器也会向客户端同时发消息。服务器有可能是即想要做应答又想要发消息的这么一个状态(这种情况叫做捎带应答)。

为什么TCP报头中要有标志位?标志位存在的意义是什么?

先了解一下ACK、SYN、FIN这三个标志位,标志位其实就是位图当中唯一的bit位

tcp服务器(面向连接)通信之前要先建立连接,建立连接也是要通过发送报文来做的

我们的服务器一定会在正常工作时,一定会在通信过程中同时收到各种各样的报文,而TCP报文是需要类型的,那么就需要区分报文类型。所以标志位存在的意义就是为了区分不同的TCP报文类型

三次握手初理解

connet建立连接是需要进行三次握手的

实际上客户端和服务器发送的并不是单纯SYN、ACK而是TCP完整的报头,建立连接之前三次握手的时候是不需要发送任何数据的,只需要发送报头就行了(最后一个可能有点特殊),而发送SYN或者说SYN+ACK只是说将报头中的标志位置1了。

建立连接之前要进行三次握手,但是我怎么没有感觉呢?由双方操作系统自动完成。

由双方自动完成总要有个人先发起握手吧——我们之前调用的connect接口就是一个发起握手的邀请。三次握手完成之前connect一直处于阻塞状态,直到三次握手完成后返回值才为0.

listen状态

只有处于listen状态的服务器才允许受理我们的SYN报文,没有处于listen状态收到SYN报文,那么操作系统会直接丢弃。

accept();

当我们三次握手完成,服务器存在一个新的建立好的连接时,accept才会直接获取连接

 如何理解三次握手?三次握手是双方进行通信之前的协商工作。

建立连接?一个人如果有大量的女朋友,维护一个女朋友的"连接",本身是有成本的

如何理解连接?

对于server来讲,一定有很多client跟他建立连接,所以服务器得维护众多连接,所以服务器就得知道哪些连接是刚建立的,哪些是正在通信的,哪些是即将被释放的。

所以服务器方操作系统就要对连接进行管理——先描述,再组织。

所以连接是一种内核数据结构,至于连接的成本就是内存空间和时间(维护成本)

四次挥手

断开连接时需要四次挥手

建立连接前,需要client主动发起连接;建立连接后,双方关系对等,那么在断开连接时,必须双方"同意",也就是client发一次,server发一次

close(fd)不仅通信双方只有一个文件描述符(fd),client不用的fd,server一样也要关,所以所谓的close就是发起2次挥手,双方都要close就是4次挥手。

SYN:用来区分建立连接的报文

ACK:用来对报文做确认的

FIN:用来断开连接的报文

双方进行正常通信的时候

TCP是面向字节流的,我们把发送缓冲区想象成一个数组char outbuffer[num],那么缓冲区中,天然的,每个字节都会有序号了(下标)

新的序号 = 确认序号  + 我要发送的长度

超时重传机制

 如果发送方(比如client)收到了应答,那么我一定能保证对方收到了,如果我没有收到应答,那么就可能有以下两种情况:

1、可能服务器没收到报文

2、可能服务器收到报文,然后应答时丢失了

也就是说发送方并不清楚对方是否收到报文(也不清楚报文究竟有没有丢),当然如果服务器(接收方)应答了那就一定收到了

所以约定没有收到应答默认对方是没有收到报文的,然后就需要重传。

那么这里还会有一个问题,发送出去一个报文,就需要等对方会给你ACK(对方可能没收到、或者收到了应答半路丢包了),那么具体需要等多长时间呢?

在一个时间段内,没有收到应答那么就是——超时了

如果判断超时了——就会自动进行重传——超时重传机制

如果说应答没丢只是阻塞在网络里,然后超时重传了,这样不就会收到两个一样的报文了?

那么如果多次收到相同的报文应该怎么解决?序号 ——去重 + 有序

超时具体要等多少时间呢?因为网络状态是动态的——超时时间也是动态的

最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回"

但是这个时间的长短, 随着网络环境的不同, 是有差异的

如果超时时间设的太长, 会影响整体的重传效率;

如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间

Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控 制, 每次判定超时重发的超时时间都是 500ms 的整数倍

如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传

如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增

累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接

连接管理机制

在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接

accept不参与三次握手,connect只是发起三次握手

当我们的客户端发SYN时就要立马将自己的状态设置为SYN_SENT

当服务器收到SYN并发出应答时也要将状态设置为SYN_RECV

客户端收到应答时发送ACK,状态要设为ESTABUSHED,此时客户端认为已经建立连接(第三次握手没有应答),服务器收到ACK才会认为连接建立好了此时也会改变状态

三次握手:不是能100%建立好连接,而是经历三次握手后,客户端和服务器都认为连接建立好了

那么如果后面的ACK丢失了呢?客户端认为连接已经建立好了,而实际上服务器根本就没有收到ACK它也就不可能走到服务器的连接这一端(ESTABUSHED)。服务器端很有可能向客户端进行超时重传。然而客户端认为建立好了,又迫切的想要发数据,那么会发生什么呢?客户端会向服务器直接发送数据(对方还在进行超时等待)。

那么服务器就会向客户端发送RST(reset重置)报文

连接重置:就是让双方重新进行三次握手;RST解决的是建立连接出现异常的问题

为什么建立连接是三次握手而不是一次、两次握手?(经典面试题)

1、2次当然不行,建立连接是有成本的(服务器维护连接成本),那么如果一个客户端疯狂的给服务器发SYN,但是又不做处理(SYN洪水攻击),服务器就会因为维护连接而出现问题(server容易受攻击)。

当然3次握手也有可能受到攻击,但是3次握手为了解决的是建立连接的问题,而不是解决安全通信的问题,虽然解决的不是安全通信的问题,但至少没有明显的漏洞。1、2次握手有明显漏洞。

3次连接建立的前提是客户端先把ACK发给我服务器,也就是客户端先建立连接后,服务器再建立连接(所以一般不可能受到单机的攻击)

上面解释的主要是1、2次握手为什么不行。

为什么三次握手?

理由一:

双方通信,需要保证信道(网络)是健康的;三次握手,客户端、服务器双方都会有确定的一次收发(client、server双方确认全双工)

1次握手只能确认client能发;

二次握手只能确认客户端能够收发,服务器只能确认发(无法确认能够收)

理由二:

确保双方操作系统(TCP)是健康的且愿意建立通信的

中间的ACK+SYN被拆开来看了,那么三次握手本质其实也可以看作4次握手,只不过中间两次被捎带应答了

那么为什么不是5、6次呢,3次已经可以说明通信双方已经建立了通信的共识了,没必要浪费成本

这也可以解释为什么用四次挥手.

第一个FIN表示:服务器我要和你断开连接了(潜台词:客户端已经没有数据给服务器发了,从此以后客户端不给服务器发消息了)

后面客户端不是还发了个ACK吗?上面说的不客户端发消息主要是用户数据,通俗来讲就是客户端的发送缓冲区数据已经发完了,客户端没有数据可以往发送缓冲区里写了(它就把文件描述符关了close(fd))。而客户端发送的FIN、ACK只是报头并没有用户数据。

服务器发的FIN也是一个意思:发送缓冲区里没数据了。

4次挥手也是双方操作系统自动完成的

客户端上层不是把连接关了吗,那么后面服务器发的数据客户端怎么拿上来呢?

假设客户端只发了一个请求,但是它得收到应答之后才会调用close,万一客户端只想发送一个request后就不再发了只负责收。操作系统也提供了这样的接口。

四次挥手的状态变化

CLOSE_WAIT:

当服务器建立好连接,当客户端发起断开连接但是服务器不调用断开连接,那么服务器就会一直处在CLOSE_WAIT状态(close(fd)不关)

如果我们发现我们的服务器有大量的CLOSE_WAIT状态时,大概率我们的服务器写的有BUG,主要是没有close(fd)

TIME_WAIT:

主动断开连接的一方会主动进入的一种状态(等待一定时长)

client先断开      客户端IP+Port    服务器IP+Port      TIME_WAIT

server先断开    服务器IP+Port     客户端IP+Port      TIME_WAIT

如果服务器先断开连接处于一个TIME_WAIT的时间,对客户端来讲它的四次挥手已经完成,而服务器(主动断开连接的一方)处于TIME_WAIT状态所以它的四次挥手并没有完成,ip和端口依旧被占用了,所以绑定失败了。

为什么要等?等多长?

TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个 MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态.

 我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在 TIME_WAIT 期间仍然不能再次监听同样的 server 端口;

MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上 默认配置的值是 60s;

可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值

MSL:报文在网络当中的最大存活时长

为什么是 TIME_WAIT 的时间是 2MSL

断开连接时,网络当中还会有一些历史报文存在阻塞着,重新建立连接还是一样的ip和端口,万一这些历史报文突然又活了,服务器可能会对这些报文进行误判,这会对我们新建立的连接造成影响(虽然这种概率特别低)。

TIME_WAIT 的时间是 2MSL就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失

同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么 服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然 可以重发 LAST_ACK);

TCP流量控制

主机A向主机B发送消息发得太快了,导致对方的接收缓冲区被打满了(来不及接收);A不管A还在打,那B只能丢弃了。那丢弃的数据不是有超时重传吗?对,但是不合理(太浪费算力了)

主机B可以向主机A通告自己的接收能力(我的接收缓冲区剩余空间的大小)

接收端将自己可以接收的缓冲区大小放入TCP 首部中的 "窗口大小" 字段, 通 过 ACK 端通知发送端;

窗口大小字段越大, 说明网络的吞吐量越高;

接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通 知给发送端;

发送端接受到这个窗口之后, 就会减慢自己的发送速度;

如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.

如何通告?

确认应答机制,在自己的应答ACK报文中,填写16位窗口大小(填写自己的缓冲区剩余空间大小)

主机B接收能力变大了,而主机A并不知道,还在等(上次B的缓冲区满了,A停止发送),那么这种情况该如何处理呢?

主机A在停发期间定期会发窗口探测(只涵盖报头的报文)

主机B缓冲区更新期间会向主机A发窗口更新通知。

如果主机B一直不读接收缓冲区的内容(一直为0),主机A就得一直发窗口探测,这时主机A的PSH(push)标记位会被置1,催促主机B尽快读取缓冲区的内容(交付给上层)

标记位URG:紧急指针——让报文优先处理

主机A向主机B传递大量信息而且TCP是按序到达的,主机B全部接收正往上层交付,主机A又不想要这些信息了于是发了一个放弃上传的请求——而这条信息又排在最后(前面一堆信息堵着),此时就可以用URG

URG标志位被置为1,表示16位紧急指针字段被启用,可以填写特定数据(紧急数据)的偏移量,紧急数据通常只有1字节,只有1字节能够存啥呢,字节里面可以约定对应的码。

滑动窗口

前面确认应答的时候我们说过一般消息的传输不是这样的

对每一个发送的数据段, 都要给一个 ACK 确认应答. 收 到 ACK 后再发送下一个数据段,这样传输的发送效率低下

正常情况下是一次发送多条数据

主机A给主机B发送大量报文有个很重要的前提:一定要保证主机B能够接收到(来的及接收),不能超过主机B的接收能力

主机A必须支持:发送前四个段的时候, 暂时不需要等待任何应答(ACK), 直接发送

为什么第五个不能直接发送而是要等到有一个应答出现才会继续向主机B发送呢?

主机A需要暂定维护一个数值——暂时称为主机A的"窗口大小"

滑动窗口的大小:指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是 4000 个字节(四个段)

滑动窗口的本质是什么?在哪里?

滑动窗口其实是发送缓冲区的一部分,就在发送缓冲区当中

滑动窗口以内的数据:无需等待确认应答而可以直接发送

不考虑网络情况,滑动窗口的大小一般是对方接受缓冲区剩余空间的大小

思考一个问题——之前我们讲过的超时重传,对发送的报文并且没有收到应答的报文进行保存,方便我们进行重传。那么我们保存的报文究竟保存在哪?

保存在滑动窗口中。

还有一个问题如何理解滑动窗口?

滑动窗口只能向右滑动吗?可以变大吗?可以变小吗?可以为0吗? 

是的,只能向右移动,不能向左移动

可以变大,只要对方的瞬间处理很多数据,对方的缓冲区腾出空间来了,那么滑动窗口也就变大了

也可以变小,只要给对方的缓冲区不断填充,对方来不及处理,那么缓冲区可用空间就会变小,滑动窗口也就变小了

当对方缓冲区一直变小到最后接收不了数据了,那么滑动窗口就为0

深入理解滑动窗口

之前我们学过TCP是面向字节流的,那么其发送缓冲区可以看作一个char类型的数组,所以它的每个字节都有相应的序号(数组下标)

滑动窗口本质是发送缓冲区的一个范围,那么这个数组的两个下标就可以限制出一个滑动窗口

int start_win; int end_win;

滑动窗口的移动就是两个下标往后++;滑动窗口变大就是end++快,start++慢;滑动窗口变小就是start++快,end++慢;滑动窗口为0,就是end不变,start往后走直到两下标相遇。

我们滑动窗口的数据依次发给对方,对方依次对我做ACK,收到ACK报文,应答报头中的窗口大小表明对方的接收能力

变更滑动窗口大小,如何变更呢?

报头中有一个32位确认序号(对方确认收到报文,并给你序号数+1的确认序号应答)

举个例子 acp_seq = 1001(收到的确认序号) win = 2000;(窗口大小)

那么start_win = 1001;窗口大小就会变为end_win = start_win + win;

滑动窗口丢包

1.最左侧丢包

TCP设计的确认序号这时候就起作用了——确认序号的意义:确认序号之前的报文全部收到了

如果2000那块丢包了,3000、4000、5000全部收到了,那么对于主机B来讲它是不敢返回3001及其往后的确认序号的。

所以主机B只敢返回1001,所以主机A的滑动窗口绝对不会向右移动,应为主机A滑动窗口的左侧下标(start)的更新策略是根据对方的确认序号来的

如果发生丢包会有两种情况

这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认

还有一种数据包真的丢失了

快重传:收到3个同样的确认应答时进行重发,立即对1001这个丢失报文做补发

一旦快重传成功那么下次的确认序号就是7001

万一3001、4001啥的也丢了呢,你怎么保证这部分的包没有丢啊?你怎么知道他一定补发的是1001到2000这个报文,而不是补发3001、4001呢?

主机A只能确定1001到2000这个包一定丢了,3001、4001主机A并不能保证,难到不管吗?当然是要管的,你后面又不是不发了,到时候重新对3001、4001进行快重传不就行了。

那么如果没有连续收到3个同样的确认应答,只收到2个呢?那么只能进行超时重传,是给我们TCP可靠性重传机制做兜底的。

所以通过上面内容我们就可以得出一个结论——我们不担心最左侧丢失

2.中间报文丢失

前两个成功传输,主机B收到了,滑动窗口往右划两次,此时确认序号为3001。然后就收不到了,丢包了滑动窗口不能向右滑动——现在情况又变成最左侧丢包了——进行我们的快重传/超时重传

最右侧丢包同理

所以在滑动窗口内,所有丢包问题都会转换成最左侧丢包的问题

流量控制是如何做到的?主机A具体是怎么向主机B进行流量控制的?——滑动窗口

滑动窗口一直向右移动,当end_win++到发送缓冲区结尾甚至溢出呢?

不用担心,(思想上类似于环状结构这样就不会溢出了,实际上非常复杂)

 拥塞控制

虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题

网络中可不止一台、两台机器,网络中大量的主机和路由器,你需要通过一定的路径把信息交给对应的主机,我们要把信息从A送到B不可能只考虑最左侧的A点和最右侧的B点。

我们向一台主机发信息,丢几次、几十次包挺正常,我们重传就行了。一旦发的包9成都丢了,那就要考虑网络问题了。

因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络 状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的。

网络拥塞了——不能立即重传——拥塞控制                网络中可不止你一台主机要发消息,是一大堆主机都要发消息,所以一旦拥塞了,全员进行拥塞控制

如何进行拥塞控制?       

引入慢启动机制,先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

再引入一个概念:拥塞窗口

如果主机A(发送方)发送的数据量再拥塞窗口以内,基本上不会引起网络拥塞。如果单次发送的数据量超过拥塞窗口,大概率会发生网络拥塞。

发送方的数据data < int con_win(拥塞窗口)    而主机A发送的数据量取决于滑动窗口

所以                滑动窗口 =  min(接收方的窗口,拥塞窗口);//这两个窗口中更小的那个

接收方的窗口(接收能力/接收缓冲区剩余空间大小)

发送开始的时候, 定义拥塞窗口大小为 1;

每次收到一个 ACK 应答, 拥塞窗口加 1;

每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快

为什么采用2^n——慢启动这种算法作为拥塞控制的核心算法呢?

一旦有应答,说明网络状态慢慢变好了——那么就要尽快恢复通信,保证效率;那么这种指数级增长前期忙中后期快的特点就十分适合现在这种情况。

虽然滑动窗口还有接收方的窗口限制不会出问题,但是拥塞窗口这样一直变化下去不会出问题吗?

真实的网络状态是变化的,有时网速快有时慢,拥塞窗口也要变化的,这样才能根据不同的网络状态,来调节我们的滑动窗口。拥塞窗口也不是一直就这么增长下去

上次增长的一半作为阈值,然后下次增长到阈值时改为线性增长——探测网络的健康状态

当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;

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

少量的丢包, 我们仅仅是触发超时重传;

大量的丢包, 我们就认为网络拥塞;

当 TCP 通信开始后, 网络吞吐量会逐渐上升;

随着网络发生拥堵, 吞吐量会立刻下降;

拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.

延迟应答

对方(接收方)收到了发送方的报文,然后应答等一会再发给你(ACK确认)

本质:延迟一部分应答,让接收方给对方通告一个更大的窗口

如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.

假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;

 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费 掉了;

 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;

 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的 窗口大小就是 1M;

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高。

我们的目标是在保证网络 不拥塞的情况下尽量提高传输效率;

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

TCP进行传输时为什么要分多次传输(比如下图为什么4000分为4个1000来发,而不是一次传输4000呢)

数据链路层规定——单个发送数据的报文有效载荷必须是MTU以内的字节,而这个MTU一般是1500字节,可以用ifconfig命令查看一下

还有一个问题:双方建立通信(连接)时不是要进行三次握手嘛,三次握手后"我(发送方)"并不清楚对方的接收缓冲区的大小,那么我应该在第一次发送数据时发送多少呢?

注意:首次发数据,又不是首次发报文;三次握手时双方在交换报头,报头中就包含了窗口大小。

面向字节流

为什么TCP报头中没有  有效载荷的长度?TCP不需要(有效载荷)

tcp发送的报文在TCP协议层面上是没有边界的,我们只需要把接收到的数据放到缓冲区里。

创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

调用 write 时, 数据会先写入发送缓冲区中;

如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;

如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或 者其他合适的时机发送出去;

接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;

然后应用程序可以调用 read 从接收缓冲区拿数据;

另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一 个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

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

例如:

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

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

粘包问题

之前我们在写TCP时遇到过,包指的是应用层的数据包,而TCP缓冲区中只是一串字节流,而区分报文和报文之间完整性的是由应用层来做的。

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

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

站在应用层的角度, 看到的只是一串连续的字节数据

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

那么如何避免粘包问题呢?

归根结底就是一句话, 明确两个包之间的边界.

对于定长的包, 保证每次都按固定大小读取即可;

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

对于变长的包, 还可以在包和包之间使用明确的分隔符

TCP 异常情况

客户端和服务器双方在进行正常通信的时候,突然出现服务器或者客户端中的某一个应用层进程被中止了/崩掉了,创建套接字的本质就是打开了一个文件,那么也就意味着当我们在进行正常通信的时候,当我们的进程退出了,打开的文件就会关闭(文件(fd)的生命周期是随进程的)。文件、连接关闭自动会触发4次挥手。

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

机器重启: 和进程终止的情况相同。(win系统在关机时,如果有些记事本、word或者是一些应用,windows会弹出窗口提示你某些应用还未正常关闭/数据还未保存,是否要关闭/保存这些。操作系统要关闭的前提是要保证所有进程安全退出。

通信时有一台机器掉电了/网线断开了,这种情况不仅连服务器反应不过来,连客户端自己都反应不过来,你拔掉网线是一种物理层面上的隔离,电脑突然就被踢出互联网了。那么曾经的连接会怎么办呢?访问网页时应该都遇到突然断网的情况,浏览器画面可能就崩溃了,然后提示网络状态异常,请检测后重试。那么对于客户端来讲这个连接肯定已经不存在了,那么对于接收方来讲呢?

接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset。

即使没有写入操作, TCP 自己也内置了一个保活定时器, 会 定期询问对方是否还在。如果对方不在, 也会把连接释放。

http://www.dtcms.com/a/569366.html

相关文章:

  • useState 真的那么简单吗?我在项目里踩过的坑
  • 如何用5种实用方法将电脑上的音乐传输到安卓手机
  • 做网页到哪个网站找素材物流网站有哪些
  • MP4视频播放问题
  • HR8837:赋能低压直流电机的高效安全驱动芯片
  • Linux源码安装FFmpeg和av库
  • 亳州市城乡建设局网站ps设计网站首页效果图
  • Syncthing Linux 部署教程
  • 做疏通什么网站推广好网页制作软件 ad
  • html 和css基础常用的标签和样式(2)-css
  • 【数据集】【YOLO】【目标检测】共享单车数据集,共享单车识别数据集 3596 张,YOLO自行车识别算法实战训推教程。
  • Coze-AI智能体开发平台5-Coze的API与SDK
  • 河南网站建设优化技术网站建设与维护学什么科目
  • 超越简单的回放:深度解析国标GB28181算法算力平台EasyGBS的录像检索与回放技术
  • HCIP Datacom 认证难度高吗?零基础能考吗?
  • 代码实战:PHP爬虫抓取信息及反爬虫API接口
  • CentOS 7 停止维护后 YUM 源配置速查手册
  • TypeScript 类型系统 ------公司项目实战 + 面试通关指南
  • 东莞网络网站建设深圳建设局网站注册结构师培训
  • 做网站推广链接该怎么做那曲地区建设局网站
  • AI研究-120 DeepSeek-OCR 从 0 到 1:上手路线、实战要点
  • 2025,5月试卷|错题笔记
  • Syslog基础详解:协议、服务器、端口和实时监控
  • rk3568-android11-wifi-aic8800
  • 东城区网站排名seo网站 动态 静态
  • 网站就业技术培训机构seo需要掌握什么技能
  • CUDA C++编程指南(4)——硬件实现
  • Nacos集群部署实战:3节点+Nginx+MySQL高可用方案
  • 深入理解五种 IO 模型与非阻塞 IO:从原理到场景选型
  • 大专生升学与职业发展路径探析:从专升本到能力进阶