Bootstrap

网络基础(4)传输层

既然是传输层首先就要明确实在层状结构的哪里,除开物理层之外分成了四层协议:

到这里上层(应用层)的使用已经没有问题,之前使用的套接字都是在应用层的。

再说端口号

到一个主机收到一个报文的时候,这个报文中一定存在这个报文需要到的主机的ip号。如果这个主机的IP就是这个目的IP那么这个主机就会接收这个报文。但是在接收到这个报文之后再一个主机中是存在很多的进程的,那么这个信息是哪一个进程需要的呢?

此时为了找到特定的进程在这个报文中一定是存在端口号的,用于找到特定的进程(找到特定的应用层的服务)。

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

其中的前面四个在之前使用套接字的时候已经了解过了,源ip标识唯一的一台主机,源端口标识主机上唯一的一个进程。目的IP目的端口也是一样的。源IP+源端口 = 互联网上的唯一一个主机上的进程。

首先说明一下通过五元组服务器是如何区分不同的请求的,下面这张图就能够说明:

最右边的画面1和画面2你可以认为是使用web标签打开了同一个网站例如下面这样:

现在当一个服务器收到不同的请求的时候:

就可以通过这个五元组中的信息辨别出响应要发送到什么地方。

那么这个协议号又是什么呢?通过之前在应用层的学习能够知道进行网络通信无非就是使用TCP或者使用UDP,而这个协议号就是为了告诉网络层这个报文使用的是TCP还是UDP。通过这个也能知道,即使一个服务器接收到大量的报文也能够辨别出来这个报文的响应应该发往的地方不会出现问题。

这个五元组单独提出来,因为这个五元组能够在netstat -nltp中看到:

未来进行网络通信的时候最重要的也就是这个五元组。

现在简单看一下netstat的选项和使用:

其中不带l就会查询非监听状态的连接信息。

除此之外还有一个ss命令也可以进行查看网络连接信息。这个命令选项和netstat的选项几乎是一样的,作用也是。

我个人比较喜欢使用netstat.

端口号范围划分

在之前学习套接字使用的时候,我进行套接字的绑定选择的都是8888,或者8889这样的端口号。如果我绑定了0到1023的端口号能否成功呢?这里我使用我之前写的服务测试一下:

这里我绑定了一个1号端口最后就显示了权限不允许。

如果我使用sudo呢?

这样倒是能够成功进行绑定。

不使用root权限只能绑定1024往后的端口号,这之前的端口号都是os内部已经部署了对应服务的端口,不使用root权限是不能进行绑定的,也不推荐进行绑定。

最后就是一些知名的端口号:

对于这些知名端口的使用是根据用户的权限进行划分的,你是root才能进行绑定。

由此就能知道端口号就是一种资源。

然后是两个问题:

在常规情况下,一个端口号只能被一个进程bind,因为端口是用来找到特定的进程的。但是在特殊的情况下,可以存在一个进程bind多个端口的情况,因为不违反通过端口号找到特定进程的规则。但是一个端口号不能绑定多个进程,因为违反了一个通过端口号找到特定进程的规则。如果违反了这个规则,底层发送上来的报文正文就不知道要给哪一个进程了。

然后再说明一个Linux系统命令:pidof这个指令用于查看某一个进程的pid,因为之后的很多服务都是以守护进程的方式在后台进行运行的,如果想要看到pid就需要使用ps -axj | grep这样去查。所以Linux系统提供了这样的指令便于得到一个进程的pid。

UDP协议

协议格式:

udp协议的格式如下:

报头信息说明

上面的图片就是UDP协议对应的报文,其中数据上面的四个部分是UDP的报头。

对于这个报头在之前使用udp套接字的时候已经做过简单的说明了,如果现在服务端和客户端要进行通信。都需要自顶向下进行封装报文,获取数据需要自定向上进行解包。

client如果要发送一个信息给服务端,首先会准备好要发送的信息,然后使用sendto();将这个信息发送给对端。但是真的是直接发送给对端了吗?其实不是而是将这个信息发送给了下一层:

也就是将信息拷贝到下一层的缓冲区中,然后在这一层(传输层)就会进行报头的添加而添加的部分就是上图中UDP格式中所说的报头部分。另外一端看到的数据也就是UDP格式中的数据。

现在能够看到的两个信息一个是源端口号一个是目的端口号,对于发送端来说,在其中程序的时候os内部会自动填写源端口号,而目的端口号则一般是由应用层填写下来的。例如当我使用我的客户端去连接服务端的时候,我的客户端不仅要知道我服务端的IP地址还要知道我服务端的目的端口。否则服务端在接收到这个报文在解包到传输层的时候不知要将这个报文交给哪一个进程。

下一个16位UDP长度,需要注意这叫做16位UDP长度而不是叫做16位UDP报头长度,这也就意味着。这个里面填写的是整个报文的长度(报头+数据),之后会重点说明。最后的检验和就是为了说明整个数据包在传输过程中是否出错的。之前在封装套接字的时候,我一直使用的是uint16_t的类型去保存端口号(端口号的范围[0,65535]),那么为什么端口号的数据大小就是16位的呢?

因为底层协议UDP端口号为16位,至于这个udp为什么是16位,我就没有继续深入考虑了。这里为什么端口号要是16位回答的是应用层使用16位数据的问题。这里也能知道应用层是受到底层的约束的。

UDP协议如何解决两个问题

首先说明两个问题是什么:

因为报文是从上往下封装,从下往上解包的。所以任何的协议都要解决这个问题。

第一个问题:因为UDP采用的是固定报头长度来解决的这个问题。前8个字节固定为udp的报头。

第二个问题:答案是目的端口号

之前在写应用层的时候也是存在数据向上交付的问题的。比如http,网络版本计算器。在代码中写的回调函数就是在进行数据向上交付。

那么UDP是如何保证接收端能够把报文收全了呢?对于TCP来说因为TCP是面向字节流的,所以这个工作交给用户来做了,虽然UDP是面向数据报的所以没有这个工作,但是这个工作还是需要做的,既然不是用户做的,那UDP自己来做的,那么UDP是怎么做的呢?

首先如果这个UDP报文连8个字节都没有,那么这个报文一定是一个不完整的报文。而如果大于8字节,然后通过16为UDP长度就能知道整个UDP报文的长度使用这个长度减去8字节。不就是数据的大小了吗?这个字段不就是一个自描述字段吗?

这个和我在自定义协议的时候使用的方式不是一样的吗?我在自定义协议的时候也让我的协议带上了一个长度。这两个方法是没有区别的。

同时这个也就叫做面向数据报。对于UDP来说如果某一个报文不够了就直接丢弃这就是UDP。

下一个问题:如何理解报头呢?

报头其实就是一个结构化的数据,协议就是一个结构化的字段。

例如上图就是对UDP报头进行说明的一个简单结构体。

那么作为一个服务器能不能受到大量的报文呢?

有些报文正在解包分用呢?我作为客户端也是能够收到多个服务器发送而来的报文的。

由此就能说明在os内部是可能积攒了一部分的UDP报文的。

那么就需要对UDP的报文进行管理,如何管理?

先描述再组织。

这里使用图像说明一下:

其中的hello上层传递过来的数据,而这个大框则是传输层此时在传输层内os为了管理这个报文需要先有一个结构体sk_buff。

这个sk_buff中存在很多的字段这里假设值存在data和tail,然后tail一开始指向缓冲区中的某一个位置。而hello是五个字节。而data这个指针一开始也是和tail指向一个位置的,后来用户需要5个字节,就让data指针往前移动5个字节。然后在缓冲区中就有位置了,然后就可以将hello从上层拷贝到传输层了。

然后这里用户要使用UDP协议传输数据,UDP协议也有报头啊?此时就定义一个结构体对象:

结构体对象也是数据。os就能够通过sizeof知道这个对象所需要的空间大小:

然后传输层需要添加报头(从上往下进行封装)如何进行封装呢?让当前的data指针再往前移动8个字节,然后将这个结构体的数据拷贝过去,不就完成了吗?这个工作就是对报文进行UDP封装。

此时报头和数据就加到一起了,图像说明:

所以管理所谓的UDP报文不止需要一个描述报文的结构体数据还需要一个缓冲区。这是一个报文如果在这个结构体中还有一个指针呢?

这样即使在UDP层积压了很多的数据,但是没有关系即使积压了很多的数据也可以使用链表的形式将这些数据关联起来。

此时对报文的管理就变成了对一个链表的增删查改。

如果有多个数据就是下面这样:

现在尝试一下从下往上进行解包,现在在传输层中已经收到了一个报文报文中包含了一串数据。然后将这一串数据的开头直接进去强转为struct udphdr结构体,然后就能知道这一个UDP报文的长度了。然后让tail指针移动到报文的最后位置,然后让data指针往后移动8个字节,此时在tail和data中的数据就需要交付给上层。此时数据就UDP/面向数据报式的交给了上层了。

那么之后的报头封装也能够理解了,不就是一直往前添加新的字段,然后让指针往前移动吗?这个过程只需要指针移动缓冲区只需要进行拷贝数据即可。

此时就有了一个切实可行的方式进行解包和封装了。并且这种方式在发送方和接收方都是这样做的。接收方也是使用的TCP/IP协议栈那么在接收方也会有一个这样的结构,然后使用几乎相同的方式接收来自发送端的信息。协议就是你要使用这样的规则来做,那么我也要使用这样的规则来做。 两端使用同样的方式来解析一段数据,这个方式不就是我在自定义协议时使用的方式吗?所以协议就是结构化字段。即使os不一样,但是只要协议栈一样,那么两个系统中一定存在相同的数据结构。同时这也就说明了协议就是一种约定。那么在Linuxos内核中是否存在这样的结构体呢?

存在下面就是

同时是否存在一个sk_buf的结构呢?

依旧是有的。此时对于UDP报文就有了更加深刻的理解了。

下面说明一下UDP的特点:

什么是无连接?通信之前不进行连接直接发送信息。第二个不可靠,即如果os在底层发送某一个报文是错误的,直接就将sk_buf删除,再将结构体对象释放这就是不可靠。至于面向数据报经过上面的学习,更能理解了。

下一个:

下一个:

之前在使用TCP的时候为了能够理解write和read函数,所以说明了TCP无论是发送方还是接收方都是存在发送和接收缓冲区的。所以无论是发送方还是接收方都是将数据从应用层拷贝到发送缓冲区或者接收缓冲区。所以发送数据的本质就是拷贝(远程拷贝)因为TCP协议发送方和接收方都有发送和接收缓冲区,所以TCP是全双工的。虽然UDP也是全双工的,但是UDP是不需要发送缓冲区的,因为应用层把数据拷贝给下层然后把对应的报头字段一填,然后就直接往下交付了,不需要保存起来。但是UDP需要有一个接收缓冲区。因为毕竟会存在多个客户端向服务端发送多个信息的情况。如果不提供这个缓冲区,倒也是符合UDP不可靠的特点,但是这样有点过于不靠谱了。UDP还是提供一个接收缓冲区的,用于储存上层暂时来不及处理的报文。如果这个缓冲区满了如果客户端再给服务器发送UDP报文这个报文就会被直接丢弃。这个缓冲区只不过是增大了一些UDP的容错性而已。接收缓冲区也不能保证服务器收到的报文的顺序一定是这个报文发送的顺序的。当你的客户端连续发送了10个报文,顺序为1到10,但是服务端并不一定能够是1到10的顺序接受的,因为网络的情况错综复杂。UDP只保证报文在接收缓冲区中收好即可。

下一个问题所谓的缓冲区要如何理解呢?

这里的缓冲区就是一个队列:

这个队列只需要指向第一个sk_buff,此时缓冲区就有了。

那么TCP呢?TCP是类似的,但是有一些特殊。之后会说明。这里只需要知道所谓的UDP的接收缓冲区就是一个队列即可。

下一个:

我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部). 然而64K在当今的互联网环境下, 是一个非常小的数字。大部分情况下是够的如果不够了那么就只能在应用层将这些报文进行拆分了。按照小于64k的方式进行发送。

这也是为什么在sendto中存在一个返回值:

其中的len是你希望发送的期望长度,而返回值是你实际发送的长度,存在这个返回值就是为了预防如果你发送的len大于了64kb之后这个返回值就有用了。

最后一个基于UDP的应用层协议:

最主要使用的地方就是直播了。还有在可靠性要求不高的地方使用的也是UDP,除此之外还有:

NFS: 网络文件系统

TFTP: 简单文件传输协议

DHCP: 动态主机配置协议

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

DNS: 域名解析协议

当然, 也包括我自己写UDP程序时自定义的应用层协议;

到这里UDP协议的原理就说明完毕了。

TCP协议

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

协议格式

首先上图中的数据指的就是应用层中拷贝下来的数据。

对于这个协议首先依旧是源端口和目的端口,前两个字段说明这个报文来自于发送方的哪一个应用,以及要去往发送方的哪一个应用。这里不继续说明了。

然后依旧是两个问题

TCP如何解决两个问题

任何一个协议都需要解决这两个问题,除了应用层没有很明显的向上交付的过程,但是其它层都需要解决这两个问题。因为只有解决了第一个问题,才能做好对报文的解包和封装。交付也需要解决否则我定制协议就没有任何的意义,因为上层拿到数据也解不开。

在学习http的时候是通过空行将http的报文和有效载荷进行分离的。然后http因为是在应用层了所以没有很明显的交付的概念。但是正文部分就是需要交付的部分。

现在的TCP协议要如何处理呢?

首先TCP报头是存在标准报头的长度的,前20个字节。

但是TCP报头其实还存在一个东西就是选项,选项通常用于TCP在连接的时候需要保活,以及握手,还要协商一些数据等等。也就是说这个选项字段是一个可选字段。 大部分的TCP报头都是标准长度,除了部分添加了选项。

但是还是无法解决上面的两个问题,哪一个字段起这个作用呢?

这个字段就是4为首部长度

然后TCP协议其实和UDP协议一样都是一个结构体。因为协议本身就是一个结构化的字段。

所以在传输层的封装解包使用的依旧是一个sk_buff的结构体。然后获取数据的方式和UDP的过程是类似的移动data指针拷贝数据,添加结构体数据。这样又能完成TCP的封装和解包了。

那么TCP协议的报头和有效数据要如何区分呢?

如果是和UDP一样存在一个字段表示报文的长度的话是行不通的,因为在TCP的报头中存在一个选项,并且这个选项是可选的。

首先在TCP报头中是存在这样的字段的这个字段就是4位首部长度:

但是如果真的是4位的话,就是从0000到1111。也就是从[0,15]但是就算不包含选项TCP的报头也是有20个字节啊。区区15怎么够呢?所以这个首部长度并不是直接使用0到15这个几个数字的而是存在一个基本的约定:

也就是说这个首部长度需要乘上规定好的单位,才能表示整个携带选项的报头。那么基本的单位为什么是4字节呢?

因为现在我所学习到的协议的宽度都是32个比特位的。不就是4个字节吗?所以这里使用的基本单位大小为4字节。

那么4为首部长度真正能够表示的长度大小为

因为TCP的标准报头大小为20字节,所以对应的选项就有40个字节去使用,并且单个选项的大小一定是4的倍数,因为这样才能符合4为首部长度的基本单位大小为4字节的约定。

所以如果TCP的长度大小为20,那么4位首部长度就填5(5*4 = 20)【0101】。到这里报文的头部和有效载荷的分离就不是问题了,首先获取标准的报头长度(20字节),然后获取4位首部长度*4就能知道整个报头的长度大小了。在将标准的报头读取完成之在往后读选项的内容。剩下的就是有效载荷了。

然后封装的过程就和UDP一样了,使用sk_buff的指针先拷贝应用层的数据,然后sk_buff根据TCP报头对象的大小往前移动指针将TCP报头对象(tcphdr)放到有效载荷的前面。到这里就完成了封装。

TCP报头字段的学习

首先通过一个例子:下面是一个客户端一个服务器。

现在客户端在应用层将一个数据(hello)使用write/send进行发送其实这里并没有直接发送到对端。而只是将数据拷贝到了发送缓冲区而已。

然后再将这个数据拷贝到对端的接收缓冲区。然后对端的应用层再调用read/recv就将数据从自己os的接收缓冲区中拷贝到应用层了。

当然真实的情况一定很复杂一定会涉及到sk_buff结构体中指针的移动等等不同的行为才能完成上面的工作。

因为使用tcp进行通信的双端都是具有接收和发送缓冲区的。双方发送和接收的步骤都是一样的。

其中对于单独的os来说,既可以发信息也可以收信息。所以才叫tcp为全双工通信。

这里再补充一个信息,tcp通信的时候双方的地位是对等的。即使有客户端和服务器的概念,也是你可以给我发,我也可以给你发。既然双方地位是对等的,那么在发送信息的时候只要研究好了一端向另外一端发送信息。那么反向的也是一样的模式。

所以这里我简化一下图,让两端只有一个缓冲区(一个发送一个接收,其实双方都是有的)

情况的时候,这个数据没有办法只能丢弃,但是和UDP不一样的是,这个被丢弃的数据一定会想办法告诉发送方这个数据没有被接收成功让发送方重新发送这个数据,也就是重传。原则上丢弃这个报文对于TCP影响不大,因为还可以重传,但是UDP的影响就大了,信息丢弃就真的丢弃了。

但是如果真的丢弃了,那么这个报文既没有问题,又经过了不少的路由器(消耗了带宽和电量才发送到这里),现在不是报文的问题,却要让我这个报文直接丢弃,这就让明明不是客户端的责任却让客户端去接收代价。虽然在技术上这是可以做到的,但是不合理也不高效。

所以就需要让这个不合理变得合理,所以需要做以下的操作:

虽然这是一个极端的情况,但是我这里就拿极端的情况去分析便于理解。

这种当接收方来不及接收了,对端就不发了,而是去等等的机制叫做流量控制。

那么这个流量控制是由谁来进行流量控制的呢?之前在写应用的socket套接字的时候没有管过这些问题。这里先出结论:

这个工作其实是发送方的TCP协议做的,而TCP协议也是属于os的,所以浏览控制这个工作是由os中的tcp模块来做的,也就是用户不需要关心。

上层用户只要将数据放到缓冲区之后就不需要再去管了。剩下的数据什么时候发,发多少?出错了怎么办,由os中的tcp协议决定,用户不用管。这就会造成,也许用户认为这个数据已经发了,但是其实这个数据还在os的缓冲区中没有进行发送。

由此tcp协议的全名才叫做传输控制协议。控制的有很多,比如上面说的流量控制。

这个用户不关心到底写到没有,而是只将其写到缓冲区中的行为和写文件不是一样的吗?

下一个问题如何进行流量控制呢?即发送方如何知道对端的接收缓冲区已经写满了呢?

再回到这个问题之前先去说另外一个问题,当客户端向服务端发送了一条信息之后,如果服务端接收了这个信息,然后服务端就需要向客户端发送一条信息进行确认。

只有当客户端接收到了这个回发的信息,才能确定自己之前发送的信息已经被服务端接受了。 原则上对于客户端发送的每一条信息服务端都需要发送一条信息,表示自己收到了这个信息。既然从左往右是这样的从右往左也是一样的(服务器往客户端)有了这个确认应答机制就能够保证客户端发送的信息,服务端一定能够收到了。

还有一个补充点:

不要忘了这里使用的可是TCP进行通信,这就意味着双方发送的hello,和收到其实本质都是一个TCP报文。

信息都在TCP报文的数据当中。携带了完整的报头。即使没有数也有一个20字节的基本报头的报文。

现在当客户端发送了一个信息给服务端,服务端接收了这个信息要向客户端发送一个应答。这个应答也是一个报文,并且按照官方的说明这是一个ACK类型的报文。

ACK类型的报文并没有什么不同只不过是报头中ACK对应的字段被设置为了1.

上图中的保留6为后面就有ACK。因为每一个客户端发送的信息都有一个应答,而在tcp的报头中还有一个16为的窗口大小。

这个东西就是:

之前说过作为发送方要进行流量控制,如何控制最基础的条件就是要知道接收方的缓冲区剩下的空间的大小(接收方的接收能力)。

所以服务端作为接收方在发送响应的时候就需要告诉发送方我的接收能力,也就是当前我剩下的缓冲区空间大小。此时发送方通过这个应答报文中的16位窗口大小就能知道对方的接收能力了。由此就能支持进行流量控制了,从左往右是这样的,反向也是这样的。此时双方都能进行流量控制也就不会出现发送过多的报文,导致报文被丢弃的情况了。

下一个结论:

进程被阻塞了,就需要等待os去发送缓冲区中的信息。

同样的道理对于接收方来说如果接收缓冲区中没有数据了,也会进行阻塞。此时系统和网络不就进行了同步了。

以上就是流量控制的基本认识。

下一个问题:

因为报文永远是发送给对方的,所以必须填写自己的。

总结一下:

UDP协议的是没有输入缓冲区的,但是还是需要提供一个缓冲区这个缓冲区是独属于UDP的某一个即将发送的报文的。

然后TCP具有输入缓冲区,因为可以接收多个报文的信息。

由此对于报文的发送需要具有下面的这张图片:

以上只是复习一下,现在继续去学习TCP的报文。

经过上面的学习,我能够知道了,当一方向另外一方发送信息的时候,另外一方会对这一方发送的数据返回一个回应的报文。这个报文中就会告诉发送端,我的接收缓冲区中还剩下多少空间可以用于接收信息。由此也就知道了TCP叫做传输控制协议。传输控制就体现在当应用层将信息拷贝到下层的输入缓冲区中后,应用层就不管了,对于这个信息什么时候发送,发送多少都是由TCP去控制的。这也就是为什么TCP叫做传输控制协议。因为双方都会对对方发送的信息进行回应,而这个回应中都会告诉对方我方的接收能力大小。此时双方就能进行shuan控制

下一个报文字段:

序号和确认序号

依旧是下面这张图:

首先得出一个结论:

在网络通行中暂时做不到100%可靠的网络通信,因为最新一条信息没有应答无法得知这个信息的可靠性。

但是这也就意味着之前发送的所有信息都是存在应答的,也就意味着之前发送的信息都是可靠的。

所以如果从左往右的发送的信息除了最新一条都有应答,那么就能保证在最新一条信息之前的所有信息从左到右的可靠性。同样的道理从右往左也是这样的。

所以TCP保证可靠性的最重要的原理就是上图中的红字。

通过上面的信息就能够知道一个结论:

这句话就是确认应答机制的底层原理。

现在有了确认应答机制的底层原理,现在再来认识一下确认应答的机制。

首先是TCP发送数据的方式:

之前说过对于客户端发送的每一条信息服务端都需要进行应答,而这个应答发送的报文类型也就是ACK类型的报文。

上面的图对于客户端来说,无法确定自己的信息是否被服务端进行了接收,但是因为服务端对于接收的每一个来自客户端的信息都要进行应答,客户端接收到了这个应答就证明服务端收到了这个信息。不要忘了TCP是全双工的协议,既然客户端可以使用这种方式确认自己的报文被服务端接收。服务端也可以使用这种方式确保自己的报文被客户端接收。

这是理解TCP数据传送方式的最朴素/基本的方式。之后理解TCP发送信息的原理可以认为是发送一条数据,然后发送一个应答,然后发送另外一个信息然后再发送应答。

但是这样发送10条信息,这10条信息都是串行发送的,这样没有问题吗?从可靠性来说是没有问题的(只说明一端,另外一端是一样的)。但是效率有点低了。在发送少量数据的时候这种方式是可能存在的,问题就是慢。由此就有了TCP发送信息的另外一种模式:首先假设通信双方的带宽是10M,但是在发送数据的时候却采用这种方式去发送,那不就相当于,给了你一个高速路但是你却要在上面踩自行车吗?有点浪费资源。所以就有了另外一种发送信息的模式:

首先客户端假设要发送四个报文,此时就允许客户端直接给服务器发送四个报文,然后原则上就需要服务器对每一个报文进行确认:

只要你连续发送了四条信息,然后客客户端也能给服务端连续回应四个应答,也能实现确认应答机制。这种方式的最大特点:

因为发送和确认应答的时间进行了重叠,就既实现了可靠性,又实现了效率高。而TCP发送信息的模式最常见的也是这种。但是这两种发送信息的方式都应该记住,第一种方式反映了TCP是如何保证发送信息的可靠性以及如何保证发送信息的效率问题的。

然后不要忘了不管是发送信息还是做出应答,都是两端在接收对方的完整的TCP报文(报头+数据)。上面的ACK就是一个完整的报头,只不过报头中的ACK标志位被设置为了1。对于第二种发送信息的模式也是这样的。

双方通信的时候发送的都是完整的TCP报头+数据(可以有数据,也可以没有)。

现在回到第二种发送信息的方式,假设存在1234四个报文,按照顺序,1 2 3 4发送报文,那么客户端接收报文的顺序也是1234这个顺序吗?答案是特别不一定,因为在网络当中发送的信息在网路当中要经过运营商的服务器,,以及各种公网,所以想要保证顺序也一定是12345,这是不一定的,需要经过的服务器也是无数的。

也就是说

如果底层不对这些报文进行排序,直接交给上层就可能出现上层读取这些报文读反的情况。比如发送的1234报文,第一个是报头第二个是有效载荷,但是因为2先收到就让上层先收到了有效载荷,但是没有报头上层又怎么判断呢?

由此就能知道了乱序是不可靠的一种,为了保证有序性就有了32位序号。

也就是说现在在发送报文的时候,这些报文不再只是abcd什么的,而是有了自己的序号。

例如下面因为双方发送的信息都是带报头的报文。由此每一个报文都有了自己的序号:

例如下面的100,200,300,400,这些信息都是写在报头的32位序号中的。

那么当服务端收到这些报文之后根据序号中的信息,进行排序后就能保证服务端收到的报文是按序号到达的了(有序的)。

服务器还要给发送端发送的报文进行确认啊,这个确认是有序的吗?不要忘了确认也是报文啊,也有序号啊,所以这个发送的响应也是能够按照序号到达发送端的。

这里继续深入,不要忘了一端在发送报文的时候是可以进行连续的发送的,那么接收端也就可以连续的发送确认,那么发送端怎么知道某一个接收的响应是对之前发送的哪一个报文的确认呢?

例如我发送了10个报文,然后如果收到了10个应答,那么没有关系,至少我确认这个10个报文都已经被对端接受了,但是就怕收到了9个或者8个确认。

此时哪一个报文丢失了呢?我怎么知道呢?此时要补发某一个报文我都不知道补发哪一个报文。为了让发送端知道这个确认是哪一个发送的报文的确认。所以需要在报文中增加一个确认序号。

使用下面的图:

现在发送端发送了100,200,300,400给接收端,接收端在接收了这些信息之后,需要重新构建一个报头其中这个报头的确认序号的值,就是发送报文的序号值+1。

你给我发了序号为100的报文,我给你发送的响应的确认序号值就是101。序号为200响应的确认序号就是201。以此类推。

这里还有一个细节需要处理,确认序号表示的含义:

什么意思呢?依旧是拿上面的例子,假设接收端发送确认序号为101和201的响应,但是其中确认序号为101的响应丢失了,此时的发送端接收到了确认序号为201的响应,此时就会认为100这个信息对端已经收到了,这就是确认序号的另外一个意思。

那么为什么要怎么规定呢?

因为在网络上发送的任何一条报文/数据都有可能丢失。

那么上面的图像可能会发送这样的情况,接收端收到了100到400的信息,但是自己发送的响应除了401被对端接收了以外,其它的都丢失了。如果按照发送一个就需要接收一个响应的规则,那么即使收到了401这个响应,发送端还会进行100-300信息的补发,但是这三个信息其实对端已经接收了啊,所以此时确认序号上面的规定

就起了作用,虽然我只收到了一个响应但是这个响应的确认序号是401,我就可以认为100-300这三个信息都被对方收到了。

这样设计就可以做到让少量的应答丢失。这激素hi为什么确认序号要怎么规定:

这里还存在问题,比如发送的报文如果真的丢失了呢?对于这些问题,之后会去处理。

这里继续思考下一个问题,那么32位序号和32位确认序号这两个东西为什么要在报文中分成两个部分呢?直接变成一个序号不也能解决这个问题吗?

因为TCP是全双工的,也就意味着客户端可以给服务端发送信息,那么服务端也可以给客户端发送信息。也就是双方可以同时进行通信。双方也要同时对对方发送的信息进行ACK:

ack也是携带了报头的报文,而服务器给客户端发送的你好也不是单独的你好,而是携带了TCP报头的你好。那么服务器能否将有效载荷(你好)写到ack的有效载荷中在发送给客户端呢?

这样这个新的报文的有效载荷为你好数据类型ack设置为1.然后再将确认序号写到这个报文的32为确认序号中,再将这个信息的序号写到这个报文中的32位序号中。这也就是为什么32位序号和确认序号要进行分开。

这种技术策略也就是捎带应答。

而在真实的通信场所中,双方一般都是会进行信息发送的。所以大部分的历史报文既是数据又是对历史报文的确认。这就意味着对于tcp中的报文来说这个报文既是数据又是应答。

所以因为tcp通信时使用了捎带应答,所以序号和确认序号要被同时使用。这也是为什么序号和确认序号要分开而不是合成一个。

依旧是上面的100到400的例子,如果服务端想要发送1000数据,那么某一个响应的序号就是1000,而确认序号就是101

此时这个报文既是对对方进行确认,又是自己发送的信息。

以上就是tcp报文中的序号和确认序号,总结一下:

提高效率的设定体现在使用捎带应答,由串行发送报文改为并行发送报文这些细节不是可靠性,是提高效率,所以TCP不止有可靠性,还有效率。

此时再回到tcp的名字传输控制协议,tcp为传输做了很多的处理,比如流量控制,序号和确认序号机制,

16位紧急指针

要理解这个16位紧急指针就需要知道TCP报头中的6个标志位。(在某些协议中是8个标志位)

在tcp结构体中:

ACK标志位

这个标志位在报文当中其实就是一个比特位,上图ACK表明这个报文是一个确认报文。

在继续说明之前,我知道使用TCP的两端在进行通行之前需要进行连接(也就是三次握手)

而上图中所谓的Syn,ACK也就是这些标志位。这些标志位的意思就是在报文中的这个比特位被设置为1了。

然后断开连接要进行四次挥手:

这里的FIN也就是特定的FIN标志位被设置为了1。除此之外还会进行正常的通信:

其中一方将信息和ACK报头一起发送给对端,另外一端如果有信息也这么处理否则就是一个ACK。

那么再思考一下一个服务器不止会给一个客户端提供服务。也就是说再给其中一个客户端提供服务的时候也会为其它的客户端一起提供服务。

这意味着不同客户端进行的操作是不同的:

意味着服务器一定会同时收到各种各样不同类型的tcp报文。

这就意味着报文需要有类型。所有有了各种标志位。

以上就是为什么要有这些标志位

所以不同的类型就意味着服务器会有不同的动作。

URG标志位

现在假设一个使用场景现在一个客户端向服务端发送了10个4KB大小的报文。并且这些报文是按照序号到达服务端的。

那么按序到达的本质就是队列。

那么如果某一个时刻客户端向服务端发送了一个紧急任务,但是因为每一个报文都是有序号的所以这个紧急报文也需要进行排队。但是紧急数据需要被尽快的处理,也就是这个报文需要进行提前的处理也就是这个报文要进行插队。

插队的时候就不需要正式的排队了,让其可以被优先处理。基于这种情况就有了URG标志位以及16位紧急指针。

如何证明这个报文是一个紧急的报文呢?只要这个URG标志位被设为1,那么这个报文就是一个紧急的报文。但是这还不够,还需要一个紧急的数据。那么这个紧急数据在哪里呢?此时16位紧急指针就有用了:

这个紧急指针的作用就是紧急数据在有效载荷中的偏移量。并且因为紧急数据只有一个字节。所以只使用紧急指针就可以说明紧急数据所在的位置了。

那么只要URG被设置为了1就必须查看紧急指针,如果URG没有被设置为1,那么这个紧急指针中的数据就是无效的。

因为紧急数据只有一个字节,这就意味着虽然我允许你插队但是不允许你经常插队。

那么这个紧急任务具体是什么呢?也就是这个紧急指针的具体使用场景是什么?首先这个紧急指针的使用场景并不是很高频的。

首先其中一个场景就是:我在发送数据给服务端的时候发现我的数据发送错了,我需要终止或者暂停发送行为。

此时将客户端关闭了,原则上是可以的,但是部分已经发送的数据已经在对端的接收缓冲区了,即使我关闭了客户端,对端也是在接收完一部分数据之后,读到0之后才会出错返回。但是我关闭的原因就是要让你连缓冲区中的数据都不要再进行处理了,尽快终止,并且我不想关闭连接。此时就可以让客户端发送一个携带紧急数据的紧急指针。虽然这个数据只有一个字节,但是我可以设置一些数字码,比如紧急数据为1表示终止接收,2表示暂停。3继续表示其它的状态等等。

如何证明呢?在recv函数的第三个参数也就是flag中存在一个标志位MSG_OOB

如果这样设计,那么recv遇到紧急数据就会直接将紧急数据直接读取上来了。

这个紧急数据(out-of-band)一般叫做带外数据,就相当于正常数据之外的数据,不能太多否则影响正常的通信。带外数据交给上层之后上层就会根据这个数据进行不同的处理。例如终止信息的读取。

以上就是第一个使用场景。

下一个场景:比如有的时候我的服务端在等待客户端发送的报文的时候,发现对端一直没有动静。而客户端也是具有收取紧急指针的逻辑的。此时服务端就可以发送一个携带紧急数据的紧急指针,例如数据为1,就是询问对端你是否还好?,因为这是一个紧急数据,所以客户端优先进行处理并且发送一个0,表示我还好。1表示我压力大,但是还好。等等。这样双方做了约定就可以完成对对端服务是否健康的检测。

由此可以看到在一些监控程序(当某个服务很慢的时候,使用紧急指针判断服务是否还好),以及部分上层程序我想提前终止。都可以使用这个紧急指针。其它的情况一般不使用。以上就是URG标志位和紧急指针的关系。

PSH标记位

这个标记位置代表的意思是push,那么这个标志位有什么作用呢?

首先设定一个场景,现在客户端向服务端发送数据。然后服务端会发送响应,这个响应的报文类型为ACK,并且将接收端的接收缓冲区大小告诉对端,便于对端进行流量控制。

那么有没有这种可能接收方的进程出现了问题导致自己的接收缓冲区中的数据一直都没有被读取。导致了接收缓冲区的大小一直在减少。而发送端一直在发送信息。那么迟早某一次的响应给服务端发送的win(窗口大小就是0了)

此时发送端流量控制就不再发送数据了。但是如果之后接收端的接收缓冲区有空间了。那么发送端要如何知道呢?此时发送方就有自己的策略了。发送方会定期的给接收端发送询问报文(不携带数据只是一个报头),此时根据tcp的规则这个询问报文也会有应答(保证可靠性)。此时这个应答报文就会有携带有自己的接收缓冲区的大小。

除了这种策略之外还有一种策略同时在被使用。那就是发送端在自己的接收缓冲区有空间之后回向发送缓冲区发送窗口大小更新的报文。

两种方式同时使用,只要有一种方式被受到,此时就会恢复到之前发送信息的状态。

那么这个标记位有什么关系呢?

这里所谓的询问报文也是一个完整的报文(没有数据),此时这个询问报文还会把PSH设置为1。就是询问对方是否有空间,并且让对方快点将缓冲区中的数据交给上层,我们需要尽快恢复通信。

这就是PSH标志位的作用。但是PSH标志位还有其它的作用。

因为PSH标志位的作用是告知对方,尽快将数据向上层进行交付。由此在数据需要被尽快交付的场景,都可以使用这个标志位(即使是正常的通信)。比如说学Linux一直在使用的指令输入我能在我的pc上使用xshell登录远程的服务器,然后使用指令。就是因为xshell在底层发送指令报文的时候一直让PSH标志位带上了1.这就让远程服务器对其进行尽快的交付。

RST标志位

RST叫做RESET也叫做重置。有什么用呢?

之前说明过,服务端和客户端在进行数据通信前需要进行三次握手。下面举一个比较极端的例子。首先是三次握手:

这里插入一个小细节为什么这些线都是斜着的而不是横着的呢?因为左边的报文从发出到另外一端接收到这个报文,是需要经历一段时间的。无论这个时间有多短。所以这样划线就是为了说明另外一端会在过了一段时间之后才会接收到这个报文信息。

回到正题上:

答案为不是。最简单的例子我在一个客户端进行三次握手的时候直接将网线拔了。此时无论TCP做什么都是无法保证三次握手的成功的。

这里就需要有一个认知:三次握手是可能失败的。由此就能知道了TCP保证可靠性的前提是没有故障

如果是因为一些不可抗因素(拔网线),TCP是无法保证可靠性的。TCP保证可靠性更多的是带来确定性(会有应答,没有应答就真的丢弃了)。可以这么理解TCP不是保证数据100%发送到(存在不可抗因素),所谓的可靠性:

所以可靠性简单点理解就是我发送的数据你没有收到我需要知道,你收到了我也需要知道。我需要带来确定性。

上面这张图客户端发送的第一个SYN报文丢失了。不用担心s不会发送响应,此时c就知道第一个SYN报文丢失了。

第二个报文丢失了,也一样不用担心。此时c不会发送响应,s就会知道自己的第二个报文丢了。而如果收到了响应那么对端就能知道自己发送的报文没有丢失。

那么这里再提一个问题,对于客户端来说当最后的ACK报文发出去了客户端就认为是连接建立好了,还是s收到了这个ACK报文才认为是连接建立好了呢?首先答案是当客户端将ACK信息发出之后就认为这个连接已经建立好了。为什么这么说呢?因为有一种可能当我的连接建立好之后无论是客户端还是服务端谁都不发送信息了。此时就代表着最新的一条信息是没有应答的。如果是第二种情况,那么就需要服务端发送了响应之后才会客户端才会建立连接,那么遇到这种特殊的情况。不就出现了问题了吗?

所以只要客户端将第三次握手的信息发出之后,就认为自己的连接已经建立完成了。

而对于服务端来说也是再收到这个ACK之后就认为自己的三次握手握手完成了就会建立连接。

这就意味着对于客户端和服务端来说建立好连接的时间是不同的也就是会出现一点点的时间差。

因为三次握手的最后一次报文是没有应答的(前面两次有应答,所以出现问题了也不需要担心),就可能会出现问题。例如最后的这个ACK服务端没有收到,丢了呢?此时对于客户端来说连接已经建立了,就会发送信息,而对于服务端来说因为ACK没有收到所以连接没有建立好(这种情况就是连接建立认知不一致)。但是在连接没有建立好的时候收到了客户端发送的信息(假设这个信息没有丢失),就是不合规的。此时服务端就会发送一个RST报文。意味着告诉客户端连接没有建立成功,需要重置重新建立连接。

此时客户端就会重新释放自己的连接重新回到三次握手的开始重新建立连接。这个就是RST标记位置。

由此就能知道TCP三次握手因为最后一次的ACK没有回应,所以TCP的三次握手就是在赌。赌最后一个报文,对方收到了。

如何知道自己赌成功了呢?如果赌成功了客户端直接发送数据,而服务端直接回复ACK不就证明我赌赢了吗?

由此就能知道了三次握手不一定是必须成功的。对于三次握手,暂时停止,之后再去说明。现在主要是说明这个标志位。

但是这个链接建立不一致的情况不止在三次握手这里存在,因为网络情况很复杂,所以在一些其他的场景这个情况也是存在的。

两个主机之间的距离很远,两个主机之间的步调如何协同也不是一件简单的事情。

例如下面这种情况,在某一个客户端和服务端正在进行正常通信时候我将,客户端的网线拔了。此时客户端是来不及通知服务端的。而服务端的网络还是好的。对于客户端来说,系统内部会检测到网络状态发生了变化,然后就会将建立的链接全部都释放了。然后过很短的时间,客户端的网络恢复了。此时客户端是没有和服务端建立链接的。但是对于服务端来说这个链接是存在的。此时就出现了链接认知不一致的情况。

所以不止服务端能够进行连接重置,客户端也是能够进行连接重置的。

以上就是RST标记位。

下面是一个RST标记位出现的网页提醒:

这个已重置连接就是上面说的那种情况,在一些抢票,选课的时候会遇到这种情况。

对于TCP报头的校验和不说明。对于选项之后会遇到一两个。

其它补充

首先TCP是具有发送缓冲区的,这个发送缓冲区和上面说的sk_buff有什么关系呢?

可以这么理解:

现在在发送缓冲区中存在数需要发送,就会在底层构建struct sk_buff对象和sk_buff的缓冲区,然后将数据放到这个缓冲区中。可以这么简单的理解一下,虽然不太准确。

这里我不将发送缓冲区当成缓冲区而是当作一个char类型的数组。

用户将上层的数据拷贝到这个发送缓冲区中,其实就是拷贝到这个数据中。为什么当作数组呢?因为数组中的数据都是存在下标的。

现在假设我要给对端发送四个字节:

此时携带这个数据的报文的序号我直接设置为3就可以了。此时对端给我返回的就是4

4就是下一次信息要从4开始往后发送,并不代表着下一次的序号就是4。例如我还是想要从4开始往后发送4个字节。所以之后发送报文的序号就是7。

然后对端在进行返回的确认序号就是8。

这里就是要理解确认序号是我下一次信息的发送从确认序号开始往后发送,而不是下一次报文的序号就是这个确认序号。

将这个发送缓冲区理解成数组,就可以从数组当中得到序号。当然以上只是为了便于理解。

另外对于接收缓冲区,也可以当成一个char类型的数组来理解。

以上除了对序号和确认序号进行说明之外,还有一点就是不要把缓冲区当作一个空壳子。

此时在接收缓冲区中使用数组的方式接收一个报文。

此时确认序号也能非常方便的进行确认序号的确认了,关键是将这个当作char类型的数组,那么上层在读取的时候不就是从这个数组中读取一个一个的字节吗?此时就相当于左端数组中的数据拷贝到右端然后上层在一个一个的读取这些信息。这个不就相当于是面向字节流吗?

但是流的概念在哪里呢?

首先在TCP的报头中是没有整个报文大小字节的说明的,四位首部长度说明的是报头的长度不是整个报文的长度。所以当一端连续收到4/5个报文的时候,通过报头只能做到将报头和数据进行拆开。对于数据的长度,我是不知道的。没有这个字段是因为TCP压根就不会管报文的分割。只要底层让我收到一个报文我要做的就是将报头和有效载荷进行分离。然后把有效载荷,无脑放到上面的接收缓冲区中,此时TCP的共组就完成了。

所以在输入缓冲区中可能积攒了几十个历史报文的数据。此时就要求上层对这些报文进行分割处理了,这个报文就是面向字节流。此时对于这个缓冲区来说有人进数据,有人出数据。此时这个缓冲区中的数据因为不断的被拿和进数据,此时的缓冲区中的数据不就显示了一种流动的概念了。并且每个报文之间没有边界。UDP直接理解成在底层维护的就是sk_buff这样的缓冲区,对于每一个报文中的数据是有分隔的。

那么每一个链接都有自己的发送缓冲区,那么两条TCP链接可能会产生相同的序号,但是可以通过其它的字段区分不同的TCP链接(IP,端口,即使IP相同,端口也一定不同)。

也就是说:

对于这个发送缓冲区,之后会和文件的概念进行整合。如何整合之后会说明。

超时重传机制

之前的学习中从来都没有考虑过丢包的问题,但是到现在就可以进行考虑了。

当A主机向B主机发送信息的时候,丢包的情况就两种,一种是发送的数据丢失了,另外一个是B给A的响应丢了。

如果A发送给B的数据丢失了,那么主机A再也收不到应答了,因为主机B根本就没有收到应答。

而在等待了一定时间之后主机A对某个信息进行重传这种机制就叫做超时重传机制。

如果重传还是失败了,那就在等待一会,在进行一次重传。

那么难道要一直进行重传吗?如果主机B挂了呢?主机B挂了,重传一定次数后就不会再进行重传了。

对于次数和间隔之后会特别说明。

还有一种可能在传递信息的过程中应答丢了,对于这种情况A也会在一段时间之后进行重传信息。

这也就意味着主机B会重复收到一个信息,这也就意味着主机B要对收到的信息进行去重操作。

如何去重呢?不要忘了每一个报文中都有一个序号只要序号是一样的,那就说明这个报文之前我已经收到了。

所以无论是主机A还是主机B都能应对某一个报文丢失的情况,主机A会进行重发,主机B会对收到的重复报文进行去重。

因为对于某一个需要发送的信息来说,有可能要进行消息的重发由此:

会被保存在那里呢?之后说明。

回到之前的问题,那么超时重传的时间如何确定呢?这里有一个前提那就是网络环境是一直在变化的。

那么这个超时重传的时间太久了就会影响效率。如果超时时间太短会过于频繁的重传,导致接收方多次进行数据去重。

由此就能知道对于重传的时间一直都是变化的,如果网络好了,那么重传的时间就短一些,否则就久一点。

为什么越到后面重传的时间越长呢?因为如果我第一次重传失败了,第二第三次也失败了,不就证明了我的网络状态/对端的网络状态已经很糟糕了,如果时间还是很短,那么就会浪费资源。当重传的次数到达一定的量,TCP会认为网络/对端主机出现了异常,强制关闭连接。但是累计重传多少次就会关闭连接,就和不同版本的协议栈有关了。

三次握手与四次挥手

图片:

从图片上首先可以看到TCP建立连接需要三次握手,断开连接需要四次挥手。

首先从应用层理解一下,当客户端使用socket创建出套接字之后套接字以文件fd的形式进行返回(至于更加深层次的原因后面说明),然后客户端调用connect(fd,服务器地址和端口)接口,此时在操作系统就会向发起三次握手。也就是发出SYN报文。对于服务器而言也已经存在了一个监听套接字。当客户端和服务端三次握手完成之后就会accept就会获取到这个链接将其交给上层。然后就是互相通信了。那么这里为了更加深入的理解:需要知道这个链接是什么东西?

这里假设一下存在很多个客户端,第一个客户端要和服务器三次握手,完成后会出现一个链接,而还有第二个客户端也要和这个服务器三次握手,完成后依旧是出现一个链接。这就注定了服务器一定允许同时存在很多个已经完成三次握手的链接。

同时在客户端也是可以和多态服务器建立连接的(一边在b站看视频,一般在往网盘上上传数据)。既然同时存在多个连接,那么每一个链接所处的状态也是不同的,有的链接处于新建状态,有的链接处于已经建立好了正在进行通信,而有的链接则即将断开。

这意味着,对于这些链接服务器和客户端(双方的os)需要管理起来,如何管理呢?先描述再组织。

所以所谓的链接也是一个结构体(在双方的os中都存在)。

类似于下面这样的:

所以当通信双方使用TCP建立链接,也就是在双方的os内核中建立一个链接结构体对象。然后使用容器将这些链接对象储存起来此时对链接的管理就变成了对容器(链表)的增删查改。对于这个结构体更加深入的说明之后会说。

而之前说的各种数据(序号,紧急指针都可以放到这个对象中)而如果收到一个报文中存在紧急指针就可以直接从这个对象中读到紧急数据在缓冲区中的位置。例如一个报文的大小为50字节,而紧急数据在有效载荷的第5个位置,此时的紧急指针中的位置就是55。如果没有读紧急指针,那么上层读到了多少字节的数据就直接在55后面减去这个数字,就能不断的维护紧急数据所在的位置了,如果没有紧急指针那么这个紧急指针中的值就是-1,-2或者其它无意义的值即可。

到这里可以得到一个知识点就是:双方维护链接是需要成本的,所谓的成本也就是(时间+空间)【申请链接对象,初始化字段或者其它的操作】,这也是为什么UDP比较简单因为UDP没有这个成本。那么在上图中当客户端发送了请求报头之后链接的状态就会变成:

SYN_SENT,这个SYN_SENT又是什么意思呢?

非常简单在内核当中直接定义宏

当客户端将SYN报头发出去了就将链接对象中的status字段修改为

对象.status = SYN_SENT;这样不就改变了链接的状态了吗?

所以链接是会有状态变化的。

当发送端第一次发送了SYN那么此时链接的状态就被设置为SYN_SENT(同步发送)而当服务端收到了这个SYN报文。服务端就会建立一个连接,这个连接的状态天生就是SYN_RCVD然后服务端再给对方发送SYN+ACK的报文此时客户端的连接状态就从SYN_SENT变成了ESTABUSHED,这个ESTABUSHED就代表着这个链接已经可用了。然后客户端在发送最后一个ACK给服务端,让服务端的链接状态也变成了ESTABUSHED表示这个链接可用。

那么下一个问题: 为什么要进行三次握手呢?

只进行一次/两次握手不行吗?首先如果只进行一次握手,那么只要客户端发起SYN,然后服务端能够收到这个SYN那么这个链接就建立好了。那么如果我的客户端是一个恶意的客户端直接给服务器发送大量的SYN(一个客户端使用某些方式直接发送大量的SYN),因为一次握手这个链接就会建立,而服务端维护这些链接是需要资源的,这样不就会导致大量系统资源被恶意侵占了。

所以一次握手会导致单机可能发送大量的SYN的情况(SYN洪水),虽然即使是三次握手也无法完全避免这个问题,但是一次握手这里的这个bug是在过于明显。虽然TCP本身是具有一些安全的策略,但是无法完全解决安全问题。虽然无法完全解决,但是TCP不能存在明显的漏洞而如果只有一次握手,那么这就是一个明显的漏洞。三次握手也有洪水问题(例如大学的选课系统,当某一天要进行选课的时候,对于某些大学的选课系统来说,服务器就会因为短时间内接入大量的链接导致选课系统无法提供服务,要么就是链接重置,要么就是返回给用户服务器已经崩溃了)但是如果只有一次握手,就会让这种洪水问题变得更加容易被人利用(虽然学校的教务系统也会遇到洪水,但是至少这是真正的人去访问导致的,或者某些人通过虚拟的IP地址通过原始套接字的方式绕过TCP层(不走三次握手)直接向IP层发送大量报文,此时服务器也是扛不住的)。总结就是虽然洪水问题会存在,但是一次握手是存在明显的硬伤的。或者某一个客户端只是不小心连接到了服务端,但是因为一次握手会直接建立连接,此时这个成本比起三次握手会高很多。那么两次握手呢?两次握手也是一个道理,对于服务器来说并不是服务器发送的SYN+ACK被客户端收到了这个连接才会建立,而是当SYN+ACK报文从服务器发送的时候这个链接就会建立了。这个和一次握手没有任何的区别,只不过一次握手不需要服务器的应答。两次握手即便服务器会做应答,但是我的客户端向服务端发送大量的SYN服务端一样是扛不住的。这就说明了一次握手和两次握手都是具有明显的漏洞的。

首先进行三次握手是为了保证从右到左和从左到右的通信信道是通畅的(必须保证双方,因为TCP是全双工的)。

此时前两次握手,就保证了客户端既能发送数据,也能收到数据,此时就证明了从客户端到服务端的通信信道是健康的。

对于服务器来说,我能收到客户端发送的SYN,说明我能收到数据,而对于我发送的SYN+ACK,客户端能够给我响应说明了我能发送数据(最后一次握手),也就证明了从服务端到客户端通信信道的通畅。

所以进行三次握手的第一个理由:以最小的成本验证全双工(理由上面)

如果只有一次握手,对于客户端来说什么都验证不了,不能保证自己能发也不能保证自己能发,对于服务端来说只能保证自己能收。两次握手对于客户端来说虽然能够保证自己既能够收也能发,但是对于服务端来说只能证明自己能够接收数据(因为服务端发送的报文没有响应)。

第二个理由:

这就好比,虽然我服务器也要建立好一个链接,但是客户端你需要先证明你不是一个恶意的客户端,你先建立一下链接如果你建立了,服务端我在进行建立。

以上是对于为什么要进行三次握手的一个角度理解下面在换一个角度去理解。

首先是客户端为了保证自己的通信信道的稳定。客户端发送SYN然后服务端发送ACK当客户端接收到这个ACK的时候证明了自己的通信信道的稳定。此时客户端也就建立连接了表示这个连接可用(站在客户端的角度)。而对于服务端来说为了保证自己这一端到另外一端的稳定也需要发送SYN,然后客户端在发送ACK给服务端进行响应。当服务端收到这个ACK之后代表从服务端到客户端的这条通信信道已经可用了,所以就建立了连接(站在服务端的角度)。

而在这个过程都已经保证了对方收到了我们连接建立的请求。

所以三次握手变成四次握手是更加好理解的。之后再发送请求已经没有意义了。因为已经建立好了连接。为什么上面看到的是三次握手呢?因为TCP为了保证发送效率存在一个叫做捎带应答的方式。服务器是为了给客户端提供服务的,直接就将自己发送给客户端的ACK+SYN报文合成一个报文发送给客户端也是一样的(捎带应答)。

此时也就可以得出下一个为什么要进行三次握手的理由了:

这样也就保证了建立连接的结果确定性(不是连接一定要100%建立成功)。这种解释方式说明了三次握手的本质其实是四次握手+一次捎带应答带来的。

总结:

三次握手成功之后双方就有链接了。而之前在应用层使用read和write将信息发送到对端。其实本质是将数据拷贝到内核层内核层再去决定这个数据什么时候发,出错了怎么办由TCP协议自己决定。

因为现在我已经知道了超时重传在一定次数之后服务器就会强制关闭链接,当前某个连接的重传次数在哪里呢?这个次数就在描述这个链接的对象中(临时增加的)。

下一个四次挥手。

依旧是三次握手差不多的问题,为什么是四次挥手才断开连接呢?

首先来看四次挥手的过程:

先从客户端的角度来说,现在客户端要断开连接了,就向服务端发送了FIN报文,服务端收到了这个FIN之后给出响应。当客户端收到这个响应之后从客户端到服务端的信道就会被关闭了。然后服务端也要断开连接了,就由服务端在发送FIN给客户端,客户端再发送ACK给服务端。此时就以最小的成本互相通告了彼此对方要和我断开连接。为什么断开连接必须保证双方都知道呢?因为TCP协议地位是对等。而四次挥手则是让对方得到我的意愿的最小方案。

那么四次挥手能不能变成三次挥手呢?

例如下面这样的:

答案是存在这个可能性,虽然主流的确实是四次挥手。

但是可能存在下面的情况,现在客户端已经不需要发送信息给服务端了,所以客户端就将通向服务端的连接通过FIN和ACK断开了。此时服务器是同意的,但是虽然客户端不想向服务端发送信息了,服务端还需要向客户端发送信息啊。所以再给客户端发送的回应FIN的ACK中就不会加上我服务器要断开连接的报文。直到后面我服务端也不想发送信息了,才会断开从服务器到客户端的连接(依旧是FIN和ACK)完成四次挥手。这也就是为什么主流的都是四次挥手,但是其实也是存在三次挥手的,那就是当客户端发送的FIN到达服务端的时候,服务端也不想和客户端进行信息通信了,也就使用捎带应答的方式将ACK和FIN发送给了客户端,客户端再将ACK返回给服务端,由此三次挥手也是存在可能性的。

就像现实生活中的离婚,只有一方同意是不可行的,需要双方都同意了才行。也就是说TCP通信断开连接是需要双方进行协商的。上文中说过了如果客户端想要和服务端断开链接了,发送FIN给服务端服务端ACK回来即可,但是如果服务端不想和客户端断开连接呢?这里是什么意思呢?

也就是如何理解断开连接呢?

但是从S->C这个信道没有关闭,仍然是可以发送信息的。发完之后再进行两次挥手。所以一般情况来说当客户端发送完信息之后服务端也完成完成了这种情况的可能性比较低,并且是当客户端的FIN报文到达服务端的时候服务端也将信息发送完成了,这种可能性就更加低了。所以一般来说,四次挥手才是常态,而三次挥手的可能性很低。

但是这就和之前学习的close说不通了,因为客户端在调用了close之后写端和读端不应该都关闭了吗?如果读端也关闭了,那么服务端还怎么写信息给我客户端呢?所以就有了下面的接口:

这个函数能够选择性的关闭socket的一端。

但是对于四次挥手的重点在下面。

首先是状态变化:当某一端接收到另外一端的关闭连接请求,并且我也同意了,那么我的状态就会变成CLOSE_WAIT

当我发出要断开连接的通信时,我的状态也就会变成LAST_ACK,而当客户端收到这个发出的FIN时另外一个连接的状态就会变成(TIME_WAIT),然后发出ACK,过一段时间之后客户端的连接状态就会变成CLOSED,当服务端收到这个ACK之后对于这个连接的状态也会修改为CLOSED。

这里可以做一个假设,当客户端发送FIN到达服务端的时候,服务端会将这个信道的读端调用close()[应该是shutdown]关闭读端,代表服务端不会从这个fd中读取信息了。此时服务端的连接状态就从CLOSE_WAIT变成LAST_ACK。

这里可以使用代码验证一下。

这里使用的代码是之前我写的一个简单线程池版tcp服务器

在正常的逻辑中服务端对于某一个已经完成服务的连接最后是会调用close进行关闭的,但是我这里将close进行了注释。

因为不会调用close所以服务端连接的状态就将一直是

CLOSE_WAIT状态结果究竟如何呢?

从左端的日志可以看到服务端确实是收到了两个信息。

此时可以看到客户端退出了。

这里出现两个是因为我又测试了一次,此时就出现了CLOSE_WAIT状态的连接。所以对于不使用的连接最后要关闭,否则就会造成文件描述符泄漏,更严重的是这个连接会在服务端维持很长的时间。很占据服务端的资源。这样的连接如果很多的话,最后就会造成服务端的卡顿。

所以Linux系统网络服务变得越来越卡的理由有很多,其中有一个原因就是上面说的。

所以如果你发现你的服务器中存在大量的CLOSE_WAIT的连接,大概率是因为你的服务器代码存在问题。

当然上面的实验如果是和我一样在一台机子上做的,那么你会看到两个连接一个连接是从服务器到客户端的(下图中第二个),一个连接是从客户端到服务器的(下图第一个)。

而回到刚刚的实验上:

当服务端没有close也就没有发送ACK,也就导致了服务端连接的状态处于CLOSE_WAIT,而客户端因为收不到这个ACK所以连接的状态也就处于了FIN_WAIT2了。

这里还有一个额外的知识点。即使你的客户端因为某些特殊事件直接挂了。但是从客户端到服务端的连接还是在的。这个连接是由os维持的。所以即使你的客户端挂了,os也能够通过连接给服务端发送FIN请求。(也就是os会自动回应)

通过这张图,可以知道主动断开连接的一方,要进入TIME_WAIT状态。TIME_WAIT是一种等待的状态。在等待什么呢?等待的事件从30-60秒不等,为什么要等,在等待什么,一会再说。下面先验证这个原理。这里为了验证我让服务端主动断开连接(因为TCP双方地位是对等的)

确实看到了这个TIME_WAIT状态。至于客户端对于被断开连接的一方在接收完响应自己在发送了FIN之后,就直接CLOSED了。

当我直接想要继续启动这个服务器的时候就会出现下面的问题:

这个报错说的就是这个地址正在被使用,并且这个TIME_WAIT的连接在过了一段时间之后就会自己消失。

那么上面的这个错误原因是什么呢?

原因就在于这个处于TIME_WAIT的连接,存在这个连接就意味着还有资源没有被彻底释放ip和port(特别是port)还在被使用,重启server就意味着需要重新bind(ip,port),但是因为这个链接的存在,所以ip和port其实还正在被使用,此时绑定就失败了。

这也是为什么之前我在遇到服务器崩溃的时候无法快速重启服务,因为主动断开连接的一方需要处于TIME_WAIT一段时间,在这段时间内,即使这个连接已经处于即将被销毁的状态,但是os还是禁止其它人去使用这个链接对应得IP和port。

使用下面得代码就能完成地址复用:

此时在进行上面的实验,即使服务端断开连接之后,马上重启服务器也能立马重启。使用netstat -ntap去查可以看到这个还是存在链接处于TIME_WAIT的但是服务器却可以直接启动了。原因就在于这个选项。这个选项就在于,如果在底层发送了处于TIME_WAIT的链接代表已经没有进程使用这个IP和port了(只有os在使用)。那么我就直接进行bind。

那么为什么客户端没有出现这个问题呢?因为客户端使用的是随机端口号(os底层对端口号进行bind),也就是客户端的端口号一直在变化。而服务器的端口号不能变化。

对于TIME_WAIT还有问题:

首先第一个问题TIME_WAIT存在的意义是什么,为什么四次挥手都完成了还需要这个TIME_WAIT。

原因有很多个这里提一个:

那么这里都已经完成了挥手了,为什么还会存内在数据呢?就算真的有数据不也应该超时重传了吗?这句话是对的,一般来说正常的数据有ACK和超时重传机制,不会存在于网络中。但是这里假设一个情况:当某一个信息因为在网路中时间过长没有到达,导致对端没有发送ACK回来,引发了超时重传机制。而重传的这个数据成功到达了对端。但是当两端数据发送完毕之后已经四次挥手了,这个数据终于到达了。如果没有这个TIME_WAIT,而我的这个服务器又重启了(重新启动服务端)。就会干扰我的服务器端。

那么为什么只有主动断开连接的一端才有这个状态呢?因为这个数据只有可能是从右到左的这端才有数据了,因为我给对方发数据的信道已经关闭了。我不可能给对方发送数据了,只有可能是对方给我发送数据了。(即便是操你个我这端到另外一端的信道上还存在数据,但是因为对方的链接还没有关闭,所以不会有事)所以从概率上来说需要主动断开的一方进入TIME_WAIT状态。这个等待就是为了等待历史报文从网络当中消散。消散的意思就是被这个即将销毁的链接收到然后直接将这个报文丢弃。

一般来说对于这样的报文,一般来说早就已经超时重传了。即便消散也是将这个报文直接丢弃。

这样就能在概率上极大的减少对新链接的影响。这也是为什么要存在TIME_WAIT的主要原因。

那么这个TIME_WAIT状态持续的时间是多长呢?

什么MSL呢?这不是超时时间,MSL就是最大存活时间:这个报文从A点发送到B点在网络中最大存活的时间为多少。

在我的centeros上显示的时间如下:

为什么是2倍呢?原因上图中最后一行已经解释了,能够保证数据报文在两个朝向上尽可能消散。

即便是2倍的MSL也只是一个经验数据,都是在os的配置文件中写好的,不同的系统之间都没有形成统一。这个时间对于某些报文来说可能不是特别准,如果有的报文就是在80秒才收到呢?而又因为这个正在TIME_WAIT对应的IP和port是可以被复用的,也就是这个链接刚断开,服务就被重启了。就能在被别人连,这意味着,不仅仅要等待这个TIME_WAIT时间还要保证历史报文不会影响新的链接(概率很低)。

这里就需要知道了在两个端进行通信的时候始发序号是随机的,只有随机了才能保证后序到来的无用报文不会影响我当前正常的通信

所以对于一个报文来说一个真正的序号:随机的起始序号+报文的真正序号。(例如随机序号为100,你要发送的报文序号为3【通过之前的理解就是3字节】,此时的起始序号就是103).

所以历史报文要影响到新链接,最巧合的情况就是这个到达报文的序号就是这个服务端要接受的那个序号,这种情况的可能性实在是太低了。

而tcp通信的双方为了能够更好的知道发送的序号的意思,所以在进行TCP三次握手的时候就需要进行随机序号的协商。

这样即便未来依旧是原先的两端重新建立了链接被之前发送的报文影响的概率又降低了。而且也能减少网络黑客定期的猜测报文序号。

滑动窗口

首先经过上面的学习我能够知道的是,如果现在TCP通信的一方发送了一堆数据,而这一堆的数据如果没有ACK的话这一堆数据是不能被移除的(并发发送),因为可能会发生重传。

滑动窗口就是为了TCP并发发送大量暂时没有ACK的数据段,从而提高发送效率的解决方案。

示意图:

os底层具体的操作就是通过滑动窗口实现的。

下面就来看一下滑动窗口如何实现的图片:

图中可以看到主机发送了从1001到2000的数据段。如果此时对端收到了这个数据滑动窗口就会移动到2001(向右滑动)。向右移动之后将5000到6000这个数据范围就包括了进来(而在滑动冲窗口中的数据都能保证同时发送)。这是最基本的原理,但是过于空洞,直接思考下面的问题。 第一个问题:滑动窗口在哪里

第二个问题:滑动窗口如何理解

第三个问题:滑动窗口向右滑动此时窗口的大小是不变吗?(变大?变小?不变?) 第一个问题:

首先滑动窗口肯定是在发送缓冲区中的,否则怎么管理发送的数据呢?

由此滑动窗口就将发送缓冲区分成了三个部分。

所以未来发送的数据都是在滑动窗口中来的,未来收到了数据就可以往右进行滑动。

第二个问题:如何理解滑动窗口

首先将发送缓冲区想象成一个char类型的数组

而滑动窗口是发送缓冲区的一段区域,而此时的问题就被转化为了如何在数据当中确定一段范围呢?很简单两个下标一个表示滑动窗口的前端,一个表示滑动窗口的后端。

此时滑动窗口向右移动就是让wind_end++往后走,win_start也往后走,单独win_end++就是让滑动窗口增大空间,而单独win_end--就是让滑动窗口减少空间。所以所谓的窗口移动就是下标移动。

那么滑动窗口有多大,由谁决定呢?

记住滑动窗口中的数据都是可以直接发送的。并且滑动窗口是为了提高效率的,但是效率再高也不能超过对方的接收能力。

所以:

那么滑动窗口如何更新呢?

在不考虑异常异常的情况下(异常之后说)

这个win就是对方的接收能力的大小。

还有一个额外的问题:

难道是随便设吗?虽然可以,但是确定性不高。

此时三次握手就又可以出来了

所以:

下面回到主逻辑的第三个问题滑动窗口的大小变化,首先就是滑动窗口的大小不变是可能的吗?在什么情况下滑动窗口的大小才是不变的呢?什么时候是变小,什么时候又是变大呢?

理论上来说当一个数据来到了接收缓冲区,如果接收缓冲区没有将数据交给上层,那么这个接收缓冲区大小是会越来越小的。

直到某一次接收缓冲区中的空间被打满了,此时空间大小就是0了

此时的滑动窗口win_start = win_end。此时滑动窗口大小就是0了。而当接收缓冲区中存在空间了,此时通过之前说的两个通知手段(发送端发送报头询问大小,接收端发送报文通知发送端)此时返回的win不等于0。win_end = win_start+win_end。此时滑动窗口大小就增大了。

例如下面这样就是在不断减少直到窗口大小为0:

所以滑动窗口大小可以为0。

所以滑动窗口变小可以,变成0也是可以的。

在上面的那个图中如果应用层突然将所有的数据都拿走了,此时服务端拿到的ACK/客户端发送的通知报文给到了服务端,这两个报头中的win = 4000,ACK = 5001。

此时服务端就会让win_end = 5001+4000,然后将这个范围中的数据发送过去。

所以滑动窗口也是可以变大的。

而这个过程是在干什么呢?根据对方的接收能力调整我发送信息的多少,这个不就是在进行流量控制吗?

所以滑动窗口不仅是提高发送效率的解决方案,也是流量控制的解决方案。

所以流量控制也能让发送数据的速度变快。

那么滑动窗口可以往左边滑动吗?当然不行,因为滑动窗口左边的数据是已经发送完毕的数据,再往左边滑动是想发送已经确定对方收到的数据吗?

下一个问题:

如果滑动窗口越界了怎么办呢?虽然TCP底层做了很复杂的操作让其防止不会出现这种情况,但是对于我来说有没有简单点的理解呢?简单说明:

底层的逻辑也是这样,但是还做了其它很复杂的操作。如果发送缓冲区满了,那么上层还想向缓冲区发送数据的进程就会被阻塞。

这里还有一个问题对于接收缓冲区来说如果2001报文和3001报文已经到达了,但是5001比4001先到,但是4001并不会进入接收缓冲区,必须等到4001先到了之后再5001放到缓冲区中。

下一个问题:如果发生了报文丢失要怎么办呢?

首先对于丢失的报文一定是会进行补发的,这里主要讨论在发射管报文丢失之后滑动窗口是怎么做的。

在滑动窗口中的报文丢失分成三种情况:

第一种情况:

依旧拿上图中的最上面的报文说事:

当第一个ACK报文丢失的时候,后面发送的ACK的值都是多少呢?正确答案是1001

从1001开始到2000的这个报文丢失了。为什么要发送1001呢?

当这些ACK的确认序号都是1001的时候发送方就会意识到从1001开始到2000的报文丢失了,就会进行补发。

此时对于发送方就存在一个机制了:

连续收到三个同样的确认序号的报文的时候,就会立即对对应的报文进行补发。这个补发的动作就是快重传。

补发的这个报文也需要ACK,而此时因为3000到5000的报文都已经收到了,此时这个补发报文的ACK的确认序号直接就是5001。

如果上图中的3000报文也丢失了呢?不影响,此时的4001和5001的报文发送的ACK依旧是1001.不满三个,但是过了一段时间之后会进行超时重传。此时的2000报文,和3000报文都会进行重传。

到这里就可以回答之前留下的问题了,对于已经发送但是没有ACK的数据需要进行保存,保存在哪里呢?

总结一下:滑动窗口不仅能够提高并发发送信息的效率,也是流量控制的解决方案。也和重传,快重传机制有关。

现在回到数据丢失的情况上,如果最左侧的数据没丢,而是最左侧信息的ACK(确认)丢失了。那么此时后面三个序号的ACK填的是多少呢?

填的就是3001,4001,5001。因为TCP根据确认序号的定义,确认序号之前的序号已经全部收到了。哪怕上图中的2001,3001,4001都丢失了,只要能够收到5001,我都能确定前面的信息对端已经收到了。

那么如果是滑动窗口中间的数据丢失了呢?例如上图中的3001到4000的数据丢失了。首先对于ACK来说第一个2001的ACK(数据接收从1001到2000)和第二个3001(数据从2001开始到3000)报文ACK都是正常的。这就意味着中间报文的丢失都会转化为最左侧报文丢失的情况。前面正常的报文收到了那么滑动窗口就会进行向右滑动。直到滑动到丢失报文的位置,此时不就将中间报文的丢失转化为了最左侧报文的丢失了吗?

而最右侧报文丢失的情况,最后也会转化为最左侧报文丢失的情况。

如果中间和后部的都丢失了,但是因为最左侧的已经收到了依旧是会滑动到丢失的报文将其一个一个变成最左侧丢失的情况的。

总结:在滑动窗口中,所有的丢失最后都是最左侧的丢失。

什么性质让其变成了这样的呢?就是序号的定义:

到这里报文丢失的异常情况就全部解决了。

现在我已经知道两个重传了:快重传和超时重传。

既然快重传既能重传还快。为什么还要超时重传呢?

首先快重传机制的触发是存在条件的

如果发送的报文数量只有两个无论如何都无法触发快重传。此时就需要超时重传机制了。所以这两种方式并不冲突。

现在考虑下一个问题,假设这里存在1000,2000,3000,4000四个报文每个报文的有效载荷都是1000字节。那么在发送数据的时候为什么要分成四个报文呢?直接将其弄成一个报文直接发送过去不行吗?即根据对端的接收缓冲区的大小,将这个大小的数据全部搞成一个报文给对端发送过去呢?为什么要分成多个报文呢?

重发成本过高是一个原因,但是不仅仅是这个原因,还有原因是和下层协议有关的。之后说明,就和公交车无法一次性下完所有人,而是只能一个一个人的下(公交车出口只有那么大),这和物理层面是有关系的。

以上就是滑动窗口。

拥塞控制

之前所设定的状态都是在两台主机之间进行通信(各种可靠性和效率都是在两个主机之间进行的)。

但是都没有考虑网络的情况,但是真实的TCP通信请求都是需要考虑网络的状态的。

如果在两个主机进行通信期间出现了大量的丢包。此时TCP就会考虑网络的问题了

那么如果出现了大面积的丢包能否进行重传呢?

答案是不能进行重传。本来现在的网络就已经不好了,还再进行重传,不就让网络变得更坏了。但是只有一个人真的会对网络造成破坏吗?答案是如果真的网络情况差的时候还会进行重传,那么你会重传难道其它在使用网络的人不会进行吗?

如果真的是这样的话,就真的会造成网络瘫痪。因为TCP所有人都需要遵守。既然出现了大面积丢包不能重发报文,所以才需要进行拥塞控制。

希望这篇博客能对您有所帮助,如果发现了错误,欢迎指出。

;