我是靠谱客的博主 成就小蜜蜂,最近开发中收集的这篇文章主要介绍第2章 用电信号传输TCP/IP数据--探索协议栈和网卡,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

          • 一. 创建套接字(socket)
            • 1. 协议栈的内部结构
            • 2. 套接字的实体就是通信控制信息
            • 3. 调用socket时的操作
          • 二. 连接服务器(connect)
            • 1. 连接是什么意思
            • 2. 负责保存控制信息的头部
            • 3. 连接操作的实际过程
          • 三. 收发数据(write、read)
            • 1. 将HTTP请求消息交给协议栈
            • 2. 对较大的数据进行拆分
            • 3. 使用ACK号确认网络包已收到
            • 4. 根据网络包平均往返时间调整ACK号等待时间
            • 5. 使用窗口有效管理 ACK 号
            • 6. ACK 与窗口的合并
            • 7. 接收HTTP响应消息
          • 四. 从服务器断开并删除套接字(close)
            • 1. 数据发送完毕后断开连接
            • 2. 删除套接字
            • 3. 数据收发操作小结
          • 杂记

一. 创建套接字(socket)
1. 协议栈的内部结构

协议栈的内部结构如图所示,图中上面的部分会向下面的部分委派工作。图中的上下关系只是一个总体的规则,其中也有一部分上下关系不明确,或者上下关系相反的情况。

image-20210224144030027

2. 套接字的实体就是通信控制信息

在协议栈内部有一块用于存放控制通信操作信息的内存空间,信息包括通信对象的IP地址、端口号、通信操作的进行状态等。套接字的实体其实就是这些控制信息,或者说是这一块内存空间。

协议栈是根据套接字中记录的控制信息来工作的。协议栈在执行操作时需要参阅这些控制信息。例如,发送数据时需要用到套接字中的通信对象IP地址和端口号;在发送数据之后,协议栈需要等待对方返回收到数据响应信息,但如果数据在中途丢失了则永远等不到对方的响应,所以在等待一定时间之后就要重新发送丢失的数据。为此,套接字中必须要记录是否已经收到响应,以及发送数据后经过了多长时间,才能根据这些信息按照需要执行重发操作。

在Windows系统中可以用netstat命令显示套接字内容:

image-20210224154235502

3. 调用socket时的操作

调用Socket库中的socket程序组件来创建套接字时,协议栈首先会分配一个套接字所需的内存空间,然后向其中写入初始状态,最后把这个套接字描述符返回给应用程序。下次应用程序在向协议栈进行收发数据委托时提供这个描述符就可以了。

二. 连接服务器(connect)
1. 连接是什么意思

连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

连接操作的目的:套接字在刚创建完成时,里面并没有存放任何数据,也不知道通信对象是谁,因此在连接阶段应用程序需要把这些信息告诉协议栈,这是目的之一。而连接操作的另一个目的则是,客户端向服务器传达开始通信的请求。

当执行数据收发操作时,还需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区,它也是在连接操作的过程中分配的。

2. 负责保存控制信息的头部

通信双方交换的控制信息,大体分为两类:

(1)第一类是客户端和服务器相互联络时交换的控制信息–头部中记录的信息。

这些信息不仅连接时需要,包括数据收发和断开连接操作在内,整个通信过程都需要,这些内容在TCP协议的规格中进行了定义。表2.1中的这些字段就是TCP规格中定义的控制信息,这些字段是固定的,在连接、收发、断开等各个阶段中,每次客户端和服务器之间进行通信时,都需要提供这些控制信息。头部的信息非常重要,理解了头部各字段的含义,就等于理解了整个通信的过程

注意:有2个ACK字段,一个是ACK号,还有一个是控制位中的ACK!

image-20210224171745805

这些头部信息会被添加在客户端与服务器之间传递的网络包的开头。如下图:

image-20210224172355517

(2)第二类就是保存在套接字中,用来控制协议栈操作的信息。

3. 连接操作的实际过程

(1)客户端TCP模块创建头部信息:连接操作的第一步是在TCP模块处创建表示连接控制信息的头部(将头部中的控制位的SYN比特设置为1,同时设置适当的序号(为了防止攻击,值为随机的,代表初始值)和窗口大小),通过TCP头部中的发送方和接收方端口号可以找到要连接的套接字。

(2)客户端委托IP模块进行发送:当TCP头部创建好之后,接下来TCP模块会将信息传递给IP模块并委托它进行发送。IP模块执行网络包发送操作后,网络包就会通过网络到达服务器。

(3)服务器收到信息:服务器上的IP模块会将接收到的数据传递给TCP模块,服务器的TCP模块根据TCP头部中的信息找到端口号对应的套接字,也就是说,从处于等待连接状态的套接字中找到与TCP头部中记录的端口号相同的套接字就可以了。当找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接。

(4)服务器返回响应:上述操作完成后,服务器的TCP模块会返回响应,这个过程和客户端一样,需要在TCP头部中设置发送方和接收方端口号以及SYN比特 。此外,在返回响应时还需要将ACK控制位设为1,这表示已经接收到相应的网络包。网络中经常会发生错误,网络包也会发生丢失,因此双方在通信时必须相互确认网络包是否已经送达,而设置ACK 比特就是用来进行这一确认的。接下来,服务器TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应。

(5)客户端收到响应:网络包返回到客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功。如果SYN为1则表示连接成功,这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成,但其实还剩下最后一个步骤。

(6)客户端通知服务器已接收到响应:刚才服务器返回响应时将ACK比特设置为1,相应地,客户端也需要将 ACK 比特设置为1并发回服务器,告诉服务器刚才的响应包已经收到。当这个服务器收到这个返回包之后,连接操作才算全部完成(到此套接字就已经进入随时可以收发数据的状态了,可以认为这时有一根管子把两个套接字连接了起来)。

三. 收发数据(write、read)
1. 将HTTP请求消息交给协议栈

首先,协议栈并不关心应用程序传来的数据是什么内容。应用程序在调用write时会指定发送数据的长度,在协议栈看来,要发送的数据就是一定长度的二进制字节序列而已。

其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序在实现上有所不同,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据。总之,一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。在这样的情况下,如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去。至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,不能一概而论,但都是根据下面2个要素来判断的。

(1)第一个判断要素是每个网络包能容纳的数据长度。协议栈会根据MTU和MSS(见文末杂记)来进行判断。当从应用程序收到的数据长度超过或者接近MSS时再发送出去,就可以避免发送大量小包的问题了。

(2)第二个判断要素是时间。当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时再发送,可能会因为等待时间太长而造成发送延迟,这种情况下,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间之后,就会把网络包发送出去。

PS:这2个要素其实是相互矛盾的,如果长度优先,可能会产生延迟;如果时间优先,又会降低网络的效率。至于如何做到两者的平衡,TCP协议规格并没有规定,而是由协议栈的开发者来决定。所以不同种类和版本的操作系统在相关操作上也就存在差异。

2. 对较大的数据进行拆分

如果发送缓冲区中的数据超过了MSS的长度,那么数据就会被以MSS长度为单位进行拆分发送。每一块数据前面都加上TCP头部,然后交给IP模块来执行发送数据的操作(IP模块会在网络包前面添加 IP头部和以太网的MAC头部后发送网络包)。

image-20210225153708653

3. 使用ACK号确认网络包已收到

TCP具备确认对方是否成功收到网络包,以及当对方没收到时进行重发的功能(在得到对方确认之前,发送过的包都会保存在发送缓冲区中)。因为有了这一强大功能,所以网卡、集线器、路由器等都没有错误补偿机制,一旦发生错误就直接丢弃包。

(1)原理

原理图2.7:

image-20210226155549290

图中接收方返回的ACK号被称为确认响应,用于告知发送方已收到第XX字节之前的数据。在返回这个ACK号的同时,还需要将控制位中的ACK比特设为1,代表ACK号字段有效,也就可以知道这个网络包是用来告知ACK号的。

发送数据时,在TCP头部的序号字段中告知接收方该网络包发送的数据相当于所有数据的第几个字节。不过数据长度并不是放在TCP头部里面的,因为接收方使用整个网络包的长度减去头部的长度就可以得到数据的长度了。

(2)实际情况

原理图2.7与实际情况是有些出入的。在实际通信中,序号并不是从1开始的,而是需要用随机数计算出一个初始值,这是因为如果序号都从1开始,通信过程就会非常容易预测,有人会利用这一点来发动攻击。客户端和服务器都需要各自随机一个数字,然后在连接阶段中互相告知自己的序号初始值。

image-20210227111004328

总结:通过“序号”和“ACK号”可以确认接收方是否收到了网络包

4. 根据网络包平均往返时间调整ACK号等待时间

超时时间:返回ACK号的等待时间(当网络传输繁忙时就会发生拥塞,ACK号的返回会变慢)。

这个等待时间是TCP设置的,超过等待时间,就会重发网络包(PS:如果某一个包被重复发送多次,接收方可以根据序号判断出这个包是重复的,因此并不会造成网络异常)。

TCP采用了动态调整等待时间的方法,这个等待时间是根据ACK号返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中持续测量ACK号的返回时间,如果ACK号返回变慢,则相应延长等待时间;相对地,如果ACK号马上就能返回,则相应缩短等待时间。

5. 使用窗口有效管理 ACK 号

TCP 采用图 2.10(b)这样的滑动窗口方式来管理数据发送和 ACK 号的操作。所谓滑动窗口,就是在发送一个包之后,不等待 ACK 号返回,而是直接发送后续的一系列包。

image-20210227115218713

使用滑动窗口方式有一个问题需要注意,那就是接收方缓冲区溢出:如果数据到达的速率比接收方处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会越堆越多,最后就会溢出。解决这个问题的方法就是通过窗口字段告知发送方自己的窗口大小(最多能接收多少数据),然后发送方根据这个值对数据发送操作进行控制。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了(每发送一个包则减去已发送的数据长度,窗口大小变小)。接收方接收到数据,并且把数据传递给应用程序后,会释放接收缓冲区,然后通知发送方最新的窗口大小(窗口大小变大)。

image-20210227115946517

PS:窗口大小是TCP调优参数中非常有名的一个。

6. ACK 与窗口的合并

当接收方收到数据时,如果确认内容没有问题,就应该向发送方返回ACK号。但如果马上就返回的话那接收方发给发送方的包就太多了会导致网络效率下降,所以会等待一段时间,这个过程如果有其他通知操作就可以把多个通知合并在一个包里发送了。举个例子,在等待发送ACK号的时候正好需要更新窗口,然后又接收到了新的包需要返回ACK号,那就可以在一个包里把最新的窗口大小与最新的ACK号一起返回给发送方了。

7. 接收HTTP响应消息

客户端在委托协议栈发送请求消息之后,会调用read程序来获取响应消息。然后,控制流程会通过read转移到协议栈,但这个时候请求消息刚刚发送出去,响应消息可能还没返回,所以会等服务器返回的响应消息到达之后再继续执行接收操作。

响应消息到达后,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有问题则返回ACK号(前面说过并不会马上返回,可能会和窗口更新一起发送)。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。具体来说,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。将数据交给应用程序之后,协议栈还需要找到合适的时机向发送方发送窗口更新。

四. 从服务器断开并删除套接字(close)
1. 数据发送完毕后断开连接

当应用程序判断所有数据都已经发送完毕的时候,数据发送完毕的一方会发起断开过程(客户端或服务器都有可能)。以服务器发起断开为例,服务器应用程序会调用Socket库中的close程序,然后协议栈会生成包含断开信息的TCP头部,具体来说就是将控制位中的FIN比特设为1,接下来委托IP模块就行数据发送。同时服务器的套接字中也会记录下断开操作的相关信息。

image-20210227155201782

2. 删除套接字

和服务器的通信结束之后,用来通信的套接字也就不会再使用了,这时我们就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间(一般来说是几分钟)之后再被删除。

等待这段时间是为了防止误操作,引发误操作的原因有很多,下面来举一个最容易理解的例子。这里假设是客户端先发起断开,则断开的操作顺序如下。

(1)客户端发送 FIN

(2)服务器返回 ACK 号

(3)服务器发送 FIN

(4)客户端返回 ACK 号

如果最后客户端返回的ACK号丢失了,这时服务器没有接收到ACK号,可能会重发FIN。如果这时客户端的套接字已经删除了,那么套接字中保存的控制信息也就跟着消失了,套接字对应的端口号就会被释放出来。这时,如果别的应用程序要创建套接字,新套接字碰巧又被分配了同一个端口号 ,而服务器重发的FIN正好到达,新套接字就会开始执行断开操作了。之所以不马上删除套接字,就是为了防止这样的误操作。

3. 数据收发操作小结

image-20210227162508563

杂记
  1. MTU(Maximum Transmission Unit) 最大传输单元:一个网络包的最大长度(包含头部的总长度),以太网中一般为1500字节。
  2. MSS(Maximum Segment Size)最大分段大小:除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度。

image-20210225144218016

最后

以上就是成就小蜜蜂为你收集整理的第2章 用电信号传输TCP/IP数据--探索协议栈和网卡的全部内容,希望文章能够帮你解决第2章 用电信号传输TCP/IP数据--探索协议栈和网卡所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(79)

评论列表共有 0 条评论

立即
投稿
返回
顶部