目录
前几期我们已经学习完了基于应用层的Http和Https协议,本期我们将继续学习网络通信协议栈中的应用层下一层传输层的重要协议。
重新认识端口号
通过前几期的学习我们知道,公网IP可以唯一确认计算机网络中的一台主机,而一个端口号可以唯一确认一台主机上的唯一一个进程,所以一个IP+端口号,可以唯一的确认一台主机上的一个进程。我们又把IP+端口号统称为socket,我们又称网络通信的本质其实就是进程间通信。
端口号:端口号(Port)标识了一个主机上进行通信的不同的应用程序,端口号是一个16位数字。
基于上述知识,我们再来回顾两个问题,一个进程可以绑定多个端口号吗?一个端口号可以绑定多个进程吗?
此时这个问题的答案就显而易见了,因为一个端口号唯一的确认一台主机上的一个进程,所以一个端口号只能绑定一个进程,而一个进程是可以绑定多个端口号的,因为进程是无限的,而端口号是有限的。
端口号划分
因为端口号是16位的数字,表示的整数的范围是0~65535,操作系统会对这些端口号进行划分。
- 0~1023:知名端口号,HTTP,FTP,SSH这些广泛使用的协议的端口号所处的范围,它们的端口号是固定的。
- 1024~65535,操作系统动态分配的端口号,如我们之前写的client端套接字代码不用绑定IP+端口号,这是在client端发送数据时,操作系统自动为其绑定的。
常见的知名端口号如下。
- ssh服务器,22号端口。
- ftp服务器,21号端口。
- telnet服务器,23号端口。
- http服务器,80端口。
- https服务器,443端口。
注意:以上服务器端口是固定的,不会进行变化的。
netstat
netstat可以搭配选项查看网络状态。
- n :不显示别名,将能显示数字的全部转化成数字
- l :仅列出有在 Listen (监听) 的服务状态
- p: 显示建立相关链接的程序名
- t (tcp):仅显示tcp相关选项
- u (udp):仅显示udp相关选项
- a (all):显示于显示当前系统的所有网络连接、监听端口以及相关的网络统计信息。
pidof
pidof用于查看进程id。
之前我们也学习过使用ps -ajx的指令来查看特定进程的id,但是今天学习的pidof查看进程id更加方便。
使用ps -ajx查看mysqld进程id。
使用pidof 'mysqld'查看进程id。
UDP协议
UDP协议使我们在网络协议栈的传输层中学习的第一个协议。
UDP报文如下图所示。
在日后学习协议时,我们通常会抛出两个问题。
- UDP报文是如何实现封装和解包的?
- UDP报文是如何实现分用的?
我们先来回答第一个问题,UDP报文通过对有效载荷添加8字节报头进行封装;在进行解包时,因为UDP报文中有一个16位UDP长度,我们规定一个长度的基本单位是一个字节,这个16位UDP长度所代表的字节数及时UDP有效载荷和报头总共的大小,所以可以根据16位UDP长度计算出报头和有效载荷即报文的总大小,然后用报文的总大小减去UDP8字节的报头,就可以得到有效载荷,即数据的大小,实现报文的解包。
再来回答第二个问题,在UDP报文中,有16位源端口号和16位目的端口号,分别表示当前报文从哪里来,到哪里去,所以可以根据16位目的端口号,实现报文的分用。
UDP的特点
无连接:只需要知道对端的IP+端口号就可以进行传输,不需要建立连接。
不可靠:没有确认机制,就是传送的一方不知道自己传送的数据是否被被对端接收到,没有重传机制,如果当前报文传送失败,当前报文再也不会进行传送。如果传送报文时因为网络故障无法传送到对端,UDP也不会给应用层传送任何错误信息。
面向数据报:不能灵活的控制读写数据的次数和数量。
面向数据报
在上一标题,UDP的特点中,我们提出了UDP是一个面向数据报的协议,那么什么是面向数据报呢?
面向数据报其实就是应用层交给UDP多长的报文,那么UDP在进行发送的时候,既不会进行报文的拆分也不会进行报文的合并。所以也就意味着,当发应用层给发送端一次发送了100字节的数据,那么接收端接收到数据之后,的时候也必须一次性向上交付100字节的数据,不能对100字节的数据多次交付,发送端发送多少次数据,发送的数据大小是多大,那么接收端就向上交付多少次,交付对应数据的大小。即不可拆分,不可合并。
UDP的缓冲区
在之前学习Linux时,我们就已经提到过了缓冲区的概念,其实在UDP协议中,也是有着缓冲区的概念的。
如上图,当应用层程序调用系统调用进行通信时,如果进行读取, UDP先将下层获取的报文读取到接收缓冲区,然后上层通过调用系统接口,将接收缓冲区中的报文进行读取。但是要注意,接收缓冲区不能保证收到的UDP报文的顺序和发送的UDP报文的顺序是一致的,同时,当接收缓冲区中的数据满了时,后续再到达的UDP报文会被丢弃。
上面讲的是UDP的接收缓冲区,UDP没有真正意义上的发送缓冲区,当上层发送数据时,会直接将数据发送到操作系统内核,然后由内核通过网络层以及下面几层,进行数据的传送。
全双工和半双工
举一个简单的例子,就是你说话的时候我不能说话,你说完了我才能说,这叫做半双工,你说话的时候我也可以说,这叫做全双工。
在计算机网络中,半双工就是当前端口往对端发送报文时,对端不能发送,只有当当前报文发送完毕,对端才能进行报文的发送。全双工就是当前端往对端发送报文时,同时对端也可以同时往当前端发送报文。
TCP协议
TCP协议是传输层中又一个比较重要的协议,TCP协议的报文图示如下。
TCP的特点
确认应答。这是TCP保证可靠传输的核心机制。
面向连接。TCP报文的发送必须建立连接。
全双工通信。接收对方的报文时,同时可以向对方发送报文。
超时重传(保证可靠性)。
流量控制(双端之间)。
拥塞控制(与网络实时状态关联)。
面向字节流。
有序传输,按序接收(保证可靠性)。
TCP的所有特点概括起来就一个特点------可靠。
何为应答机制?
大家可以想象生活中的一个场景,老师在上网课,老师提出了一个问题,那么此时老师怎么样保证自己提出的问题已经被同学们听到了呢?实际上老师是在看到同学们做出响应之后,才能知道自己的问题被同学们听到了。
在计算机网络中也是一样的,当前端口发送TCP报文给对端,也是通过接收到对端做出的响应之后,才知道自己的报文被对端接收到。
TCP报头分析
源端口,目标端口,数据偏移(报文首部长度)
依旧是两个问题。
- TCP报文如何实现封装和解包?
- TCP报文如何实现分用?
先来回答第一个问题,在TCP报文中,可以看到有着一个4位数据偏移的字段,通俗点来说,4位数据偏移其实就是4位报文首部长度,4个bit位表示的10进制数的范围为0~15,TCP报文长度的基本单位与UDP不同,UDP报文长度的基本单位是1字节,但是TCP报文长度的基本单位是4字节(考虑到了对齐),所以对于TCP报文而言, 因为TCP报文首部长度最大为15,也就意味着报文首部的最大长度为60字节。我们可以根据这4个bit位算出报文首部的长度,然后加上数据的长度,就实现了报文的封装。分用就更为简单,因为我们已经知道了报文的总大小和首部大小,所以根据报文的总大小减去报文首部大小,就得到了数据即有效载荷的大小,就可以轻松实现解包。
第二个问题与UDP的第二个问题的答案类似,可以通过16位目标端口号,实现分用。
序号
那么有一个问题,client端口可能依次发送多个报文,我们规定报文发送的顺序是什么样,那么sever端接收报文的顺序就是什么样(因为如果发送的顺序和接收的顺序不同,可能会导致信息发送的错误),那么我们是如何保证TCP报文的发送顺序和接收顺序是一样的呢?
这是因为在TCP的报文中,有序号字段,这个序号可以表示报文发送的顺序,至于按顺序发送的报文是否是按顺序送达的,我们不关心,我们最终会根据发送的报文中的序号来依次进行接收。
确认号
我们知道当前端口发送完报文之后,是通过接收对端的响应来得知自己发送的报文已经被对面接收到了。那么当当前端口发送了多个请求报文,并且得到了多个响应报文,那么当前端口是如何知道这些响应报文对应的是哪些请求的报文呢?
其实在TCP报文中,还有一个确认号,确认号一般比序号大1,当当前端口得到响应报文之后,会分析报文中的确认号来得知,是哪一个请求报文的响应。且可以通过确认号得知,当前确认号之前的所有请求报文都已经被接收到。
还有一个问题,为什么TCP报文中既有32位序号又有32位确认号, 发送请求的时候将32位表示为序号?发送响应的时候将32位表示为确认号不就行了?
其实大家可以仔细想想,响应报文又何尝不是一个请求报文呢?当前的响应报文,可以通过确认号去告知对端上一条请求报文被接收,也会有自己的序号,去发送给对端,最终通过对端的响应报文得知当前响应报文是否被接收到。
窗口
UDP协议具有接收缓冲区,但是没有真正的发送缓冲区。但是在TCP协议中,TCP协议既具有接收缓冲区,也具有发送缓冲区。我们通过图示为大家进一步讲解。
如上图,当client调用write/sendto接口时,相当于先将应用层的数据拷贝到了client的发送缓冲区,然后TCP协议会将发送缓冲区中的数据发送到sever的接收缓冲区,最终,sever的应用层程序会调用系统调用接口read/recvfrom将接收缓冲区中的数据读取到应用层。
会什么会有缓冲区的概念?我们直接给出答案。
- 提高应用层传输数据的效率,将应用层和TCP协议进行解耦。因为有了缓冲区的概念之后,应用层只用考虑如何把应用层数据发送到缓冲区中就可以,至于之后数据在TCP的传输细节,应用层根本不去考虑,因为剩下的事情都是由TCP协议内部做的。
- TCP是传输控制协议,所以数据发送多少,如何发送,这些都是TCP去考虑的事情,也只有TCP协议能够做到这些,所以也必须有缓冲区的概念。
可是大家来想一个问题?既然TCP中有缓冲区的概念,那么这个缓冲区肯定会有大小限制的,如果发送至接收缓冲区的数据,因为应用层没有及时读取缓冲区的数据,或者说应用层读取缓冲区数据的速率比较慢,那么此时缓冲区会很快的被数据填满。那么如果缓冲区即将被填满,或者已经被填满,此时如果TCP发送端仍然发送了大量的数据报文,因为接收缓冲区的大小不够,就有可能报文被丢弃,也就是会有丢包现象,虽然TCP协议中会针对丢包现象有重传机制,但是这并不能从根源上解决丢包的现象。那么如何从根源上解决丢包这一现象呢?
为了解决丢包这一类问题,在TCP协议的报文的报头中有了16位窗口的概念,当发送端发送报文时,接收端会做出响应,同时在响应报文中会填充16位窗口字段,代表着自身的接收缓冲区还剩余多大的内存,所以当发送端接收到接收端的响应报文时,会根据响应报文报头中的16位窗口的大小,及时的去调整自己报文的发送数量和大小。
6个标志位
TCP报头为什么会有标志位的概念,这是因为在日常生活的情景中,一个sever端可能会收到多个client端的请求报文,所以为了区分大量报文,TCP协议中有了6个标志位来区分报文的种类。 6个标志位分别为,URG,ACK,PSH,RST,SYN,FIN,接下来,我们依次为大家讲解这六个标志位的作用。
ACK
我们知道,TCP有确认应答机制,所以当发送端发送了一个请求报文,那么当接收端接收到请求报文,就会发送相应报文,作为对请求报文的确认应答,当接收端接收响应报文时,就知道自己发送的发送报文已经被接收端接收到。所以只要当前报文是一个响应报文,就会将响应报文的报文头部的ACK标志位置为1,代表着当前报文是一个响应报文。
SYN
TCP协议是一个面向连接的协议,所以基于TCP协议的通信必须先建立连接。那么TCP协议是如何建立连接的呢?
这就牵扯到了TCP通信中一个很重要的知识点------三次握手。通过图示为大家简单先讲解一下三次握手。
所以,如果报文是一个建立连接的报文,就要将报文的报头中的SVN标志位置为1。
RST
仍然是三次握手,我们发现,三次握手的前两次握手是不容易丢包的,因为前两次握手的请求都会有响应, 所以只要前两次请求没有收到响应我们就认为该请求被丢包了,但是第三次握手因为没有响应,所以第三次握手很容易被丢包,一旦第三次丢包,就意味着第二次请求没有得到响应,会导致一系列严重的问题,图示如下。
如果当前请求报文是为了重置连接,那么就要将该报文报头中的RST位置为1。
PSH
我们在上文已经讲过了,TCP协议中有发送缓冲区和接收缓冲区。如果TCP协议的接收缓冲区已经快被填满,那么此时发送端发送数据的大小和数量就会降低,所以此时发送端就会发送一个报文,并且将该报文的PSH字段置为1,即告知接收端,快速的将接收缓冲区中向应用层进行交付。
URG
我们知道,TCP通信过程中,发送端报文的发送是具有顺序的,接收端接收报文的顺序也必须按照报文发送的顺序进行接收,也就意味着发送端发送报文的顺序是什么样,接收端接收报文的顺序就是什么样,进一步也就意味着被上层读取的顺序也是基本确定的。但是如果此时我们就像让某个报文插队,顺序接收之后,但是被率先被上层读取呢?
那么此时就需要将该报文的URG位置为1,表示当前报文发送的是紧急数据,要将当前报文的数据率先为上层交付。
所以如果当前报文要发送紧急数据,想让当前报文被接收之后,率先向上进行交付,就需要将报文的报头的URG位置为1。
FIN
TCP是面向连接的通信协议,所以要建立连接,最终肯定也是会断开连接的,所以如果当前请求报文的目的是为了断开连接,就要将当前报文的报头中的FIN标志位置为1。
断开连接又会引入另一个比较重要的知识点,就是四次挥手断开连接,简单图示如下。
如果当前报文是一个请求断开连接的报文,就要将当前报文的报头的FIN位置为1。
TCP的三种机制
确认应答机制
确认应答机制上文已经讲述过,我们在此作为补充。
TCP有序号我们能理解,这是为了方便将来接收报文时,对报文进行有序接受,但是这个序号是怎么来的呢?
其实我们可以认为TCP的发送缓冲区就是一个字符数组,数组的每个元素占用了一个字节,如果一个报文在缓冲区中占用了缓冲区的空间,我们就把该报文在缓冲区中占用的最后一个字节的空间的下标,作为当前报文的序号,如图,我们当前的报文的序号就应该是100,因为占用的最后一个字节的空间的下标就是100。
超时重传机制
当我们在TCP协议的发送端,发送了一个报文,那么此时就会等待对端的响应,因为有了响应就意味着当前报文已经成功的被对端接收到了。
那么有没有一种可能当发送端发送完报文之后,为了确认对端收到了报文会一直等待对端的响应。如果此时接收端将网线拔掉了,此时发送端是不知道对方将网线拔掉的,所以此时通俗的来说发送端是一直在干等的,难道就这样一直干等下去吗?
肯定不是这样子的,我们规定,在发送端发送了报文之后,发送端会进行等待,等待对方的响应,但是这个等待时间是有一个限度的,我们会规定一个最大的等待时间(等待的时间会根据操作系统的不同以及网络环境的不同(网络带宽大就设置时间短,带宽小就反之),动态进行设置),确保在这个时间内一定会收到对端的响应,如果此时超过了这个时间还没有收到对端的响应,我们就认为当前报文丢包了,对端没有接收到发送的报文,所以会进行报文的重传即再次发送。
问大家一个问题,发送端在规定的时间内如果没有接收到对端的响应,一定是对端没有接收到报文吗?并不是,通过图示为大家解释。
所以针对上述情景,当client端在规定时间内没有接收到对端的响应,就认为发送的报文对方没有接收到,就认为丢包了,所以会进行超时重传。
但是针对上图中的第一个情况,如果sever端没有接收到报文重传最终让sever端接收到报文,这没有问题,但是如果是第二种情况呢?sever端接收到了报文,只是返回响应及响应到达的时间比规定时间大,我们仍然认为sever端没有接收到进行了重传,那么sever这不就是接收到了重复的报文吗?
是的,道理是这样子的,但是与此同时,sever端的TCP协议内部会有去重机制,因为重传的报文的序号是一致的,所以可以根据序号进行去重。
如果真的对端把网线拔了,那么此时发送端一直发送,一直重传,这样不就会导致发送端浪费资源一直在做无用工吗?
针对这种情况,TCP协议内部又规定了重传的次数,如果重传的次数到达了这个规定的重传次数,那么发送端就不会再进行重传了。
连接管理机制
TCP协议是一个面向连接的协议,所以使用TCP通信时必须经过,建立连接,数据传输,断开连接这三个步骤。所以对于连接的管理对于TCP而言是非常重要的,所以TCP对于连接的管理有着自己独特的管理机制。
先对连接的整个过程进行图示。
我们再次对三次握手和四次挥手进行进一步探究。两个问题?
- 为什么要三次握手建立连接?1次,2次,4次为什么不可以呢?
- 为什么要四次挥手断开连接?
先来回答第一个问题?我们使用3次握手有3个原因。
- 保证双方的主机都是正常的。
- 因为TCP是全双工通信,也就是双方可以同时接收对方的数据,也可以同时向对方发送数据,而三次握手是能够保证双方都具有收发能力的最小次数,通过三次握手能够保证保证TCP的全双工通信。
- TCP三次握手建立连接是比较安全的。
我们先来对前两个原因做出解释,我们先简单分析一下,当client端向sever发送建立连接的请求时,一旦sever接收到了建立连接的请求,此时sever就知道了client的发送能力和自己的接收能力是正常的,然后sever向client发送了同意建立连接的响应,并且发送了立即建立连接的请求,一旦client接收到了响应和请求,client就认为自己的发送能力是正常的(因为收到了响应),sever的接收能力是正常的(因为收到了响应),sever的发送能力是正常的(因为收到了响应和请求),自己的接收是正常的(因为接收到了响应和请求),client端就建立了连接,然后client向sever发送我已经建立了连接响应,当sever接收到了client端的已经建立了连接的响应之后,就认为自己的发送能力是正常的,client的接收能力是正常的,至此双方都知道自己和对方的接收和发送能力都是正常的,所以就保证了TCP的全双工,以及双方机器都是正常的。
第三个原因,这是因为,如果是只有1次握手或者2次握手,此时的服务器就很容易收到恶意的SYN洪水攻击,使得服务器资源被恶意占用,导致服务器响应卡顿。 图示如下。
上述图示对于1次握手建立连接不可行的解释,但是同样对于2次握手建立连接也是可行的,因为二次握手就多了一次sever的响应,但是对于恶意机器而言,我不管你回不回应,我只是发送请求就行,所以对于恶意机器而言,不会产生资源的消耗,但是对于sever而言就不一样了,因为创建了大量的数据结构,产生了大量的资源消耗,所以1次2次握手就不行,那么三次握手没有安全问题吗,当然是有的,但是因为三次握手必须保证有对端的响应,一旦对端有了响应,对于对端而言,同样是要消耗大量的资源的,对于安全机器而言成本会高很多,所以三次握手的对于普通恶意攻击的防护是比较高的。
再次回答第二个问题,为什么需要四次握手断开连接呢?
这是因为四次握手是一个协商的过程,可以理解为双方签订了一个协议,甲方同意乙方的规定,乙方同意甲方的规定,双方都同意之后,协议达成,这对于断开连接也是一样的,双方都需要进行断开连接的请求的发送,等待对方进行应答,最终双方都断开连接。
两种等待状态
TIME_WAIT
在上图的断开连接的图示中,有了TIME_WAIT状态,TIME_WAIT状态是谁先发送了断开连接的请求,谁就会进入这个状态,一般情况下是client端率先请求断开连接,所以一般情况下是client端具有TIME_WAIT状态。
我们在sever端进行测试,让sever端主动断开连接。sever源代码如下。
int main(int args, char *argv[])
{
if (args != 2)
{
return 1;
}
// 作为服务器端
// 1.创建socket文件
int Listen_fd = Socket::Sock();
// 2.绑定IP和端口号
Socket::Bind(Listen_fd, atoi(argv[1]));
// 3.进行监听
Socket::Listen(Listen_fd);
// 4.持续的获取链接
for (;;)
{
sleep(1);
int newfd = Socket::Accept(Listen_fd);
}
return 0;
}
运行可执行程序,我们发现此时的sever端的是LISTEN状态。
我们在本地使用telnet 进行连接。
然后我们ctrl+c,主动关闭sever端,再次观查sever的状态。
我们发现此时sever端变成了TIME_WAIT状态。
当sever主动断开连接之后,sever端再次启动绑定端口时,就会出现bind err的错误。
首先我们分析一个问题,当sever处于TIME_WAIT的状态时,连接是否已经断开了,其实上图已经很明显了,当client端(此时我们认为为sever,因为sever是主动断开的一方)处于TIME_WAIT的状态时,连接其实是还没有断开的,只有在TIME_WAIT状态下等待2MSL时间之后进入CLOSED状态时,此时我们才认为断开了连接。所以答案就已经很明显了,因为当我们主动结束掉sever进程时,sever进程的网络状态是TIME_WAIT状态,并不会立即断开连接,所以sever的进程实质上是还没有结束的,所以此时如果再次进行端口的绑定,就会导致多个进程绑定同一端口号,我们知道一个端口号只能绑定一个进程,所以此时就会出现绑定错误。那么如何处理这种绑定错误呢?
可以使用setsockopt接口解决bind绑定错误的问题。
static int Sock()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
cout << "socket create error" << errno << endl;
exit(1);
}
int opt =1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return fd;
}
此时就可以重复的绑定端口。
上文我们也提到了MSL的概念,那么MSL是什么?为什么要在TIME_WAIT状态下等待2MSL才能进入CLOSED状态呢?
先回答第一个问题,MSL我们称之为报文传输的最大时间,即一个报文传送到对端的最大时间。
再来回答第二个问题,在TCP传输报文的过程中,我们也说过报文的接收顺序必须与报文的发送顺序一致,但是这是建立在有应答的基础上的,四次挥手的最后一次ACK是不需要应答的,所以最后一次的ACK可以被提前接收,所以当最后一个ACK被发送之后,等待了一个MSL就将状态改为CLOSED就会导致连接关闭,进而导致历史的报文有一部分没有收到,所以设置为2MSL的第一个原因就是为了使得历史数据被收到。第二个原因,就是在四次挥手中,由sever端在LAST_ACK状态发送断开连接的请求时,此时开始计时,如果该报文被cient收到也就是一个MSL,此时client端接收后立即应答,在一个MSL之后到达sever端,所以整个过程需要2MSL,如果sever端在一个MSL时间没有收到client的应答,就会认为自己发送的请求断开连接的请求没有被client端收到,所以就会进行请求的重传,此时重传到达client端又是一个MSL,所以从client端发送响应到接收sever重传刚好就是2MSL,但是此时在接收到sever重传的请求报文时,client端是没有断开连接的,所以是可以接收的。这就是为什么client在处于TIME_WAIT状态时,要等待2MSL的时间才会进入CLOSED状态的原因。
综上,client从TIME_WAIT状态到CLOSED状态有两个原因。
- 保证client端历史报文可以被sever端接收。
- 保证client端可以接收到sever端的重传请求。
CLOSED_WAIT
在四次挥手中,只有当sever处于CLOSED_WAIT状态调用了close关闭连接的接口,此时sever的状态才会变为LAST_ACK状态。那么如果在sever端不显示调用close就意味着sever的状态永远是CLOSED_WAIT状态的。
依然是上述sever代码,获取了连接,但是不调用close接口关闭连接。
那么服务器长期处于CLOSED_WAIT状态有什么影响吗?
肯定是有影响的,sever长期处于CLOSED_WAIT状态,意味着sever一直没有关闭连接,所以sever就会一直维护这个没有断开的链接,如果client接受了大量的连接而不去关闭连接,sever就要去维护大量的连接,sever维护是用数据结构维护的,大量的链接就意味着大量的数据结构,就意味着要浪费大量的资源,有小伙伴可能会去说手动关闭连接不就行了,确实是可以这样的,当有大量的client访问,关闭连接是一件代价很大的事情。虽然主动关闭连接的是客户端,但是依然会有其它的客户端去访问,如果此时sever手动关闭,造成的损失是极大的,双11关闭服务器一秒试试。所以我们一定要记得手动的写上close这个关闭连接的函数。
滑动窗口
TCP协议中也有着滑动窗口的概念,那么什么是滑动窗口呢?
滑动窗口就是上图中tcp发送缓冲区的一个区域,这个区域的前半部分是已经发送且已经收到确认的报文数据,滑动窗口中的数据就是即将要发送或者已经发送但是未收到确认的报文,第3个区域就是未发送的报文。
为什么会有滑动窗口的概念?我们直接给出结论是为了提高效率。
本来是一请求一响应,但是这种方式效率太慢,采用了多请求多响应的模式,直接发送多个报文,多个报文的总大小就是滑动窗口的总大小,上图滑动窗口的大小是4000字节。
需要注意的是,前四次请求不需要响应,之后每次的多次请求都必须根据上一次最后一个响应报文的16位窗口大小调整这次传递报文的大小。
为什么称为滑动窗口,这是因为1000号报文被发送且收到1000号报文的确认的时候,滑动窗口会向右移动准备发送5000号报文。图示如下。
需要注意的是滑动窗口的大小是动态变化的,接收到最后一个请求报文的响应之后,就要根据最后一个报文的窗口得出接收端缓冲区的剩余大小调整滑动窗口的大小。
滑动窗口的丢包处理
我们使用滑动窗口的策略在进行报文的传输过程中,必然会面对丢包现象。
中间报文的响应丢失
中间报文的ACK丢失,但是收到了之后的报文的确认。
针对上图这种情况,如果2000号和3000号报文的响应丢包了,此时我们没有收到响应,但是我们收到了4000号报文的响应,针对这种情况,我们怎样处理呢?
其实这种情况不用担心,因为报文是按序进行接收的,所以如果接收到了4000号报文,那么就意味着4000号报文之前的报文应该是全部被接收到了的。所以一旦我们收到了4000号报文的响应,就能根据确认号4001得知4000号报文已经被接收,那么4000号报文之前的所有报文都是已经被接收了的,所以不用担心中间的报文的响应丢失。
中间报文的请求丢失
第一种情况是响应的丢失,但是这一问题被TCP确认应答中的确认号这一机制给成功解决,但是如果是中间的某一请求报文丢失呢?如下图中的2000号报文被丢失。
这种情况是存在的,那么当2000号报文丢失之后,之后的报文仍然会被按序进行接收,也会做出响应,但是此时的响应报文中报头的确认号不能是发送的报文的序号对应的确认号 ,因为确认号的本质其实就是代表着当前确认号之前的所有序号的报文都已经全部被接收到了。所以,当前发送失败的请求报文之后的所有请求报文的确认号都必须是当前请求报文之前的报文的确认号,因为只有这样才能正确的保证确认号本身的含义,就是确认号之前的报文已经全部被接收到了。
所以,当接收端连续接收到了三个相同的确认号之后,此时就会知道是1000号报文之后的请求报文没有被送达,所以此时就会进行重传,当2000号报文被重发,且被对端接收到之后,此时返回的确认号就应该是5001,因为此时5001号之前的所有报文都已经被接收到了。
上述这种发送端接收到三个同样确认号的响应报文之后,进行重传的这种重传机制就叫做快重传。
流量控制
什么是流量控制?
发送端根据对端报文报头中的16位窗口字段,得知接收端接收缓冲区的接收能力,从而调整自身发送缓冲区中的滑动窗口的大小,保证报文高效传输的机制,这一机制就叫做流量控制。
那么当我们在第一次使用流量窗口发送数据时,我们知道此时发送的报文是不需要响应的,那么问题来了,发送端是如何知道对端的接收能力,从而一次性自信的发送大量的报文的呢?
其实回过头来,大家可以想想TCP通信的全过程,建立连接,数据交换,断开连接,无非就是这三步,滑动窗口发送报文就是在第二步,所以在滑动窗口发送数据之前,还要进行三次握手建立连接,在三次握手前两次建立连接的过程中,其实双端就已经获知了对方的接受能力,为什么是双端,这是因为TCP是一个全双工通信,接受对端数据的同时,也可能在发送数据。所以滑动窗口第一次发送大量的报文,是因为在三次握手期间已经知道了对端的接收能力(窗口大小)。
当接收端的窗口大小为0时,后续如果发送端要发送数据,此时就有两种方法可以获知接收端窗口的大小,一是通过发送端发送窗口探测的请求,然后由接收端发送响应,告知发送端接收端的接收缓冲区的大小,一种是直接由接收端发送一个相应报文,告知发送端自己的窗口大小。
同时,TCP首部还有一个40字节的选项,中间还包含了一个窗口扩大因子M,实际窗口字段的大小是16位窗口字段左移M位。
拥塞控制
在讲拥塞控制之前,我们都是站在发送端和接收端的角度上去考虑的, 如果引入了网络呢?
引入了网络之后,发送端的滑动窗口的大小此时还能只以接收端的窗口的大小为依据吗?试想一下,如果此时滑动窗口的大小符合了对端的接收能力但是此时如果此时滑动窗口就发送了大量的报文,但是此时网络状态是十分拥堵的,就会导致大量的报文被发送之后,频繁地进行丢包重传,这样就会造成大量的没有必要的资源的消耗,所以发送端的滑动窗口发送报文时,必须考虑网络状况。
那么TCP是如果根据网络状况来及时调整滑动窗口的大小,来发送数据呢?
TCP中提出了拥塞控制的概念。如何实现拥塞控制呢?
在TCP中引入了慢启动的机制,同时也提出了拥塞窗口的概念。
先来讲解拥塞窗口,拥塞窗口区别于滑动窗口和16位窗口,拥塞窗口是出于网络中的有一个对滑动窗口大小约束的一个窗口,可以通过这个窗口实时检测网络的传输能力。对于拥塞窗口而言的,一开始,我们让滑动窗口的大小为1,然后发送的报文一旦接收到了响应,就让拥塞窗口的大小为前一次拥塞窗口大小的2倍,之后呈指数增长,我们所说的慢启动其实就是刚开始时,拥塞窗口的大小都是比较小的。
如图所示,一开始拥塞窗口的大小都是1,然后获取到响应之后,拥塞窗口的大小呈指数增长,难道拥塞窗口的大小一直呈指数增长吗?
当然不是,我们规定,滑动窗口的大小为拥塞窗口和16位窗口的最小值,所以当拥塞窗口的大小大于16位窗口的大小时其实也就没有什么意义了。所以我们规定了一个阈值,这个阈值就为TCP刚开始通信时慢启动的阈值(ssthresh),当拥塞窗口大小到达这个阈值时,此时拥塞窗口的大小不再指数增长,而是呈线性增长,在拥塞窗口呈线性增长,增长到了一定的大小时,此时一旦发送端进行了超时重传,那么此时拥塞窗口的大小就会瞬间变为后又1,然开始慢启动,此时慢启动的阈值为重传前拥塞窗口大小的一半,后续依然是按照前面的逻辑进行TCP通信。
单独的流量控制和拥塞控制,是不能提高保证TCP通信的效率的,只有流量控制和拥塞控制结合起来,将网络的实时传输能力和接收端的接收能力都考虑进去,才能真正意义上提高TCP通信的效率。
延时应答
我们上文多次讲述了一个概念,叫做接收缓冲区。当发送端发送数据,接收端接收数据到接收缓冲区,缓冲区的数据可以被上层取走。当发送端发送了报文之后,接收端接收报文到缓冲区中,此时如果接收端立即给发送端发送了响应,那么此时响应报文中的窗口的大小一定是原有的接收缓冲区的大小减去接收的报文的大小,长此以往,随着接收缓冲区中接收的报文越来越多,此时如果都立即做出了响应,那么接收缓冲区的接收能力就越来越弱,导致后续发送端发送的报文的效率变慢。
所以此时我们就引入了延时应答的机制,让接收端接收到报文到接收缓冲区中之后,不立即做出响应,而是等待一段时间,等待应用层取走数据之后再做出响应,此时响应报文中的窗口大小每次都是处于一个很大的值,接收端的接收能力也是比较大的,所以发送端的滑动窗口的大小总是会保持一个较大的值,这样就能保证在网络的传输能力较好的前提下,提高报文传输的效率。
捎带应答
捎带应答其实也很好理解,建立连接的三次握手中的第二次握手就是捎带应答,第一次握手,发送端向接收端发送了请求连接的报文,第二次握手,接收端发送了同一连接的响应报文以及请求获取连接的请求报文,注意这个相应报文和请求报文都是同一个报文,也就意味着在第二次挥手时,接收端发送获取连接的请求时,顺带做出了同意连接的请求,这就是捎带应答。
面向字节流和粘包
我们知道UDP是面向数据报的,TCP与UDP不同,TCP是面向字节流的,面向字节流又是什么意思呢?
面向字节流即无论应用层一次向发送端发送了多少的数据包(如http请求),当发送端将这些请求发送至接收端时,会将这些请求拼接成一个字节序,这个字节序是没有边界的,接收端会将这些字节序随意的向上交付,可能交付一次完成,也可能交付多次完成,所以就会涉及字节序的拼接和分割。这样就会导致http请求可能会被拆分最终合并成多个非法的http请求,这就是粘包现象。
TCP如果解决粘包现象呢?
总结一句话就是明确包和包的边界,TCP实际上是解决不了粘包的问题的,只有应用层自己可以解决粘包的问题, 如http协议中的空行,报头中的content_length属性,以及http协议的每行中的\n,上层在进行接收端数据的读取时,应该以这些特殊符号和字段作为读取的依据,确保读到完整的http请求,其它的数据包也是类似的,要在应用层去设计读取的规则,保证读取时,每个数据包的完整性。
那么UDP会有粘包现象吗?
答案是不会有的,因为UDP是面向数据报的协议,所以当应用层向发送端一次发送大量的数据包(如http请求时),发送端讲这些数据包向上进行交付时,也必须一次性向上交付发送过来的同样大小的数据包,整个过程不会涉及对应用层发送的数据包进行拆分与合并,所以就不会导致粘包的现象。
TCP异常
TCP的异常总共有三种情况。
- 进程关闭。我们知道,进程关闭时,会同步的关闭进程所打开的文件描述符,所以和正常关闭连接时调用close接口没有什么区别。
- 机器重启。机器重启其实和进程关闭没有什么两样,因为机器重启也伴随着进程的关闭。
- 机器断电/网络断开。这种情况下,因为网络已经断开,此时客户端不能向服务器端发送断开连接的请求,所以发送的报文无法被送达至客户端(此时自身也不具有重传的能力),当服务器端发送数据时,发送的数据如果在超时重传的时间时内没有收到响应,就会进行数据的重传,但是因为网络一直是断开的,所以当重传的次数到达了一定的次数时,就会向客户端发送reset请求,最终服务器端关闭连接。
TCP与UDP对比
- TCP:最大的优点就是可靠,因为其是面向字节流的协议,一个符号都可以被TCP明显的甄别出来,可以用于文件传输等需要可靠传输的情景。
- UDP:UDP区别于TCP,UDP的报头是8个字节,TCP的报头最大是60个字节,而且UDP的报文也是有大小限制的,但是TCP的报文没有大小限制,与此同时,因为TCP要保证可靠传输,会有确认应答,超时重传,流量控制和拥塞控制等多个机制,所以这就导致了UDP传输的速率是高于TCP的,UDP可以用于对于实时性要求比较高的场景,比如直播,打视频电话。
listen的第二个参数
在之socket编程中,sever端使用listen接口进行监听时,我们默认设置了第二个参数为5,那么第二个参数到底有什么含义吗?
其实第二个参数+1就是全连接队列中全连接的个数,在TCP中我们引入了全连接和半连接的概念,全连接就是当sever端得知client建立了连接之后进入ESTABLISHED状态之后,此时client和sever都认为client连接已经建立好了连接,那么此时的连接就是全连接,当sever处于SYN_RCVD和ESTABLISHED之间的状态,我们就称之为半连接。
对于sever而言,一次性可获取的连接是有限的,因为sever的资源也是有限的。全连接就意味着client和sever端连接已经建立完成,所以只需要等待后续进行获取连接,就可以进行后续的数据交互,但是因为sever的资源是有限的,一次性获取不了大量的连接,如果此时sever的资源已经被使用完了,当大量的全连接到来时,如果不及时的获取这些全连接,就会导致这些全连接依次的断开,在某些场景下,会造成大量的损失,所以就引入了一个全连接队列的概念,将未被accept的全连接放在一个全连接队列中,当使用完sever资源的全连接都一一断开连接之后,,全连接队列中的队头的全连接会被立即获取,这样就能充分的使用sever的资源,而不会导致sever资源空闲而未被使用的情况,但是全连接队列也不宜过长,这样就会导致在队列尾的连接等待过长时间,造成饥饿问题。
以上便是TCP的所有知识点,至此,传输层的主要协议我们已经讲解完成。
本期内容到此结束^_^