udp && tcp协议

文章目录

  • [1. UDP协议](#1. UDP协议)
    • [1.1 端口号](#1.1 端口号)
    • [1.2 UDP协议格式](#1.2 UDP协议格式)
    • [1.3 UDP特性](#1.3 UDP特性)
    • [1.4 报文的封装](#1.4 报文的封装)
  • [2. TCP协议](#2. TCP协议)
    • [2.1 TCP协议格式](#2.1 TCP协议格式)
    • [2.2 TCP策略](#2.2 TCP策略)
    • [2.3 面向字节流](#2.3 面向字节流)
    • [2.4 粘包问题](#2.4 粘包问题)
  • [3. TCP全连接队列](#3. TCP全连接队列)
    • [3.1 backlog](#3.1 backlog)
    • [3.2 全连接队列的原理](#3.2 全连接队列的原理)
  • [4. `tcpdump`](#4. tcpdump)

1. UDP协议

1.1 端口号

在前面的文章中说过,每一个要网络通信的进程,都要绑定IP地址和端口号

其中端口号划分为两部分:

  1. 0~1023:一些知名协议已经绑定了的端口号
  2. 1024~65535:OS随机分配的端口号

在OS内部,端口号与进程之间的关系类似于哈希表,通过键值port能找到唯一的进程与之对应

同时,也能说明,一个进程能够绑定多个端口号,但一个端口号只能被一个进程绑定

1.2 UDP协议格式

  1. 如何将报头与有效载荷分离?

    UDP报头字段固定8字节,读取其中的16位UD长度,减去8字节就是有效载荷的长度

  2. 如何分用?

    根据报头中的16位目的端口号将有效载荷向上层交付

1.3 UDP特性

  1. 无连接
  2. 不可靠
  3. 面向数据报

无连接,在socket编程章节中已有所体现,使用udp通信的双方创建好套接字就能直接通信

而对于不可靠这一特性,讲完tcp的可靠性,自然也就懂了

所谓面向数据报,发送方发多少,接收方就接多少,不能灵活控制发送和接受的次数

udp注意事项:udp没有发送缓冲区,调用sendto直接由内核发送,但有接受缓冲区;udp报头字段中的16位长度表明udp最大长度是64KB,如果发送的数据超过64KB,则需要分批发送

1.4 报文的封装

所谓的报头,在内核中其实就是结构体字段

在应用层,由于双方操作系统、机器大小端、计算机语言等的不同,发送的数据需要先进行序列化

而传输层属于内核,内核的部分双方几乎一样,都是用C写的,进行序列化再传输反而增加了传输量

因此,横向来看,udp报头是以结构体直接发送的

纵向来看,在OS中,可能同时存在各种各样的报文,有准备向上交付的,有准备向下交付的,有要丢弃的...那么OS就需要对这些报文进行管理 ----- 先描述,再组织;所以,报文的本质也是一个内核数据结构,它有非常多的属性,其中我们重点了解两个指针

当应用层向下交付时,数据存放在内核中的一段内存上,该内存前面预留了一些空间

最开始,sk_buff中的headdata都指向数据的起始位置

当要封装udp报头时,head向前移动udp报文长度个字节,并赋好值,这样就完成了报文的封装,往下依旧如此

2. TCP协议

2.1 TCP协议格式

  1. 如何将有效载荷与报头分离?

    tcp协议格式中的4位首部长度,每bit位表示4字节,总共能表示0, 60字节

    tcp协议格式前20字节固定,取出后,拿4位首部长度 - 20字节就是选项,剩余的就是数据

  2. 如何分用?

    接收方根据16位目的端口号向上交付

2.2 TCP策略

我们知道,tcp协议是可靠的、面向连接的,那么tcp是如何保证可靠性,如何连接的呢?这与tcp策略息息相关

2.2.1 确认应答机制(ACK)

发送方发送的报文,可能在传输的过程中丢了,也可能对方收到了,那发送方如何得知报文究竟是丢了还是对方收到了?

很简单,如果对方收到报文,只要给我个应答,就证明我之前发送的数据对方一定收到了;相反,如果我没收到应答,则证明我发送的报文对方没收到

但是又有问题了,S怎么得知应答对方一定收到了呢?在这里,我们规定,不对应答进行应答,因为如果对应答进行应答,就会无限套下去,C又怎么得知自己的应答对方一定收到...

因此,对于应答,我们无法保证可靠性,但只要收到应答,历史发送的数据对方一定收到;而如果没收到应答,不管是报文丢失,还是应答丢失,发送方就认为对方没收到报文

所谓可靠性,指的是对方收到报文了,我能知道,对方没收到报文,我也能知道

我们把这种确认应答机制也叫做ACK机制,当进行应答时,需要将tcp报头字段中的ACK标志位置1,双方都采用ACK机制,就能保证数据传输的可靠性

序号与确认序号

tcp有两种通信模式,一种是向上述那样,发送一个报文,回一个应答

但更常用的则是:发送方可以一次发送多个报文,接收方在接收到报文后分别对每个报文进行应答,从而提高通信效率

那么接收方对每个报文应答后,发送方如何确认哪个应答对于哪个报文呢?这就需要tcp报头中的序号与确认序号

当发送方发送报文时,需要对该报文填写唯一性标识 ---- 序号,当接受方收到报文进行应答时,填写确认序号,其中确认序号 = 序号 + 1

其中确认序号的含义是:+1之前的数据已经全部收到,下次从+1的数据开始发

比如当发送方发送序号为2000的报文,接收方应答时确认序号填写2001,如果接收方收到应答,则认为2000之前的数据对方已经全部收到,下次从2001开始发

同时,如果应答部分丢失,只要收到最近一次报文的应答,就认为之前的报文对方一定收到,因此,该做法允许部分应答丢失

从上述例子看,貌似tcp只需要一个序号也能对每个发送的报文进行确认,当发送时,填写序号,应答时,将序号当作确认序号来填,不也ok吗?为什么要以序号 + 确认序号的方式?

当发送方发送报文时,对方进行应答的同时,也想发送数据,此时为了提高通信效率,接收方将ACK与要发送的数据作为一个报文,将ACK标志位置1 + 填写确认序号表示应答,填写序号 + 数据表示要发送的数据;在这种情况,就需要序号 + 确认序号

6个标志位

udp协议中我们说过,OS内有各种各样的报文,在tcp这里更是如此,有应答报文、还有后面讲的发起连接的报文、断开连接的报文等等

因此,OS要对它们进行管理的同时,还要能够对它们进行区分,如何区分报文?根据tcp报头中的6个标志位

序号的理解

根据我们学过的知识,应用层调用sendto,本质是将数据拷贝到发送缓冲区,在这里,我们不妨将缓冲区看作字符数组,于是每一字节就天然的有一下标,序号的本质就是发送缓冲区数组的下标

当确认序号为1001时,表示1000之前的数据已经收到,下次从下标为1001的位置开始发

2.2.2 超时重传机制

当发送方发送报文,在一段时间以内,没有收到应答,此时就认为报文丢失,会重新发送报文,我们把这种机制叫做超时重传机制

那么超时时间应该是多少呢?由于报文是经过网络传输的,而网络的状态是浮动的,有时好,有时坏,当网络好时,超时时间应该短点,以提高效率,但网络坏时,超时时间应该长些,因此,超时时间也应当是浮动

一般来说,linux下的超时时间起初是500ms,第二次发送没收到应答时,超时时间变为2 * 500ms,以此类推,如果连续多次超时重传后还是无法收到应答,则关闭连接

没有收到应答的原因可能是报文丢失,也可能是应答丢失,如果是应答丢失,表明对方收到报文,如果重新发送报文,且对方收到,此时对方的OS内就有两份相同的报文;OS会根据序号来判断两份报文是否相同,如果相同,则丢弃新报文的并给出应答

tcp通信模式中,常见的是第二种,多个报文一并发送时,由于报文是经过网络传输的,它们走的线路不同,到达对方主机的时间也就不同,也就意味着发送的报文可能是有序的,但接受时可能是无序的,此时,OS会根据报文的序号来排序,确保向上层交付的数据是有序的

总上来看,序号的作用主要有3点:

  1. 对发送的报文进行唯一性确认
  2. 对报文进行去重
  3. 使报文按序到达

2.2.3 连接管理机制

三次握手

我们总是tcp是面向连接的,那么它是如何建立连接的?

使用tcp协议通信的双方,在通信之前需要建立连接,通常由client发起连接

当server调用listen后,进入LISTEN状态,client调用connect,发起连接,连接报文中不能携带数据,将SYN标志位置1,表示是连接报文,询问sever,我要和你建立连接,你是否同意

server收到报文后,同意建立连接的同时,反问client,我同意你到我的一个连接,但我也要和你建立连接,你是否同意

client收到报文后,同意server到client的连接

在上述过程中,client发起SYN,收到SYN + ACK,发送ACK,总共收发3个报文;server收SYN,发SYN + ACK,收ACK,也总共收发3个报文

我们把上述建立连接的过程叫做tcp的三次握手

那么问题来了,如果client发出的最后一个ACK丢失了呢,由于无法保证应答的可靠性,不就代表连接建立失败了吗?事实上,的确如此,所以建立建立的本质就是在赌最后一个ACK对方一定收到

那如果最后一个ACK就是丢了呢?其实也是有办法的,要注意,对于client,在发送最后一个ACK时,它到server的连接已经建立好

但对于server,只有收到ACK时,它到client的连接才建立好

也就是说client->server的连接与server->client的连接建立好之间,有一段时间间隔

在这段间隔之内,也就是server->client的连接建立好之前,client有可能已经给server发送了数据报文,如果最后一个ACK丢了,而server又收到了client发送的数据报文,此时server判断自身连接还没建立,就会给client发送一个将RST标志位置1的报文,表示异常连接,要求client重新建立连接,client收到后,就会断开连接,重新发起三次握手

不仅仅是上述示例中,在通信的任何时刻,只要发现tcp连接异常,都可以向对方发送RST标志位置1的报文,重新建立连接

四次挥手

建立连接后,双方正常通信,突然client不想通信了,关闭了文件描述符,于是发起断开连接的报文 --- 将FIN标志位置1,询问对方,我想和你断开连接,你是否同意,server同意了,给了应答

过会,server也不想和client通信了,于是也关闭文件描述符,发起断开连接的报文,server要client断开连接,问client是否同意,client也同意,至此,双方的连接彻底断开

同理,client和server收发报文各自总共4次,我们也把双方断开连接的过程叫做tcp四次挥手

在这里,又有几个问题

  1. 根据上文说的,能不能将server发给client的ACK与server要与client断开连接FIN报文合并?

    不能,一方想要与对方断开连接,原因是我要发给对方的数据已经发完了,所以我要和对方断开连接,但对方要给我发送的数据不一定发完

    因此,client发送的数据发完,就和对方断开连接,此时server仍可能要给client发送数据,等到server的数据发完,由server自主发起断开连接

    这也是为什么断开连接叫四次挥手

  2. 根据现有的知识,一个fd对应一个连接,一个连接在传输层对应两个缓冲区,发送和接受缓冲区,既然client已经关闭fd了,也就是销毁发送和接受缓冲区了,为什么还能接受server发来的数据?

    这里的close(),我们可以理解为shutdown(sockfd, SHUT_WR),也就是只关闭了发送缓冲区,等到双方的连接都断开,才是真正的close()

需要注意的是,client应答ACK后,到server收到应答,需要一段时间,这也是为什么之前我们写tcp服务器时,关闭服务器后又立马重启发现绑定端口号错误的原因,只要没收到应答,连接就还在,端口号就还在被占用,也就绑定失败了

理解三次握手

建立连接,为什么是三次握手?

在OS内,连接有很多种,有正在三次握手的连接、正在四次挥手的连接、正常通信的连接...OS需要对这些连接管理 --- 先描述,再组织,连接在OS内本质也是内核数据结构,需要花费空间创建结构体对象,花费时间管理,这意味着管理连接是需要成本的

首先,来看一次和两次为什么不行,一次不用说了,SYN报文的可靠性都不能保证;而两次中,client发送SYN,server收到应答就建立好连接,由于建立连接的成本太低,client可以向server发送大量建立连接的报文,而server对每个连接都默认接受,导致server承受过重压力,可能会引发SYN洪水问题

而三次握手,虽然也可能存在SYN洪水的问题,但最重要的是:

  1. 验证了双方网络的连通性
  2. 以最小的成本建立双方通信的共识

client能收、能发,client->server网络正常,server也能收、能发,server->client网络正常,双方网络连通

client想和server通信,server同意了,server也想和client通信,client同意了,双方达成共识

以上两点,一次两次握手都达不到,因此,这才是为什么tcp建立连接是三次握手的原因

理解四次挥手

实际上,跟三次握手同理,四次挥手之所以叫做四次挥手,在于它以最小的成本建立双方断开连接的共识

根据我们四次挥手图的理解,对方在收到建立连接的报文后,如果不关闭fd,会处于CLOSE_WAIT的状态;同时,主动断开连接的一方,在收到对方断开连接的报文后,会处于TIME_WAIT的状态,下面我们来验证

我们拿tcp_echo_server来验证

那么主动断开连接的一方为什么最后要处于TIME_WAIT的状态?

该状态维持多长时间?

能不能不处于该状态,好让下次服务器直接重启?

报文是经过网络传输的,当断开连接时,报文可能还在传输的路上,如果连接直接断开,又立马重启了,这时就可能收到上次在网络中遗留的报文,影响到了本次通信,因此,处于TIME_WAIT 的状态就是为了等待这些报文到达后,将它们丢弃;也有另一个原因,就是最后一个ACK,server不一定收到,超时时间后,server会再次发送FIN,此时client还没退出,就能再ACK,确保server也能退出

处于TIME_WAIT的状态的时长一般是2 * MST,这里的MST是报文的最大生存时间,根据操作系统而变

shell 复制代码
cat /proc/sys/net/ipv4/tcp_fin_timeout # 查看当前系统的MST

要想client退出后,不进入TIME_WAIT状态,将server的listensockfd设置以下即可

2.2.4 流量控制

应用层调用write,将数据拷贝到发送缓冲区,由tcp控制数据的传输;如果对方的接受缓冲区满了,那么到达的报文也就直接丢弃了,这种做法虽然可行,但却不合理,报文的传输消耗了网络资源,CPU等硬件资源,却就这样被丢弃

因此,tcp在传输数据时,应该要考虑对方的接受能力,根据对方接受能力的大小来控制数据应该发多少,我们把这种策略叫做流量控制

所谓对方的接受能力,实际就是对方接受缓冲区剩余空间的大小

那么,发送方该如何得知对方的接受能力呢?

这就引入tcp报头字段中的"16位窗口大小"了,在对报文应答时,不仅要将ACK标志位置1,有数据的加数据,还要将自身发送缓冲区剩余空间的大小填入16位窗口大小中,这样,对方在收到ACK时,就能得知我当前的接受能力

tcp报文字段中的选项中,可能会有窗口扩大因子的选项,实际的窗口大小 = 窗口大小 << 窗口扩大因子

实际上,在tcp三次握手时,双方就是在告知对方自身最初的接受能力

在极端情况下,对方的接受能力为0,此时发送方停止发送数据,对方也就不ACK了,对方的接受能力也就无从得知了,此时如何判断对方有接受能力了,从而继续发送数据?

一方面,接收方会时不时的发送窗口更新通知的报文,主动告知自身的接受能力;另一方面,发送方也会时不时的发送窗口探测报文,对方只要ACK,就能得知其接受能力

那如果对方的接受能力仍然是0呢?发送方可以发送PSH标志位置1的报文给对方,提醒对方尽快将缓冲区里的数据向上交付;任何时候,只要希望数据尽快向上交付,都能将PSH标志位置1

至此,还剩一个URG标志位,它通常配合16紧急指针使用,为1表示紧急指针有效,否则无效;紧急指针实际是一个偏移量,指向接受缓冲区中某1字节;有时,我们给对方发送数据,在某一时刻希望取消发送数据,于是发送URG标志位置1的报文,但如果接收方依旧按序接受报文,当读到URG标志位,前面的报文已经处理完毕,又要撤回,相当于前面的工作白做了;因此,tcp提供了某些报文"插队"的做法,一般两个进程,一个正常读取,一个快速读取URG标志位,如果为1,则告知另一个进程取消读取

2.2.5 滑动窗口

前面我们介绍了超时重传机制和流量控制,但仍有问题:

  1. 已经发送的报文,在接受到ACK之前或者超时重传时间之内,不能被丢弃,需要保存起来,保存在哪?
  2. 通过窗口大小已经得知对方的接受能力,如何根据对方的接受能力进行数据的发送呢?

为了解决上述问题,tcp引入滑动窗口的概念,规定,在滑动窗口以内的数据可以直接发送,暂时可以不做应答

所谓的滑动窗口,实际就是发送缓冲区的一段区域,于是,滑动窗口将发送缓冲区分成三部分

到这里,我们可以粗略的将滑动窗口定义为:滑动窗口 = 对方的接受能力(对方接受缓冲区剩余空间的大小)

于是,我们根据滑动窗口的定义,来判断下面的问题:

  1. 滑动窗口能不能向左移动?

    已发送,已应答的数据无需再发送,因此滑动窗口只能向右移动,不能向左

  2. 滑动窗口能不能变大?变小?为0?

    滑动窗口 = 对方的接受能力,对方应用层调用read,接受能力就变大了,滑动窗口也就变大了;同理,滑动窗口也能变小,甚至为0

又根据之前的知识,缓冲区可以认为是一个字符数组,我们进一步将滑动窗口精细化的描述

滑动窗口本质是数组中的两个下标

现在,我们已经知道滑动窗口的具体含义了,可以根据滑动窗口来进行数据的发送了

那么问题来了,如果发送的报文丢了呢?

拿上图举例,如果最左边报文丢失,接收方在对其他报文进行应答时,根据确认序号的定义,只能填0,因为1000的报文没收到,当发送方收到3个以上的相同应答,触发快重传机制,立即补发1000的报文,接收方可以直接ACK4001,表示4000之间的报文已经收到

对应滑动窗口,win_start不动

如果是中间报文丢失,接收方ACK确认需要填1001,win_start右移,此时问题变成最左边报文丢失

最右边报文丢失同理,最终变成最左边报文丢失问题

上面提到的快重传机制与超时重传机制有何不同?

快重传是连续收到3个以上同样的应答所触发;如果在通信末期,可能报文丢失,但收不到3个以上的同样的应答,此时超时重传能进行报文的补发;可以说,超时重传是为报文的可靠性兜底的

如果是ACK丢失呢?这点我们之前说过,允许ACK丢失,只要收到最后的ACK,就能保证前面的数据对方收到了

因此,重新看待最开始的两个问题,收到ACK之前,报文保存在哪?---- 滑动窗口中;如何根据对方的接受能力控制数据的发送---滑动窗口内的数据就是能直接发送给对方的数据

2.2.6 拥塞控制

报文是经过网络传输的,网络有时好,有时坏;因此,有时报文的丢失可能是因为网络的原因

如何得知报文的丢失是因为网络的原因还是对方主机的原因呢?

如果发送100报文,其中小部分丢失,则判断网络状况正常,而如果大部分都丢失了,则认为是网络的原因,网络出现了拥塞问题

一旦网络出现拥塞问题,就要降低发送报文的数量,由于所有主机传输数据都遵循tcp协议,因此当网络出现拥塞,所有主机都能降低报文发送的数量,解决拥塞问题的关键在于,所有主机都有拥塞避免的共识

tcp引入拥塞窗口,它的含义是导致网络拥塞的最大报文数量

因此,在最开始,我们发送报文的数量不能超过导致网络拥塞的报文数量,滑动窗口 = min(对方的接受能力,拥塞窗口)

由于网络是浮动的,拥塞窗口也应当浮动,如何得知当前网络的拥塞窗口?一定需要多次探测

为什么拥塞窗口,tcp采用慢启动机制,采用2^n的数学模型来控制报文数量

采用2^n的好处是,一开始,报文数量少,能慢慢增加报文的数量,等待网络恢复;一旦网络恢复,因其增幅快的特性,能快速进入正常通信

但也不能一味的增长,我们需要准确探测到当前的拥塞窗口,当以2^n增长到某一阈值时,改为线性探测,知道导致网络拥塞,将新的阈值改为拥塞窗口的一半,继续采用慢启动机制...

2.2.7 延迟、捎带应答

当接受方接受到报文时,并不会立马做应答,而是会延迟一段时间,在这段时间内,应用层可能将数据取走,这样就能给发送方一个更大的窗口大小,以提高通信效率,我们把这种策略叫做延迟应答

而捎带应答,我们之间也见过,当给对方应答时,也想发送数据,就能把ACK和数据一并发送,tcp三次握手就是做了捎带应答

2.3 面向字节流

对于udp,发送方发的,就是接收方收的,不能灵活控制发送次数和接受次数

对于tcp,发送方可以多次发送,接收方也能分多次接受 --- 面向字节流

2.4 粘包问题

对于tcp,接收方收到报文将数据放到接受缓冲区,对于应用层来讲,数据全是字符串,也就是各种数据黏在一起,怎么区分一个完整的数据 --- 序列化与反序列化

3. TCP全连接队列

3.1 backlog

为了解释listen的第二个参数backlog具体有何作用,将服务器不对任何连接accept,并设置backlog为2,将3个client与server连接

确实存在三个client->server的连接,三个server->client的连接,同时也证明accept不参与三次握手的过程

当再有client连接时

client->server连接处于SYN_SENT的状态,这表示SYN报文已经发出,但server没同意建立连接,同时没有server->client的连接

当sever来不及accept时,允许client与server继续建立连接,但最多只能有 backlog + 1个连接

在OS中,来不及accept的连接会以队列的方式管理起来,我们把管理这些连接的队列叫做全连接队列

3.2 全连接队列的原理

需要注意的是,不是指服务器只能接受backlog + 1个连接,而是来不及接受的连接最多只能是backlog + 1个

backlog的值不建议太大,也不能为0,如果太大,后面的连接就需要等待较长时间,如果为0,等待server需要accept连接时,还需要继续三次握手

当前server来不及accept,等待空闲了,直接从全连接队列中拿,本质是减少了服务器的闲置率,提高用户的体验

从内核层面,我们理解连接

4. tcpdump

shell 复制代码
sudo tcpdump -i any      # 捕获任意报文
sudo tcpdump -i any tcp  # 捕获任意tcp报文

sudo tcpdump -i eth0 tcp # 捕获指定网络接口的tcp报文

sudo tcpdump src host xxx and tcp # 捕获指定源ip的tcp报文
sudo tcpdump src host xxx and dst host xxx and tcp # 捕获指定源ip和目的ip的tcp报文

sudo tcpdump port xxx and tcp # 捕获指定端口的tcp报文

sudo tcpdump -n port xxx and tcp -w xxx.pcap # 将捕获的tcp报文写到文件中
sudo tcpdump -r xxx.pcap # 将文件中的报文信息显示
相关推荐
HLC++13 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室14 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy16 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕17 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫18 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生19 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1120 小时前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器