文章目录
- [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地址和端口号
其中端口号划分为两部分:
- 0~1023:一些知名协议已经绑定了的端口号
- 1024~65535:OS随机分配的端口号
在OS内部,端口号与进程之间的关系类似于哈希表,通过键值port能找到唯一的进程与之对应
同时,也能说明,一个进程能够绑定多个端口号,但一个端口号只能被一个进程绑定
1.2 UDP协议格式
-
如何将报头与有效载荷分离?
UDP报头字段固定8字节,读取其中的16位UD长度,减去8字节就是有效载荷的长度
-
如何分用?
根据报头中的16位目的端口号将有效载荷向上层交付
1.3 UDP特性
- 无连接
- 不可靠
- 面向数据报
无连接,在socket编程章节中已有所体现,使用udp
通信的双方创建好套接字就能直接通信
而对于不可靠这一特性,讲完tcp
的可靠性,自然也就懂了
所谓面向数据报,发送方发多少,接收方就接多少,不能灵活控制发送和接受的次数
udp
注意事项:udp
没有发送缓冲区,调用sendto
直接由内核发送,但有接受缓冲区;udp
报头字段中的16位长度表明udp
最大长度是64KB,如果发送的数据超过64KB,则需要分批发送
1.4 报文的封装
所谓的报头,在内核中其实就是结构体字段
在应用层,由于双方操作系统、机器大小端、计算机语言等的不同,发送的数据需要先进行序列化
而传输层属于内核,内核的部分双方几乎一样,都是用C写的,进行序列化再传输反而增加了传输量
因此,横向来看,udp
报头是以结构体直接发送的
纵向来看,在OS中,可能同时存在各种各样的报文,有准备向上交付的,有准备向下交付的,有要丢弃的...那么OS就需要对这些报文进行管理 ----- 先描述,再组织;所以,报文的本质也是一个内核数据结构,它有非常多的属性,其中我们重点了解两个指针
当应用层向下交付时,数据存放在内核中的一段内存上,该内存前面预留了一些空间
最开始,sk_buff
中的head
和data
都指向数据的起始位置
当要封装udp
报头时,head
向前移动udp
报文长度个字节,并赋好值,这样就完成了报文的封装,往下依旧如此
2. TCP协议
2.1 TCP协议格式
-
如何将有效载荷与报头分离?
tcp
协议格式中的4位首部长度,每bit位表示4字节,总共能表示[0, 60]字节tcp
协议格式前20字节固定,取出后,拿4位首部长度 - 20字节就是选项,剩余的就是数据 -
如何分用?
接收方根据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点:
- 对发送的报文进行唯一性确认
- 对报文进行去重
- 使报文按序到达
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
四次挥手
在这里,又有几个问题
-
根据上文说的,能不能将server发给client的ACK与server要与client断开连接FIN报文合并?
不能,一方想要与对方断开连接,原因是我要发给对方的数据已经发完了,所以我要和对方断开连接,但对方要给我发送的数据不一定发完
因此,client发送的数据发完,就和对方断开连接,此时server仍可能要给client发送数据,等到server的数据发完,由server自主发起断开连接
这也是为什么断开连接叫四次挥手
-
根据现有的知识,一个
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洪水的问题,但最重要的是:
- 验证了双方网络的连通性
- 以最小的成本建立双方通信的共识
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 滑动窗口
前面我们介绍了超时重传机制和流量控制,但仍有问题:
- 已经发送的报文,在接受到ACK之前或者超时重传时间之内,不能被丢弃,需要保存起来,保存在哪?
- 通过窗口大小已经得知对方的接受能力,如何根据对方的接受能力进行数据的发送呢?
为了解决上述问题,tcp
引入滑动窗口的概念,规定,在滑动窗口以内的数据可以直接发送,暂时可以不做应答
所谓的滑动窗口,实际就是发送缓冲区的一段区域,于是,滑动窗口将发送缓冲区分成三部分
到这里,我们可以粗略的将滑动窗口定义为:滑动窗口 = 对方的接受能力(对方接受缓冲区剩余空间的大小)
于是,我们根据滑动窗口的定义,来判断下面的问题:
-
滑动窗口能不能向左移动?
已发送,已应答的数据无需再发送,因此滑动窗口只能向右移动,不能向左
-
滑动窗口能不能变大?变小?为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 # 将文件中的报文信息显示