《网络是怎样连接的》——第二章笔记

        本章接续之前的内容,更加具体和详细的介绍了协议栈的组成和工作原理,采取TCP模式进行通信传输时的交互流程,以及IP模块是如何对TCP模块传输过来的网络包进行处理的。除此之外还介绍了以太网的概念,IP协议和MAC协议之间存在什么样的关系。从本章开始,数据才算真正意义上的进入了网络传输阶段,协议栈生成的网络包(数字信息)被网卡转换成电信号或光信号送入网络传输介质,进入了以太网世界。


第一节 协议栈的真正原貌

        这一节主要研究当应用程序(浏览器)调用Socket库的程序组件把生成的HTTP请求报文委托给协议栈进行下一步处理后发生了什么。首先我们需要厘清应用层的Socket库和协议栈之间的关系。协议栈位于TCP/IP架构中的传输层,位于应用层的Socket库则提供了一系列程序组件,来帮助应用程序(浏览器)去调用协议栈的功能而不用了解它内部的底层实现。所以Socket库和协议栈实际上是一种协作关系,Socket库就是建立在协议栈之上的一种供应用程序使用的工具(对浏览器来说屏蔽了协议栈底层的具体实现技术细节)。

        从上一章的内容我们可以了解到,协议栈在网络通信中担任的角色是将浏览器发送的HTTP请求报文转换成带有头部控制信息的网络包,然后将网络包交给网卡进一步转换成电信号(或光信号),接着信号离开客户端主机传入网络。之前的内容将协议栈的工作抽象化成建立了一条连接客户端和服务器的数据管道。实际上却并非这么简单,更加准确的说协议栈的工作有两部分:第一部分是将数字信息(报文信息)切成无数个网络小包,第二部分是为每一个网络小包赋上头部控制信息(地址信息、ACK位、SYC位、数据长度等)。网络在物理线路中的通信实际上就是依赖无数个像这样的包进行的,网络线路中也每时每刻充斥着无数个流动的网络包。

        协议栈可以分为上下两个部分:① 上面的部分是TCP模块UDP模块,这可以代表两种不同的通信模式。TCP是经由“三次握手”建立的可靠通信模式,对数据传输的准确性要求极高,设计了一系列复杂的保障机制来确保这种数据传输的准确性。而UDP模式则是一种更加追求传输效率的通信模式,因为TCP设计的确保数据被准确传输的机制不可避免的牺牲了一部分传输效率,于是有了UDP的出现。UDP仅仅只责输送数据而没有太多的保障接收措施。TCP和UDP各有优劣,适用的场景各有不同;② 协议栈下面的部分是IP模块,它负责把TCP或UDP模块发送来的网络包加上IP头部信息和MAC头部信息,相当于在包裹上贴上物流面单,以确保数据包被送入网络后能被正确传输到目的服务器。

        书中着重讲解了协议栈的TCP模块的工作模式,理解了这种模式后会更容易理解UDP模式,因为UDP实际上就是TCP的简化版本。 我们可以把TCP模式划分为四个阶段:创建阶段、连接阶段、收发阶段和断开阶段

1、创建阶段

        首先我们介绍一下创建阶段,创建阶段就是通信双方各调用Socket库的socket()程序组件创建套接字,实际上这个创建套接字的过程只是在内存中开辟出一块空间,用来存储双方的通信控制信息(如源主机和目标服务器的IP地址和端口号等)以便后续的通信使用。套接字真正的样子就像下图中这样,只是一段控制信息而已。这段控制信息包括本地主机的IP地址和端口号目标服务器的IP地址和端口号当前的通信状态进程标识码

        创建开始时,套接字会被赋予一个初始值。这个初始值的源IP地址和目标IP地址(外部地址)是一样的,通信状态也是默认的LISTENING(监听中,代表已开启并等待外部服务器的接入)。当连接建立后,套接字的外部地址(目标服务器的IP地址和端口)会被更新成接收到的目标服务器IP地址,并且通信状态也变成了ESTABLISHED(已建立)。这就是TCP在套接字创建阶段的真正工作内容。这个创建过程对于目标服务器也是一样,一般来说,目标服务器会首先创建好套接字,然后等待客户端主机发送连接请求,收到请求后它就会做出响应。这个我们在接下来的连接阶段会提到。和源主机不同的是,源主机的协议栈起码已经通过从应用程序收到的HTTP报文知道需要和谁通信了,而目标服务器压根就连通信对象是谁都不知道,它只是被动的在那里等着,看是不是有谁会理它一下),所以在这个过程中需要我们的源主机先发送一段连接请求。

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


(1)网络包

        在这里我们首先需要介绍一下网络包的概念。首先回顾一下从我们在浏览器输入URL并点击回车键以后,浏览器会根据URL生成请求报文,也就是我们看到的数据字段(请求行/状态行,消息体和消息体)。请求报文被浏览器交给协议栈以后,协议栈不会管报文是什么内容什么格式,而是统一识别成二进制描述的数据信息然后进行切割操作(把数据信息切成一小段一小段的包,包的大小通常在1.46KB),TCP模块会给这些包加上头部控制信息(用来确保包被传输到目标服务器后能被服务器的协议栈正确的组合,并根据组合成的有效数据包条还原成报文数据),接着TCP模块处理后的包我们可以称之为TCP包(TCP头部+数据块)。TCP包会被TCP模块发送给IP模块,IP模块会为TCP包添加上IP头部和MAC(以太网)头部(为了确保包在网络线路中能去往正确的传输路径),IP模块处理后的网络包会被交给网卡进行下一步处理。简单来说网卡会把包转换成电信号或光信号(这里的相关机制其实十分复杂)送进网络。

        我们可以简单理解,浏览器信息首先从请求报文转换成了无数个二进制网络包,接着这无数个网络包被转换成了数字信号(电/光信号)进入网络,然后信号通过网络到达目的地后会被重新读取成二进制网络包,最后网络包再被还原成请求报文,这就是网络通信的本质。事实上这里的每一个步骤都十分复杂,有着相关的对应机制去处理,同时还要考虑各种突发的状况。

        从报文信息到网络包这个阶段,对应的其实就是协议栈的工作。协议栈会将报文转换成网络包并加上头部控制信息,然后交给网卡进行下一步处理。这里的头部控制信息可以分为三类:TCP头部、IP头部和MAC头部。TCP模块负责添加切割数据并为网络包添加TCP头部,IP模块则负责为TCP处理后的网络包添加IP头部和MAC头部。

  

(2)头部控制信息

        那么这些头部控制信息又包括什么内容,有什么作用呢?我们先来看看TCP头部信息的作用。前文我们提到数据会被切成网络包进行传输,但是数据被分成了这么多网络包,接收方怎么知道谁前谁后呢?而且在实际传输中会有损耗造成某些包在网络线路中遗失,这样接收方就收不到那个包,还原出来的数据也就不完整了。基于这方面考虑,TCP头部通过记录每个包在数据中的序号(第1个字节...第1460个字节等)来确认次序,这样接收方在收到包后就会按照序号在缓冲区中进行组合排序,这样就确保了数据会被应用程序按照正确的顺序提取和解析。

        那么如果包在传输过程中遗失了怎么办?这里可以通过在包的TCP头部中记录ACK位来保证包确实被接收方收到了。简单来说,发送方发出一个包后,接收方会回复一个包,这个回复包的TCP头部会记录一个ACK位,如果这个ACK位为1,那么就是在跟发送方说:你刚刚发过来的包,我已经收到了。如果发送方在发出包以后,没有收到接收方的ACK信息,那么它就会默认这个包在传输中遗失了,于是重新发送一个原来的包。这里需要解释一下,协议栈在发出一个TCP包时会将发出的包继续保留在缓冲区中,只有在确认接受到接收方的ACK包后,才会让下一个包进入缓存区顶掉原来的包,否则就会从缓冲区中继续提取原来的包并再一次发出。

(3)重发的时机

        这里又引出了另外一个问题,发送方发送出包后既然要等待接收方回复ACK包后才会发出下一个包。如果迟迟没有ACK包过来,则默认原来的包已丢失并重新发送原来的包,那么它这个等待时间会是多久呢?假设我们设置一个固定的等待时间,超出这个等待时间就重新发送包,如果刚好此时通信双方的网速比较差,接收方成功接受网络包后发回ACK包的时间稍微超出了这个等待时间,发送方这边却已经默认包丢失了重新发了一个。这看起来好像并没有什么太大影响,但是如果此时出现了很多多余的重传无异于加剧网络的拥堵,造成通信效率越来越慢,那这时候就不是重传而是加塞了。

        对于这个问题,TCP模块采取的解决方法是动态调整等待时间。也就是说从网络包一开始发送那一刻开始,TCP模块就持续监控收发网络包等待时间的长短。如果收到ACK包的时间很快,那么就缩短重发的等待时间,如果收到ACK包的时间很慢,那么就延长重发的等待时间。当然,一开始会设置一个非常短的初始等待时间值。

(4)窗口和窗口大小

     我们总是讲计算机的数据处理效率是非常惊人的,但在以上我们描述的场景中,似乎TCP模块从发出一个包后到接收到接收方返回的ACK包之间的这段时间里似乎什么都没有干?这种单线程式的任务处理方式简直就是在浪费计算机的强大处理能力。于是我们引出了窗口这个概念。

        简单来说,就是发送方会一次性发出很多个网络包给接收方,接收方会将接收到的包进行处理,如果确认是给自己的,就会返回ACK包给发送方,并且从收到的包中取出数据放到缓冲区中等待应用程序提取。在这个过程中发送方发出的包的数量,并不会超出接收方的最大处理限度,接收方也会同时处理很多个包并给回对应的ACK包。这个接收方的最大处理限度就是窗口大小,而这种同时接收方同时处理多个网络包的方式就叫滑动窗口

        我们很容易联想到这样一种场景:发送方的协议栈发出了ABCD四个(实际上远不止)网络包给接收方的协议栈,接收方的协议栈可能先处理完了B网络包,这时就需要返回B对应的ACK包了,发送方接收到B对应的ACK包后就知道B已经被接收方收到了(也就不需要重传),然后继续发送出E网络包,这时可能发送方又收到了AD的ACK包,于是发送方知道接收方的缓冲区又可以再多处理两个包了,就把FG也发送了过去。通信双方的就这样来来回回,直到完成数据的传输并断开连接。

(5)窗口更新

        下面来解释一下,发送方之所以只发出了四个包是因为在建立连接阶段知道了接收方的窗口大小(假设是4,这个数字在通信双方相互交换通信控制信息时确定),于是发送方不会在同一时间发出超出接收方处理能力的包的数量。并且在收到接收方返回的ACK包后,发送方可以自己计算出当前的窗口大小并确定接下来要发送的包的数量。比如现在发送方发送了4个包,等于接收方窗口大小的数量,那么在得到接收方的进一步回复前,发送方不会再发送新的包。如果等待过程中收到了接收方返回的一个ACK包,那么发送方就知道那边有一个处理位置空出来了,就会发一个新的包过去,如此循环往复,我们可以把发送方动态计算接收方窗口大小的这个动作称之为窗口更新

        问题是发送方是怎样知道这个窗口更新的时机的呢?在我们想象的场景中发送方每接收到接收方返回的ACK包时就会更新当前的窗口大小,这似乎非常的理所当然。但是接收方每收到一个包时都要返回对应的ACK包的这种行为本身就让网络中充斥着很多ACK包,而ACK包的作用实际上只是为了通告发送方自己有接收到数据后对发送方发出的提示信息而已。于是为了提高网络的传输效率,接收方这边同样设计了一个等待时间,假设接收方成功接收到了某个包并且生成了对应的ACK包,它并不会立即发出给发送方,而是进行一个时间段(在计算机中通常极短)的等待,等待有多个连续的ACK包积累起来时,再统一发回一个包含最终ACK号的ACK包。

        然而实际上决定窗口更新时机的并不是ACK包,而是接收方的应用程序从缓冲区提取出数据的时候,这时接收方的协议栈才会生成一条请求更新窗口大小的请求信息(包含当前的窗口大小)并发出。发送方会根据这个从接收方传回的请求信息去确认当前的窗口大小。而如果接收方的应用程序非常频繁的从缓冲区中读取数据,同样也会产生多个请求信息,这时接收方就会和对待ACK包一样,进行一段时间的等待,但只发送最新的那条更新窗口的请求信息。


2、连接阶段

        连接阶段的本质是通信双方交换自己的通信控制信息。既然双方都创建好了套接字(控制信息),那么下一步就是获取对方的IP地址和端口号等信息了,有了这些信息,当数据进行收发时才知道要发送给谁。而通信控制信息可以分为两类:一类是保存在套接字,也就是连接阶段中收到的控制信息;另一类是保存在网络包头部,用以进行网络传输的通信控制信息。我们可以把连接阶段我方主机与服务器建立连接的过程抽象化为“三次握手”,这也是描述TCP模式通信的经典术语了。

        用以建立连接的通信控制信息的主要字段有:本地IP地址和端口号、目标服务器的IP地址和端口号(这个Web服务的端口号一般是默认的,所以源主机一开始就知道)、序号(网络包数据块的起始序号)、窗口大小开辟缓存区。通信双方会就这部分信息进行交换。具体的步骤为:① 源主机的协议栈发出连接请求,而这个连接请求实际上就是一个网络包,这个网络包没有数据块,只是含有一个TCP头部,其中记录了我们刚刚提到的控制信息字段。需要注意的是TCP头部中的SYC字段被设置为了1,这是为了标识它是一个发起请求的网络包;② 目标服务器的协议栈接收到连接请求后,会识别其中的字段,知道了它是一个请求包并且是发送给自己的以后,就会提取出其中的控制信息字段并记录在自己的套接字中。并返回一个ACK包用来告诉源主机:我收到你的请求了。③ 源主机收到目标服务器返回的ACK包以后,这时才会将收到的控制信息写入自己的套接字,至此连接正式建立。这个过程实际上就是“三次握手”。

               

3、收发阶段

        前面我们讲述了什么是网络包,我们提到协议栈会将HTTP报文这种数据信息作为二进制数据进行切割,分成一个又一个小的数据块,然后给这些数据块加上头部信息就变成了网络包。TCP收发阶段遇到的问题其实就是如何确保传输的数据的准确性(还原顺序正确)和完整性(内容没有丢失),解决方案就是为每个数据块添加头部控制信息。那么头部控制信息是如何做到解决问题,它自己又包含了哪些内容呢?下面我们重点围绕序号ACK号进行讨论。

        网络包的头部信息中包括非常多的内容,在这里我们只简单讨论TCP头部,TCP头部信息中记录了有下列字段:

在这些字段中最重要的除了收发方的端口号外,就是序号和ACK号了。

        我们知道当网络包被发送给目标服务器的协议栈后,其中包含的数据会被按照一定的顺序重新排列还原成原始数据,而这顺序的依据就是来自TCP头部字段中的序号。可以简单理解成,通过序号接收方就知道了当前网络包是数据中的第几个。而这个序号会被发送方写入网络包的TCP头,每个网络包的序号都是不一样的。比如发送方在发送完请求包并建立起连接以后,发送出第一个网络包,我们可以默认这第一个网络包的序号被发送方的协议栈标记为了1,那么接收方在收到这个包之后就知道这其中包含的是组成原始数据的第一段数据块。由于网络包本身有自己的长度(头部(0.04KB)+数据块(1.46KB)=1.5KB),那么发送方发出的下一个网络包的序号就会是1+网络包的数据块长度,那么下一个包的序号就会是1461,接收方会自动提取出数据块并计算长度(网络包长度都是固定的)得出1461对应的是组成原始数据的第二段数据块,然后排列在缓冲区的指定位置。这样,凭借TCP头部的序号字段,我们就解决了如何确保数据的准确性(顺序正确,不会排列错乱)的问题。

        那么我们是如何保证发送的数据的完整性的呢?首先我们知道在网络传输过程中,往往因为各种干扰或突发状况会造成信号丢失,在传输层和网络层的理解其实就是丢包。解决这个问题的关键就是在知道网络包没有被对方接收到后,重新把原来的网络包再发一遍。那么发送方是如何知道接收方没有收到自己的网络包的呢?这里去其实我们前文已经提到了,其中的奥秘就是接收方在接收到发送方的网络包后,它会发回一个对应的ACK位为1的网络包(这个包的其他控制信息能够和原来发送方发来的包进行配对,这样发送方就知道这个ACK包对应的是哪个网络包了)。凭借这种方式就保证了传输数据的完整性。当然存在的问题我们上面也有提到,就是重发的时机和效率的问题,这部分可以参考创建阶段最后水平线之后部分的内容讲解。

        最后还需要说明的是,数据的传输是双向的。当发送方发出网络包的时候,接收方同样也在返回网络包,所以发送方不仅需要在连接阶段确认给接收方的初始序号,接收方同样需要在连接阶段给发送方一个初始序号,毕竟接收方本身也可以算作一个发送端,而双方的角色往往在相互转换。

        我们之前提到的协议栈调用Socket库的程序组件进行数据的收发,以上描述的发送方所进行的动作其实可以对应库中的write函数程序。那么你可能会问,不是还有一个read函数程序吗?站在发送方源主机的视角,read函数程序只有在应用程序从缓冲区中取出数据时才会调用,所以在通信刚开始时,read函数是被默认挂起的,因为这时候缓冲区中并没有数据,于是计算机会先去继续处理其他的工作。但是当协议栈发现缓冲区中有了数据可以进行提取时(也就是服务器发回响应信息后),就会给计算机发送一段“中断信号”,计算机注意到了这个信号,这时候read函数就会被重新调用了。

4、断开阶段

        通信完成之后,会有某一方主动发起断开请求,这个过程实际上可以对应建立连接阶段的“三次握手”。具体的步骤为:① 服务器确认数据收发完毕,发起断开请求。与发送建立连接请求数据包不同的是,这次服务器的协议栈会将TCP头部的FIN位(FINISH)设置为1,标识这是一个申请发起断开连接的网络包(此时服务器实际上并没有对自己套接字中的内容做任何修改,也没有把套接字删除)。② 源主机收到FIN位为1的网络包后知道数据收发已经结束,并且服务器那边已经进入了断开连接的流程。这时它会把自己(源主机)套接字的连接状态标记为已断开,并且返回一个ACK包以告知服务器自己收到了它的断开请求(此时源主机也并没有删除套接字,只是修改了套接字中的连接状态字段为已断开)。然后源主机会自己再发送一个FIN位同样被设置为1的申请发起断开连接的网络包给目标服务器。③ 服务器收到了源主机的ACK包后,知晓源主机已经接收到了它发送的申请信息。然后又接收到了源主机发送过来的申请断开连接的FIN包,这时才会把自己的套接字中的连接状态也设置成已断开。

        至此双方的断开操作就完成了。再过一段时间双方就会删除套接字,这时候才算双方在物理状态中也不存在关联了。这个断开请求如果由源主机发起流程也是一样的,不过是把上述的服务器换成源主机,把源主机换成服务器。这里需要厘清一个概念,就是连接一方只有在收到了一个FIN位设置为1的网络包时才会将自己套接字的连接状态标记为已断开,同时套接字本身会进入删除倒计时。

        那么为什么要等待一段时间再删除套接字呢?这主要出于防止误操作的考虑,参照上面的场景,假设连接一方套接字在接收到FIN包并发出最后的ACK包后紧接着马上把自己删除,那么这个连接端口就会被空出,就有可能被分配给新的套接字。然而连接的另一方假设碰巧没有收到对面发来的ACK包(最后的波纹),那么它就会认为之前发送过去的FIN包丢失了,于是就会再发送一个FIN包给原来的端口。但是这个时候的通信端口已经狸猫换太子变成了新的套接字对象,于是新套接字可能刚刚创建就接到要断开连接的FIN包,不明所以之间标记自己的连接状态为已断开并进入了删除倒计时。

5、IP与以太网

        介绍完了TCP模块的工作内容,下面我们来介绍一下协议栈的第二个组成部分——IP模块。从之前的内容我们知道TCP模块会生成网络包(TCP头部+数据块),然后交给IP模块,IP模块则负责为网络包继续加上IP头部和以太网(MAC)头部控制信息,然后将处理过的网络包交给网卡进行下一步处理。

        IP头部中包含了收发双方的IP地址和其他控制信息(版本号、协议号等),而MAC头部则包含了网络设备的物理信息(在这里主要指网卡和最近一个路由器的MAC地址)。在这里会有个疑问,既然我们有了IP地址,还需要MAC地址做什么呢?那么就需要介绍一下以太网和子网的概念了。

        首先我们需要知道网络线路是由传输介质和连接设备组成,传输介质可以是双绞线(电信号)或光纤(光信号)等,而连接设备可以是集线器、交换机或路由器等,实际的网络线路是由这些连接工具混合组成的。在初始计算机网络中我介绍了网络设备的变迁史,以太网实际就起源自交换机的工作模式。也就是说在一个局域网下,每个设备都有自己的MAC物理地址(全球唯一),通过交换机就可以实现数据的传输交互(① 将数据发送至接收方MAC地址代表的目的地;② 以发送方的MAC地址代表发送方),而不再需要进行广播操作。既实现了一对一的通信。

        交换机之所以知道局域网中其他设备的位置,是因为交换机内部维护了一张MAC表,这个MAC表中记录了每一个接入交换机端口的设备,记录的凭据就是接入设备的MAC地址。当计算机A向计算机B发送数据时,交换机会首先查询计算机B的MAC地址,在自己的MAC表中找出该MAC地址对应的端口,然后将计算机A发送过来的数据从对应计算机B的端口传输过去。这就是交换机的工作原理。

        在这个传输过程中,并没有使用到IP地址。真正需要使用到IP地址的其实是路由器,因为在主流的TCP/IP架构下,规定了不同的子网之间通信需要经过网关。至于为什么需要网关,这是因为如果在不同的子网也通过交换机进行通信,最后的结果就是加入网络的设备越来越多,直到超出每个交换机MAC表的最大记录长度。于是出于这种考虑,我们为每个子网都分配一个地址(公网IP),子网间的通信就借助网关结合这些分配给不同子网的地址来进行通信,这个分配给每个子网的地址就是公网IP地址了。

        也就是说,在子网(局域网)内部的通信实际上是使用MAC地址的,而跨子网的通信则是使用公网IP地址,我们可以将自己的家庭局域网或公司局域网看作一个子网,而把大的互联网也看做一个子网,这两个子网之间的通信就是凭借网关IP地址来实现,在这里担任了网关角色的设备就是路由器

        这里需要明确一个点:路由器本身也可以作为一个交换机来使用。因为它同时存在LAN口和WAN口。也就是说在局域网内部通信时,连接了不同设备的路由器其实就是一台交换机,凭借自己维护的MAC表来实现局域网内不同设备的通信传输。而在局域网之外,它担任的角色就是路由器,因为它接入了运营商提供的广域网(WAN口),借助网关实现了跨子网的通信。

        举个例子,子网A中的计算机a如果想和子网B中的计算机b进行通信,会经过这么一个过程:计算机A先向路由器发送消息,路由器如果发现消息的接收方(计算机b)IP地址刚好在同一个子网(子网A)内,就会根据MAC表做个交换机式的内部信息转发。但是如果发现这个地址并不是当前子网A中的,那么此时就会真正发挥路由器的作用,从路由表中寻找接收方网络(子网B)的IP地址,然后将消息从子网A中发出到子网B,然后子网B的路由器接收到就会识别出消息接收方IP地址是在同一个子网中,这时候路由器就会做一个MAC转发,将消息发送给自己LAN口对应的计算机b。

        现在我可以来介绍一下IP协议和MAC协议的具体工作了。被IP模块添加了IP头部和MAC头部的网络包进入网络后,首先会被发到最近的一个路由器,然后这个路由器会把网络包的MAC头丢弃,并根据网络包IP头部的目标服务器IP地址在自己的路由表中寻找能通往目的地的下一个路由器结点的IP地址,找到下一个路由器结点的IP地址以后会使用ARP获得这个IP地址对应的MAC地址,然后根据这个新的MAC地址信息生成新的MAC头部添加到网络包上,网络包获取到MAC头部后就知道下一个设备的物理地址,于是继续在以太网中进行传输,知道进入目标子网被目标服务器的协议栈解析。

        上面可能会存在一些疑惑,所谓的以太网传输究竟是什么样的?ARP又是什么?在这我解释一下,当两个设备需要在不同网段间传输时,这个网段在传输过程中实际上会被切割成很多个最小的同一网段,也就是说从源主机到目标服务器经过了很多个子网(局域网),而每两个子网间的路由设备有肯定会有直接线路相连,从子网A的路由器到子网B的路由器实际上就是做了一次交换机式的MAC转发,因为路由器A和路由器B存在直接关联时可以判断为归属同一局域网,因为它们之间有直接的端口连接关系(物理关系或者说MAC关系),于是网络包就在不同的局域网间转发直到到达目标网络,这就是以太网传输的本质。而ARP就是一种根据IP地址获取MAC地址的技术,实际上就是在同一个局域网下,假设设备A需要知道设备B的MAC地址,那么它就会在局域网中进行广播(设备B的MAC地址是多少),设备B收发了这条广播就会返回自己的MAC地址给设备A,如果其他无关设备收到了广播发现不是给自己的就会无视掉。

6、网卡和数字信号

(1)从数字信号到电信号

        介绍完了IP模块的功能,下一步就是将网络包交给网卡。网卡的主要工作,是把网络包这种数字信号的格式,转换为电信号并发送到网络中。首先我们来看看网卡的主要组成:缓冲区、MAC模块、PHY/MAU信号收发模块、物理接口。我们接下来根据网络包在网卡中的具体处理流程,分别介绍这些网卡组件的工作内容。

        网卡是由网卡驱动控制的,IP模块会将网络包(以太网包)交给网卡驱动,网卡驱动会将网络包复制到网卡的缓冲区,并向MAC模块发送处理指令。MAC模块收到指令后会从缓冲区中取出网络包,为网络包加上三条新的控制信息:报头、起始帧分隔符、帧校验序列(FCS)。要搞清楚这三个控制信息有什么作用,我们首先要从电信号在网络中的传输机制开始说起。

        我们知道网卡会将网络包的数据由数字信号转换成电信号,数字信号其实就是一连串比特序列(1001110...),而电信号是由电流和电压的变化模拟出来的信号,我们可以简单的理解为高电平代表数字信号的1,而低电平代表数字信号的0,于是数字信号和电信号的关系就对应上了。但是这其中有一个问题,就是当我们用电信号模拟一连串连续的数字信号1或0时(11111或00000),由于电流和电压本身持续没有发生变化,所以我们很难从中分析出究竟有多少个连续的1或0。于是我们引入了时钟信号这个概念,通过固定的电流变化周期和频率确定读取时机。

        简单来说,我们会先向接收方发送一连串有规律模拟时钟信号(01010101...)的电信号,接收方会读取电信号中的周期变化时间频率,然后我们再发送代表网络包的数据信号,接收方会结合先前时钟信号的频率周期,进而分析出数据信号中的比特序列。这时,哪怕数据信号中有连续的相同比特位(0或1),接收方也可以得出其中对应的具体数量。

        但是对于物理距离较远、网线较长的情况,两段信号在传输过程中可能会产生时间差,造成时钟的偏移,于是信号的内容也就不准确了。为了解决这个问题,我们采取了将时钟信号和数据信号叠加(一种逻辑上的异或操作,涉及信号调制的知识)在一起生成混合信号的方式。也就是说,接收方在收到时钟信号后,先分析周期变化和时间频率,然后根据其中的规律,从混合信号中提取(解调)出对应的数据信号。更具体的来说,就是根据时钟信号获取到的规律和变化频率,对混合信号进行边沿同步,通过分析高低电平在某个极小区间内的边沿上升下降趋势获取出对应的比特位。这里就涉及到通信学的知识了,书中也没有去具体讲解。

        总而言之,报头的作用就是一段用来确定读取时机的时钟信号,接收方先通过读取到报头获得电信号的周期频率,遇到起始帧分隔符(一般是特殊的连续比特位,例11)就知道分隔符后面的是真正数据信号的内容。那么帧校验序列在其中由起到了什么样的作用呢?实际上,帧校验序列就是通过公式结合数据信号(从头到尾)的所以比特位计算得出的一个值。因为信号在实际传输中难免会遭到噪声干扰,造成波形紊乱数据错误。而有了这么一个发送方已经确定好的值,接收方就可以在读取到数据信号的比特序列后,结合相同的公式计算出自己的结果值,并和发送方发来的值(也就是帧校验序列)进行比对,如果值相等就说明接收到的数据没有错误,如果不一样那么就会丢弃,等待重传。

        MAC模块在发送阶段的工作内容,除了为网络包添加新的控制信息,还负责将数字信号转换成电信号(这种转换速度也就是我们常说的网速)。但是这种MAC模块转换的电信号是任意格式的通用信号,在网络传输中有规定不同的网线类型和速率必须使用规定的信号格式,而这种将通用信号转换为规定格式的电信号就是PHY/MAU信号收发模块的工作 。再下一步,处理完的电信号就会经由网线进入网络,开始真正意义上的网络传输。

(2)从电信号到数字信号

        在开始这一部分的内容之前,首先需要介绍一下信号传输的两种操作模式:一种是集线器式的半双工,另一种是交换机式的全双工。简单来说,半双工就意味着在某一时刻以太网(局域网)中只能进行接收或传输其中一种信号操作,否则就会发生信号碰撞;而全双工就不存在这种问题,它允许同一时刻进行信号的发生和接收操作,这部分的内容我们会在下一章学到。在本章还是着重讲解了半双工的工作模式。

        在半双工模式下为避免信号碰撞,信号在发送进网线之前会首先确认当前网线中没有其他的信号存在,这种确认操作主要通过载波侦听来实现。如果出现了意外情况,在信号传输过程中出现了另一种信号,两种信号在网线中碰撞了,这时就会发送一段持续的阻塞信号,接着停止发送新的信号,直到网线重新恢复成空闲状态,然后设备间会根据MAC地址生成的随机数确认等待时间并错开传输时机。

        下面让我们从结合网卡和协议栈的角度,来看看网络包在接收阶段发生了什么。首先,电信号从外部网络被路由器转发到了目标子网内的集线器,由集线器通过广播的方式,将从外部网络中接收到的信号发送给线路中的所有设备(半双工模式下),目标主机从网卡的外部接口收到了发来的电信号,电信号经由信号线来到了网卡的PHY/MAU信号收发模块,PHY/MAU信号收发模块会将电信号转换还原成通用信号(MAC模块可以识别的、任意格式的通用电信号),接着通用信号被发送给了MAC模块,由MAC模块将通信信号还原成了数字信号。MAC模块在还原这部分的工作其实做了很多:首先,MAC模块读取了信号一开头的报文,确定下了时钟信号,然后根据这个时钟信号的变化周期频率,在读取到起始帧分隔符后,正式进行内容解析工作将电信号还原成比特序列(数字信号),在读到尾巴时还需要根据还原出的比特序列结合公式计算FCS值,用来和发送方发送过来的帧校验序列进行对比,如果对比结果不一致的,那么就丢弃这段信号等待重发。否则就进行下一步操作,也就是从这段数据信号中取出MAC地址值,用来判断是不是和自己MAC模块中网卡驱动分配的MAC值一致,如果一致就说明是发送给自己的,就会将数据信息放到缓冲区中,否则同样直接丢弃。

        进入了网卡缓冲区的数据信息就重新回到了网络包这个概念。MAC模块会提示网卡驱动向计算机(通过总线接口)发送中断信号,于是协议栈开始工作。协议栈的IP模块会取出缓冲区的网络包(IP包:MAC头此刻已经完成使命被丢弃了),我个人理解IP包中的收发方IP地址这个字段只有在建立连接阶段(确认套接字控制状态信息)和网络传输过程(路由交换传输)中有用途。而在收发阶段,进入协议栈的IP包最重要的信息还是协议号(TCP、UDP等)这个字段,因为可以根据它来确认接下来要分配给哪一个协议栈模块。假设IP模块读到的网络包中的协议号为06(也就是TCP协议),那么这个网络包就会发送给TCP模块进行下一步处理,同时网络包的IP头部也完成了自己的使命,可以理解为到现在被交给TCP模块的就只是一个带TCP头部+数据块的网络包了。

        这里需要补充一个之前没有讲到的关于IP模块的知识点。对于局域网和网线的传输来说,TCP模块生成的网络包还是太大了。所以在发送阶段,IP模块会对TCP模块发过来的网络包进行二次切割,也就是IP分片,以此生成很多更小的网络包(IP包)以便于传输。而这些更小的网络包存在和TCP模块中用来确保网络包准确性和完整性的字段相同作用的字段,可以简单理解为TCP包的序号对应IP包的ID,而IP包的分片偏移量字段则用于区分拥有同样ID的IP小包之间的先后顺序。在接收阶段,IP模块会将接收到的网络包(IP头部+TCP包)的数据,根据ID和分片偏移量按照顺序存放在自己的内存中,进而将ID相同的IP小包重新拼凑成一个完整的TCP网络包,这个过程叫做分片重组。然后IP模块会将这个网络包交给TCP模块。

        TCP模块收到这个网络包后,会根据包的内容做出相应的操作。如果是用来建立或断开连接的控制包,那么TCP模块就会根据包的端口号找到本地对应的套接字,再根据包的控制信息更新套接字的控制状态信息,并返回相应的ACK响应包,并告知应用程序建立连接或断开状态;如果是包含应用程序数据的传输包,那么就会返回对应的ACK响应包,然后从包中取出数据放在缓冲区中,等待应用程序的读取。

7、UDP协议

        在本篇前半部分我们见识到TCP协议传输数据时的复杂机制,不仅需要建立连接,在数据收发时还需要时时监控包的接收情况,没有收到时就要重新发送,同时为了实现某些遗失包的重发,我们不得不在网络包中记录序号、长度和偏移量等字段,由此衍生出来了接收确认和窗口等机制。在这种复杂机制的加持下,我们可以成功确保数据传输的准确性和完整性。但是也付出了相应的代价,那就是传输速率的下降,在我们重传包的过程中肯定会产生一定的时间成本,这是不可避免的。

        但在某些更加追求传输速率的场景中,我们有了更加适合的传输模式,这就是UDP。它只负责发送带有UDP头部(只包含接收方端口号等少量信息,没有ACK字段和序号)的网络包,然后交由IP模块进行进一步处理。从中我们可以看出,UDP模式下的包传输完全没有为包的重传做准备,它只会一股脑的把包发送过去,不管接收方有没有接收到。于是UDP就不需要像TCP那样又要建立连接、又要监视接收情况、又要考虑重发。

        UDP的适用场景是像视频、音乐播放中,如果在这个过程中某些包没有收到,那么按照TCP的角度来说就需要进行重传,但是这个时候视频已经卡顿了,重传也无济于事。而在UDP的角度看来,一定程度的失真和卡顿是可以接受的,所以在保证带宽的情况下,UDP比TCP的表现更好。

        总的来说,TCP和UDP各有优劣,只是适应场景不同。TCP适用于对可靠性要求较高的应用,如文件传输、Web浏览、电子邮件等;UDP适用于对实时性要求较高,能够容忍一定数据丢失的应用,如音频、视频、在线游戏等。


       

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值