协议栈
协议栈,就是一群真正的专家,为了满足机器和机器之间的通信,而定的一种概念,熟悉的讲,就是七层模型,从上到下:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。 那为什么是栈呢?而不是队列呢?
这个命名的人是真牛,很形象,栈就是先进后出。我们设想下,商家给客户寄快递,顺序是商家先把商品放到盒子里面,然后封上胶带,通过快递员寄到客户手中,客户要先拆开胶带,然后才能拿到商品。这个和网络通信,是一样的。
两个应用之间的通信,主机A和主机B的过程如上图:
- 主机A从上至下,从应用层到物理层,分别封装数据包。
- 通过传输介质,就是双方的路由器,到达对应终端主机B。
- 主机B拿到数据包之后,分别从下至上的拆封,直到应用层拿到数据。
TCP/IP五层模型
这七层模型,是学术界的定义,有着大量的论文进行论证,但是拿到工业界进行实践的时候,发现不太需要七层,只需要五层就好了,就是应用层、传输层、网络层、数据链路层、物理层这五层。那么,为什么把应用层、表示层、会话层都统称为应用层呢?
我们先看看这三层所负责的功能:
应用层:
负责网络服务和应用程序之间的通信,包含各种应用层的协议如HTTP等。
表示层:
主要处理数据的格式、加密、压缩等,处理数据的转换,使不同系统之间的数据能够正确解析,比如JSON格式等。
会话层:
管理和维护应用程序之间的通信会话,建立、维护、结束会话,以便应用程序能够在通信过程中保持同步。
由此可见这三层,都是程序员所实现的,所关注的,说到底,程序员就是负责者三层,都是和用户和应用程序的交互有关,所以统称为应用层。这个应用单单指的是app、web等等,甚至可以理解成 端 ,比如手机端、客户端、pc端等等。
我们来看看五层模型,分别对应的设备以及协议:
名称 | 设备 | 协议 |
---|---|---|
应用层 | 应用层 | HTTP、FTP、SMTP、DNS |
传输层 | 四层交换机、四层路由器 | TCP、UDP |
网络层 | 路由器、三层交换机 | IP、ICMP、RIP、IGMP |
数据链路层 | 网桥、以太网交换机、网卡 | ARR、RAPP |
物理层 | 双绞线、中继器、集线器 | FE自协商 |
应用层
我们先来了解一些概念:
以太网协议
“以太网” 不是一种具体的网络,而是一种技术标准;既包含了数据链路层的内容,也包含了一些物理层的内容。
以太网帧格式如下:
- 以太网帧中的地址,指的是mac地址,可以作为网络上身份识别的唯一地址,与IP地址类似,mac地址占6个字节,每个网卡都有一个mac地址,是网卡出厂时被写死的,不需要动态分配。而且mac地址,只是在局域网中有用,出了局域网就没有意义。
- 目的地址、源地址、类型,这14个字节组成以太网协议头。
IP协议
计算机里面没有一个物件,叫做IP地址,只是在协议栈中间,有一个谓语为IP协议的版本,其实现在所谓的IP地址,在计算机里面一般指的是IP整个协议,定义了在网络中如何寻址和路由数据包,以及在源和目的之间如何传递数据。
IP协议头格式如下:
- 版本号4位,指定IP协议的版本,对于IPv4来说就是4位,不过慢慢开始不太够用,在11年就用完了。于是有了IPv6,IPv6可以为地球上的每一粒沙子分配一个ip地址。
- 首部长度4位,IP头部的长度是60字节,因为4bit最大的数字是15,所以IP头部最大长度是15*4字节。
- 服务类型,TOS,3位优先权字段(已经弃用),4位TOS字段,和1位保留字段(必须置为0)。4位TOS分别表示:最小延时,最大吞吐量,最高可靠性,最小成本。这四者相互冲突,只能选择一个。对于ssh/telnet这样的应用程序,最小延时比较重要;对于ftp这样的程序,最大吞吐量比较重要。
- 16位总长度,IP数据报整体占用了多少个字节。
- 16位标识:唯一的标识主机发送的报文,如果IP报文在数据链路层分片了,那么每一个片里面的这个id都是相同的。
- 3位标志字段:第一位保留(保留的意思是现在不用,但是还没想好说不定以后要用到)。第二位置为1表示禁止分片,这时候如果报文长度超过MTU,IP模块就会丢弃报文。第三位表示"更多分片",如果分片了的话,最后一个分片置为1,其他是0。类似于一个结束标记。
- 13位分片偏移(framegament offset):是分片相对于原始IP报文开始处的偏移。其实就是在表示当前分片在原报文中处在哪个位置。实际偏移的字节数是这个值 * 8。得到的。因此,除了最后一个报文之外,其他报文的长度必须是8的整数倍(否则报文就不连续了)。
- 8位生存时间(Time To Live,TTL):数据报到达目的地的最大报文跳数。一般是64。每次经过一个路由,TTL -= 1,一直减到0还没到达,那么就丢弃了。这个字段主要是用来防止出现路由循环。
- 8位协议:表示上层协议的类型,是TCP还是UDP。
- 16位头部校验和:使用CRC进行校验,来鉴别头部是否损坏。
- 32位源地址和32位目标地址:表示发送端的网络地址和接收端的网络地址。
- 选项字段(不定长,最多40字节):选项字段用来支持排错、测量以及安全等措施,内容很丰富。 此字段的长度可变,从1个字节到40个字节不等,取决于所选择的项目。
UDP协议
- 计算机中没有一个物件叫做端口,端口现在用来表示计算机上面程序的进程,一般是跟ip进行使用,有0-65535个值,其中0-1023为公有端口,我们应用不能使用。
- UDP源端口,2个字节
- UDP目的端口,2个字节
- UDP长度,2个字节
- UDP校验,2个字节
TCP协议
- 16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或者应用程序(目的端口)的,进行TCP通信时,客户端通常使用系统自动选择的临时端口,而服务器则使用知名服务端口,也就是自动应用程序定义的端口。
- 32位序号:一次TCP通信(指的是从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
- 32位确认号:用作对另一方发送来的TCP报文段的响应,值是收到的TCP报文的序号值加1。假如主机A和主机B进行TCP通信,那么A发出的TCP报文段不仅仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同时携带自己的序号和对A发送来的报文段的确认号。
- 4位头部长度:标识TCP头部有多少个32bit字节,因为4位最大能表示15,所以TCP头部最长的字节是60。
- 6位标志位:
- URG标志,标识紧急指针是否有效。
- ACK标志,标识确认号是否有效,我们称携带ACK标志的TCP报文段为确认报文段。
- PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区读走数据,为接受后续数据腾出空间,
不然它们就会一直停留在TCP接收缓冲区中。
- RST标志,标识要求对方重新建立连接,我们称携带RST标志的TCP报文段为复位报文段。
- SYN标志,标识要求对方重新建立连接,我们称携带SYN标志的TCP报文段为同步报文段。
- FIN标志,标识通知对方本端要关闭连接了,我们称携带SYN标志的TCP报文段为结束报文段。
- 16位窗口大小:是TCP流量控制的一个手段,这里说的窗口,指的是接收通告窗口,它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
- 16为校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中释放损坏,注意,这个校验不仅包括TCP头部,也包括TCP数据部分,这也是TCP可靠传输的一个重要保障。
- 16位紧急指针:是一个正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号,因此确切的说,这个字段是紧急指针相对当前序号的偏移,称为紧急偏移,TCP的紧急指针是发送端向接收端发送紧急数据的方法。
三次握手
三次握手动图如下:
- 首先第一次客户端发送一个SYN包给服务器端,询问服务器建立连接。SYN为1表示为同步报文段的标识,seq为一个之前的序列号递增值。
- 服务器收到后,回复一个SYN包和ACK包,其中seq为序列号递增值,ack的值为原seq的值+1。
- 客户端收到后,检查ack的值是否正确,即第一次发送的seq+1就是正确,如果正确,客户端再次回复一个ACK包给服务器端,其中ack为seq的值加1。
从状态上面来讲:
- 开始客户端和服务器端都是closed状态。
- 客户端建立TCP主动打开状态,到服务器之后,服务器为listen状态。
- 当服务器第二次握手,回复客户端的时候,客户端为sys-sent状态。
- 客户端发起第三次握手,到服务器之后,服务器为syn-recd状态。 此时链接建立,客户端和服务器端都是established状态。
三次握手,就是为了防止已经失效的连接请求报文,突然有效,然后传送到服务端,导致错误产生。
比如第一次客户端发送一个SYN包请求建立连接,但是由于网络原因并未及时到达服务端,但是为了连接健壮性一般都会重连,客户端会继续发送SYN包来请求连接,如果这次数据包是正常的,服务器收到回复SYN和ACK包给客户端。但是网络好了后,第一个包又突然正常到达服务端了,服务器以为是客户端发送的新的连接,如果没有客户端的第三次确认,那么服务端就会认为客户端发送了两次链接,而客户端认为自己只是创建了一次连接,造成状态不一致。
因为服务器不仅保存一个客户端的信息,服务器如何保存这些客户端的信息呢?第一次握手的时候,服务器如何保存客户端的连接握手信息呢?
服务器有一个半连接队列,也就是SYN队列,第一次握手的时候,所有的信息都会保存在这个队列里面。
三次链接之后,最后一次握手来的时候,服务器就会判断在半连接队列里面,是否有当前客户端的信息,如果有则把信息拿出来,放入到新的队列中,这个新的队列就是全连接队列,也就是acept队列。如果没有,在这种情况下,服务器可能会向客户端发送RST(复位)包,表示连接被重置或终止。客户端接收到RST包后,会认为连接未能成功建立,可能会尝试重新发起连接请求。
如果,客户端不发起三次握手,那半连接队列里面,当前的节点无法移除,随着时间的推移,或者黑客的攻击请求,会造成半连接队列满额,没法接受新的请求了。
这两个队列,都是使用链表实现的。
四次挥手
动图如下:
四次挥手的过程,其实是不区分客户端和服务器的,因为谁都可以发起断开链接的过程。我们就按照客户端主动断开链接吧。
- 客户端主动发送了一个FIN包,用来关闭客户端和服务端的数据传送,带一个seq过来。
- 服务端收到FIN包后,回复一个ACK包,并把ack回复为seq+1,再带一个seq序号回复给客户端。
- 此时服务端关闭和客户端的连接,再次发送一个FIN包,给客户端,并且把剩下的数据全部发送过去给客户端。
- 客户端收到之后,回复一个ack包,并且把seq和ack都回复过去给服务端。
- 断开链接。
第二次和第三次,如果在第二次的包还未发出去的时候,服务端立马调用了close函数,则第二次和第三次的包可能会合并发出去。
状态如下:
- 双方建立连接后,都处于established状态。
- 当客户端主动关闭,调用close之后,客户端进入fin-wait-1的状态。
- 当被动方服务端接收到FIN包之后,服务端进入close-wait状态。然后发送第三次挥手后,变为last-ack状态。
- 当服务端第三次挥手,发送包到客户端的时候,客户端进入fin-wait-2的状态。
- 客户端收到包后,立马发送第四次挥手,此时处于time-wait状态。
- 服务端收到第四次挥手后,进入closed状态。
- 客户端等待2ms后,然后置于closed状态。
那,这几个状态保存在哪呢?
是保存在TCB控制块,因为它伴随每一个链接的什么周期。
为什么客户端有一个time-wait状态呢?需要等待一段时间呢?
这是为了保证服务端可以接收到发送过去的ACK包,如果一旦立马关闭连接的话,一旦最后发送的ACK包丢失,则服务端会一直停留在最后的确认状态。服务端没有收到对应的ACK,会等待一段时间重发第三步的FIN包,那么客户端会响应这个,重发ACK包的,这是为了可靠的断开机制。
close-wait这个状态有什么用?
这个是被动方,比如服务端调用close不正常,然后被动关闭,或者是这个状态是让服务器发送还未传送完的数据。
TCP是如何保证顺序的?
有个超时机制,定为200ms,比如现在客户端有1,2,3,4,5,6个包发给服务端,每个包都有序列号,这五个是顺序发送的,但并非是顺序到达,如:
- 1号包先到,等待200ms,再回确认消息,如果200ms内,2号包或者其他包到了,会重置定时器,继续等待200ms。
- 如果200ms超时了,回去判断,在这个过程中,哪些包之前的已经全部收到了,比如目前收到了1,3,2,6号包了,那就回复一个确认消息,就是4号包以前的都收到了。那么从4开始全部重新发,6号包直接丢掉。
如何确定一次性发送多少个包?
这就涉及到了慢启动和拥塞控制,和拥塞控制门限值。
慢启动:
- 初始阶段:在TCP连接建立的时候,拥塞串口的初始值一般比较小,比如为1个最大报文段长度。
- 指数增长:在慢启动阶段,每当收到一个ACK确认报文,拥塞窗口大小就会加倍,这种指数增长使得拥塞窗口快速增大,以利用网络的潜在带宽。
- 窗口大小控制:在慢启动阶段中,拥塞窗口大小由慢启动门限值限制,一旦拥塞窗口达到或者超过慢启动门限,就会进入拥塞避免阶段。
- 快速重传:如果发送方连续收到三个重复的ACK,表名有数据包丢失,它将执行快速重传,直接重传丢失的数据包,而不等待超时。
拥塞控制:
- 拥塞避免阶段:一旦拥塞窗口大小达到或者超过慢启动门限值,TCP进入拥塞避免阶段,在拥塞避免阶段,拥塞窗口线性增长,而不是指数增长。
- 线性增长:拥塞避免阶段中,每收到一个ACK,拥塞窗口大小会增加一个MSS的大小。这种线性增长的方式帮助TCP更稳健地调整发送速率,防止过快引发网络拥塞。比如门限值为16,达到16之后,就会每次加1.
- 快速重传和快速恢复:如果检测到网络拥塞,TCP可能会执行快速重传和快速恢复机制,通过调整拥塞窗口来适应当前网络状况。
- 拥塞窗口减半:当发生超时或者检测网络拥塞时,拥塞窗口大小可能减半,慢启动门限也会被设置当前拥塞窗口的一般,并进入慢启动阶段重新开始。
那么如何确定门限值呢?
- 丢包检测,当TCP检测到丢失的数据包时,这通常被视为网络拥塞的信号,拥塞控制算法会根据丢包情况来判断当前网络的拥塞状态。
- 延迟测量:高延迟是网络拥塞的指标。
举例说明:
假设拥塞窗口在超时时为18,慢启动门限为16。当发生超时时,可能的步骤是:
拥塞窗口减半:拥塞窗口变为9。 慢启动门限更新:慢启动门限设为当前拥塞窗口的一半,即16/2=8。 拥塞窗口重新开始:拥塞窗口初始值设为1。 这样,TCP会从一个较小的拥塞窗口开始重新发送数据,逐步调整发送速率,以适应当前网络状况,避免进一步加剧网络拥塞。这个过程是为了实现拥塞控制,确保网络的稳定性和可靠性。
滑动窗口
滑动窗口大小,是有慢启动得出的。 有以下概念:
- 发送窗口:发送方维护一个发送窗口,它是一个连续的序号范围,标识可以发送但还未收到确认的数据,发送窗口由左边界和右边界确认,标识可以发送的序号范围。
- 接收窗口:接收方维护一个接收窗口,同样是一个连续的序号范围,表示期望接收的下一个字节的序号范围。接收窗口通过确认号来表示。
- 窗口大小:是一个动态调整的,反映当前网络条件下的流量控制情况,TCP通过不断调整窗口大小来适应网络的带宽和延迟情况。
- 滑动操作:滑动窗口的核心操作是滑动,发送方通过接收到的确认信息来调整发送窗口的大小,确保只发送接收方能够处理的数据,随着数据的被确认,发送窗口的右边界会向前滑动。
- 流量控制:滑动窗口机制实现了流量控制,确保发送方不会发送超过接收方处理能力的数据。接收方通过调整窗口的大小来通告发送方其处理能力,从而控制发送速率。
- 确认号的作用:接收方通过确认号告诉发送方它期待接收的下一个字节的序号,同时确认已经成功接收的字节数,发送方根据这些信息来动态调整发送窗口的大小。
举个例子:
假设有一个发送方A和接收方B,之间通过TCP进行通信:
- 初始时刻,发送窗口和接收窗口的大小都是3字节,序号范围是1-3.
[ A: Sending Window | 1 2 3 ]
[ B: Receiving Window | - - - ]
发送方接收发送数据:
发送方发送3个字节的数据,序号为1,2,3给接收方B
接收方确认数据:
接收方B成功接收并确认收到3个字节的数据。
现在接收窗口向前滑动,变为序号为4到6.
[ A: Sending Window | 1 2 3 ]
[ B: Receiving Window | 4 5 6 ]
发送方向接收方发送更多的数据
发送方A继续发送3个字节的数据,4,5,6给接收方B。
接收方确认数据。
接收方B再次成功接收并确认3个字节的数据。
现在,接收窗口再次向前滑动,变为序号为7到9.
[ A: Sending Window | 4 5 6 ]
[ B: Receiving Window | 7 8 9 ]