Loading... # 网络是怎样连接的 笔记 ![目录.png](https://cdn2.feczine.cn/2023/03/25/641e9c8be18f9.png) ## 第一章 浏览器生成消息 ### URL ```text 协议类型 http: https: ftp: file: mailto: ``` ![web_1.1.jpg](https://cdn2.feczine.cn/2022/11/11/636e05f5d173c.jpg) ![web_1.3.jpg](https://cdn2.feczine.cn/2022/11/11/636e05f923bcb.jpg) ### HTTP协议 Request:Method + URI Response: Status Code + Header + Body + ....... ![web_p12.jpg](https://cdn2.feczine.cn/2022/11/11/636e07d20ff92.jpg) 客户端向Web服务器发送数据时,会先发送头字段 收到请求消息后,服务器会对内容进行解析,通过URI和方法来进行处理,然后将结果放在响应消息中,响应消息开头有一个状态码,后面就是头字段和数据。 响应消息会被发送回客户端,客户端收到之后,浏览器会从消息读出所需的数据并显示在屏幕上 #### 格式 ![web_p15.jpg](https://cdn2.feczine.cn/2022/11/11/636e09a888e73.jpg) ```text // 请求方法 + 请求URI + HTTP版本 [Method] [URI] HTTP1.1 // 消息头 存放例如 cookie,日期,支持语音,压缩格式等 [ReqHeader] // 消息体 // 完全没有内容的空行 [Block line] // 需要发送的数据 [ReqBody] ``` #### 状态码 ![web_p20.jpg](https://cdn2.feczine.cn/2022/11/11/636e0b45bd7e3.jpg) ### 通过DNS查询IP地址 生成HTTP消息后,下一步就是根据域名查询IP地址 互联网是基于TCP/IP设计的,由一些小的子网,通过路由器连接起来组成一个大的网络。 ![web_p26.jpg](https://cdn2.feczine.cn/2022/11/11/636e0eaf54c84.jpg) #### IP IP地址是一串32位的数字,按照8位一组,分为4组,用十进制表示,再用 `.` 隔开 在IP地址的规则中,网络号和主机号连起来总共是32位,但这两部分的具体结构是不固定的, 因此我们还需另外的附加信息来表示IP地址的内部结构,即 `子网掩码`。 ![web_p27.jpg](https://cdn2.feczine.cn/2022/11/11/636e0fe28ec99.jpg) 子网掩码的格式如下图 ② 所示,是一串32位数字,其左侧为1,右侧为0 其中,为1的部分表示网络号,为0的部分表示主机号 也可把1的部分用十进制表示并写在IP右侧。 主机号全0代表整个子网 主机号全1代表向子网的全部设备发包,即广播 (例:UDP广播) ![web_p28.jpg](https://cdn2.feczine.cn/2022/11/11/636e13a7644c8.jpg) ##### 域名和IP地址并用的理由 使用IP地址只需要处理4字节的数字,而域名需要处理几十个到255字节的字符,增加了路由器的负担,也增加了延迟。 #### Socket库 查询IP 注:Socket库是用于调用网络功能的程序组件集合 对于DNS服务器,我们计算机上一定有相应的DNS客户端,而相对于DNS客户端的部分称为DNS解析器,通过DNS查询IP地址的操作称为域名解析。 解析器是一段程序,包含在Socket库中 根据域名查询IP时,浏览器会调用解析器,解析器会向DNS服务器发送查询消息,然后DNS服务器 会返回响应消息,其中包含查询到IP地址,解析器会将IP地址写入到浏览器指定的内存地址中。 ##### 解析器 原理 当浏览器调用解析器时,程序的控制流程就会转移到解析器的内部。 当控制流程转移后,解析器会生成要发送给DNS服务器的查询消息,与生成HTTP消息的过程类似,并将它发送给DNS服务器,发送这一操作是委托给操作系统的协议栈执行的。当解析器调用协议栈后,控制流程再次转移,协议栈会执行发送消息的操作,然后通过网卡将消息发送给DNS服务器。 当DNS服务器收到消息后,它会根据消息中的查询内容进行查询,如果要访问的Web服务器已在DNS服务器上注册,那么这条记录就能被找到,然后其IP地址会被写入响应消息并返回给客户端。消息经过网络到达客户端,再经协议栈传递给解析器,然后解析器读取出IP地址,并将IP地址写入指定的内存地址。 注:向DNS服务器发送消息是,我们需要知道DNS服务器的IP地址,只不过这个IP地址作为 TCP/IP 的一个设置项目事先准备好了,不需要再进行查询。 ### DNS服务器 原理 客户端发送的查询消息包含以下3种信息: ```text (a) 域名 // 在最早设计DNS时,DNS在互联网以外的其他网络中的应用也被考虑到了,Class就是用来识别网络的信息。不过如今除了互联网并没有其他网络,因此Class的值永远是 IN . (b) Class // 对于不同记录的类型,返回的信息也会不同 // 例:A IP地址 // 例:MX 邮件服务器 (c) 记录类型 ``` #### 域名的层次结构 域名与IP地址的对照信息存放在多台DNS服务器中,这些DNS服务器相互接力配合,从而查找出要查询的信息。 DNS服务器中的所有信息都是按照域名以分层次的结构来保存的。 DNS中的域名都是用 `.` 进行分割的看,例如 `blog.mashiro.ski` 在域名中,越靠右的位置表示其层级越高。 这种具有层次结构的域名信息会注册到DNS服务器中,而每个域都是作为一个整体来处理的 即,一个域的信息是作为一个整体存放在DNS服务器中的,不能将一个与拆开存放在多台DNS服务器中,但一台DNS服务器可以存放多个域的信息。 #### 寻找相应的 DNS服务器 并获取 IP地址 首先,将负责管理下级域的DNS服务器的IP地址注册到它们的上级DNS服务器中去,然后上级DNS服务器的IP再向上注册,以此类推。 在 `com, net, org`等顶级域的上面还有一级域名,被称作 `根域` `根域` 不像顶级域一样拥有自己的名字,因此书写时经常被忽略,如果要明确表示根域,应该像 `cloudflare.com.` 一样,在最后加一个 `.`,就代表根域 不过一般都不写,因此根域的存在经常被忽略 根域的DNS服务器中保管着顶级域的DNS服务器信息,由于上级DNS服务器保管着所有下级DNS服务器的信息,所以可以自顶而下,找到任意一个域的DNS服务器。 因此,只要客户端能找到任意一台DNS服务器,就可以通过它找到根域的DNS服务器,然后自顶而下,找到存有所需信息的DNS服务器。 分配给根域的DNS服务器的IP地址在全世界仅有13个,而这些地址几乎不发生变化,因此将这些地址保存在所有的DNS服务器中也并不是一件难事 (根域的DNS服务器信息已经保存在DNS服务器程序的配置文件中了) 注:根域DNS服务器在运营商使用多台服务器来对应一个IP地址,所以尽管IP数量只有13个,但实际的服务器数量还是很多的。 ![web_p42.jpg](https://cdn2.feczine.cn/2022/11/11/636e303f715e7.jpg) ![web_p43.jpg](https://cdn2.feczine.cn/2022/11/11/636e303fa8104.jpg) #### DNS缓存 DNS服务器有一个缓存功能,可以记住之前查询过的域名,如果要查询的域名和相关信息已经在缓存中,那么就可以直接从缓存中得到所需的信息。 但原本的信息可能会发生改变,此时缓存的信息就变得不正确了,因此缓存有一个有效期,当缓存的信息超过有效期后,这些信息就会从缓存中删除 ### 委托协议栈发送信息 同样需要调用Socket库,但需要按指定顺序调用多个程序组件 我们可以将数据通道想象成一条管道,将数据从一段送入管道,数据就会到达管道的另一端然后被取出,数据的流动是双向的,这些通道的出入口称为 `套接字`。 ![web_p46.jpg](https://cdn2.feczine.cn/2022/11/11/636e35d57391d.jpg) 管道的生命周期是这样的: 1.服务器创建套接字,等待客户端向该套接字连接管道 (创建套接字阶段) 2.客户端创建一个套接字,连接到服务器的套接字上 (连接阶段) 3.收发数据 (通信阶段) 4.断开管道并删除套接字 (断开阶段) 管道在连接时是有客户端发起的,但在断开时可以由客户端或服务器任意一方发起 当管道断开后,套接字就会被删除 注:以上四个操作都是由协议栈来执行的,浏览器等应用程序并不会自己去做,而是委托给协议栈代劳 #### 创建套接字阶段 应用程序调用Socket库中的socket程序组件 控制流程会转移到socket内部并执行创建套接字的操作,然后控制流程会回到应用程序 套接字创建完成后,协议栈会返回一个 `描述符`,应用程序会将其存放在内存中 在同一台计算机上可能同时存在多个套接字,我们需要一种方法来识别出某个特定的套接字,也就是`描述符`的作用 当创建套接字后,我们就可以使用这个套接字来执行收发数据的操作 这时我们只要出示描述符,协议栈就能够判断出我们希望用哪一个套接字来连接或收发数据 #### 连接阶段 应用程序调用Socket库的connect的程序组件 需要传入 `描述符、IP地址、端口号` 三个参数 服务端端口号是根据应用的种类实现规定好或设定好的 例如:80, 443, 25, 22是规定的 而一些程序可以在配置文件内修改端口 客户端在创建套接字时,协议栈会为这个套接字随便分配一个端口号 当协议栈进行连接操作时,会将这个端口号通知给服务器 连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,端口号就是这样一种识别方式。当同时指定IP地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。 描述符是和委托创建套接字的应用程序交互时使用的,并不是用来告诉网络连接的另一方的 如果说描述符是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能识别出套接字的机制 #### 通信阶段 ##### 发送 应用程序调用Socket库的write程序组件 需要传入 描述符、发送数据 ##### 接收 应用程序调用Socket库的read程序组件 调用read时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区 当消息被存放到内存缓冲区时,就相当于已经转交给了应用程序 #### 断开阶段 调用Socke库的close程序组件 连接在套接字之间的管道会被断开,套接字本身也会被删除 Web使用的HTTP协议规定,当Web服务器发送完响应消息后,应该主动执行断开操作 因此服务器会首先调用close来断开连接,断开操作传达到客户端之后,客户端的套接字也会进入断开阶段 接下来,浏览器调用read执行接受数据操作时,read会告知浏览器收发数据操作已结束,连接已断开,浏览器得知后,也会调用close进入断开阶段 ## 第二章 用电信号传递 TCP/IP 数据 学习 网络控制软件(协议栈)和网络硬件(网卡)是如何发送数据的 ### 创建套接字 #### 协议栈的内部结构 ![web_p61.jpg](https://cdn2.feczine.cn/2022/11/11/636e427e10d8c.jpg) 协议栈的上半部分有两块,分别是负责用TCP协议收发数据的部分和用UDP协议收发数据的部分,它们会接受应用程序的委托执行收发数据的操作 下面一半是用IP协议控制网络包收发操作的部分 在互联网上传送数据时,数据会被切分成一个一个网络包,而将网络包发送给通信对象的操作就是由IP来负责的。 还包括ICMP协议和ARP协议 ICMP用于告知网络包传送过程中产生的错误以及各种控制信息 ARP用于根据IP地址查询的 MAC地址 #### 套接字的实体就是通信控制信息 在协议栈内部有一块用于存放控制信息的内存空间,记录了用于控制通信操作的控制信息 协议栈是根据套接字中记录的控制信息来工作的 #### 调用socket时的操作 ##### 创建套接字阶段 协议栈首先会分配用于存放一个套接字所需的内存空间 写入表示初始状态的控制信息 将表示这个套接字的描述符告知应用程序 ### 连接服务器 连接实际上是通信双方交换控制信息 连接操作中所交换的控制信息是根据通信规则来确定的 当执行收发数据操作时,需要一块用来临时存放要收发的数据的内存空间,这块内存空间称为缓冲区 #### 负责保存控制信息的头部 控制信息可以分为两类 第一类是客户端和服务器相互联络时交换的控制信息 这些内容在TCP协议的规格中进行了定义 这些字段是固定的,每次客户端与服务器进行通信时,都需要提供 ![web_p71.jpg](https://cdn2.feczine.cn/2022/11/11/636e4cabc9bc9.jpg) 这些信息被添加在客户端与服务器传递的网络包的开头,因此被称为头部 为避免各种不同的头部发成混淆,一般记作 TCP头部、MAC头部、IP头部 第二类是保存在套接字中,用来控制协议栈操作的信息 应用程序传递的信息、从通信对象接收到的信息以及收发数据操作的状态等信息会保存在这里 协议栈会根据这里的信息来执行每一步操作 套接字的控制信息和协议栈的程序本身其实是一体的,因此协议栈需要的信息会因为协议栈本身的实现方式不同而不同 #### 连接操作的实际过程 从应用程序调用`connect`开始,传入了 描述符、IP地址、端口号 其中IP地址、端口号被传递给协议栈中的`TCP模块` ##### TCP头部 客户端先创建一个包含很多开始数据收发操作的控制信息的头部 通过头部中的发送方和接收方端口可以找到需要连接的套接字 然后将头部中的控制位的 `SYN位` 设置为1,它表示连接。还需要设置适当的序号和窗口大小 ##### TCP握手 TCP模块会将信息传递给IP模块并委托它发送 IP模块执行网络报发送操作后,网络包会通过网络到达服务器,然后服务器上的IP模块将收到的数据传递给TCP模块,TCP模块根据TCP头部中的信息找到端口号对应的套接字 找到套接字后,套接字中会写入相应的信息,并将状态改为正在连接 上述操作完成后,服务器TCP模块会返回响应,这个过程和客户端一样,需要在TCP头部中设置发送方和接收方以及SYN位 (如果拒绝连接,则设置RST位为1),此外还需要将控制位的ACK (Acknowledge character) 位设为1,表明已接收到相应的网络包 之后,服务器上的TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应 网络包通过网络回到客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功 (SYN位为1),这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态修改为连接完毕 最后,客户端也需要将控制位的ACK位设置为1并发回服务器,表明刚刚的响应包已收到 ### 收发数据 从应用程序调用`write`将要发送的数据交给协议栈开始 应用程序在调用`write`时会指定发送数据的长度 协议栈在接收到数据后会将数据存放在内部的发送缓冲区中,并等待下一段数据 这样做的原因是: 应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序会在实现上有所不同 一次将多少数据交给协议栈是由应用程序决定的,协议栈并不能控制,因此如果收到就马上发送,可能会发送大量的小包,到网络效率下降 因此需要在数据积累到一定量再发送出去,至于要积累多少,不同种类和版本的操作系统会有所不同,但都是根据以下因素判断的 第一个要素是MSS - MTU MTU (Maximum Transmission Unit) 最大传输单元,表示一个网络包的最大长度 在以太网中一般是1500字节 (在ADSL等网络中,会小于1500),包含头部长度 - MSS MSS (Maximum Segment Size) 最大分段大小,由MTU减去头部长度得到 TCP和IP头部加起来一般是40字节,因此MSS的大小一般是1460字节 ![web_p77.jpg](https://cdn2.feczine.cn/2022/11/14/63713830f03f7.jpg) 另一个要素是时间 当应用程序发送数据的频率不高时,如果每次都要等到长度接近MSS再发送,可能会因为等待时间过长导致发送延迟 为此,协议栈内部有一个计时器,当经过一定时间 (毫秒级) 之后,就会把网络包发送出去 但是上面两个要素是互相矛盾的 如果长度有限,那么网络效率会提高,但是延迟会增大 如果时间有限,那么延迟会减小,但是网络效率会降低 因此发送时需要综合考虑两个要素以达到平衡,但TCP协议规格中没有规定如何平衡 因此不同种类和版本的操作系统在相关操作上也就存在差异 #### 数据拆分 发送缓冲区中的数据超过MSS的长度时,数据就会被以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中 当需要发送这些数据时,就在每一块数据的前面加上TCP头部,然后交给IP模块执行发送操作 ![web_p78.jpg](https://cdn2.feczine.cn/2022/11/14/63713a65cc94f.jpg) #### 使用ACK位确认网络包已收到 TCP具备确认对方是否成功收到网络包,以及对方没收到时重发的功能,因此在发送网络包之后,接下来还需要进行确认操作 这一机制非常强大,无论网络中发生任何错误,我们都可以发现并重新发包 因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包 如果发生网络中断等问题,在怎么重新发包都没有用的情况下,TCP会在尝试几次重传无效后强制结束通信,并向应用程序报错 ##### 原理 首先,TCP模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,在发送数据时,将算好的字节数写在TCP头部中,“序号” 字段就是用来填写这个数据的 然后,发送数据的长度也需要告知接收方,不过是通过整个网络包长度减去头部长度得到的 有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始的,长度是多少了 通过这些信息,接收方可以检查收到的网络包没有遗漏 例如: 上次接收到1460字节,那么下一个包的序号应为1461,如果收到了序号为2921的包,那么就说明中间的包有遗漏 如果确认没有遗漏,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入TCP头部的ACK号中发送给发送方 (同时需要将控制位的ACK位设置为1) 这个返回的ACK号的操作被称为确认响应,通过这样的方式,发送方法就能够确认对方到底收到了多少数据 注:收到一个包就返回一个ACK号 在实际的通信中,序号并不是从1开始,而是需要用随机数计算出一个初始值,这是因为如果序号都从1开始,整个通信过程会非常容易预测,从而导致攻击的发生 但如果是随机的,对方就不清楚了,因此需要在开始收发数据之前将初始值告知通信对象 在之前的操作中,有一步将 SYN控制位 设为1的操作,就是在这一步将序号的初始值告知对方的 在设置SYN控制位为1的同时,还需要设置序号字段的值为序号的初始值 注:SYN (Synchronize),意思是通过告知初始序号使通信双方保持步调一致,以便完成后续的数据收发检查 ![web_p80.jpg](https://cdn2.feczine.cn/2022/11/14/637150ebdb3c7.jpg) ##### 双向传输 上文只考虑了单相的数据传输,但TCP数据的收发是双向的,在客户端向服务器发送数据的同时,服务器也会向客户端发送数据 因此只需增加一种左右相反的情形就可以了 首先,客户端先随机出一个序号,然后将序号和数据一起发送给服务器,服务器收到之后会计算ACK并返回给客户端 相反的,服务器也先随机出一个序号,然后将序号和数据一起发送给客户端,客户端收到后计算ACK号返回给访问 客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中胡先告知自己计算出的序号初始值 ![web_p82.jpg](https://cdn2.feczine.cn/2022/11/14/637156236f1c4.jpg) TCP采用这样的方式确认对方是否收到了数据,才对方确认之前,数据会被保存在发送缓冲区中 如果对方没有返回某些包对应的ACK号,那么就重新发送这些包 #### 根据网络包平均往返时间调整ACK号等待时间 返回ACK号的等待时间 (超时时间) 当ACK号的返回变慢,这是我们就必须将等待时间设置得稍微长一点,否则可能会发生已经重传了包之后,上一次的ACK号才刚刚收到的情况 在局域网中,ACK号几毫秒就可以返回;而互联网拥堵时,可能需要几百毫秒才能返回ACK号 正因为波动如此之大,所以将等待时间设置为一个固定值并不是一个好办法 因此,TCP采用了动态调整等待时间的方法 即,在发送数据的过程中持续测量ACK号的返回时间 如果ACK号返回变慢,则相应延长等待时间 如果ACK号马上返回,则相应缩短等待时间 注:由于计算机的时间测量精度较低,ACK返回时间过短时无法被正确测量,因此等待时间有一个最小值,这个值在每个操作系统上不一样,基本上是0.5s~1s之间 #### 使用窗口有效管理ACK号 如果发送一个包就等待一个ACK号,那么中间的等待时间就会浪费 因此TCP采用滑动窗口来管理数据发送和ACK号的操作 滑动窗口就是 在发送一个包之后,不等待ACK号返回,直接发送后续一系列包 ![web_p85.jpg](https://cdn2.feczine.cn/2022/11/14/6371733a18243.jpg) 使用滑动窗口可能会出现发送包的频率超过接收方处理能力的情况 具体解释就是: 当接收方接收到TCP收到包之后,会先将数据存放到接收缓冲区中,然后接收方计算ACK号,将数据块组装起来还原成原本的数据并传递给应用程序 如果这些操作还没完成,之后又有很多包到达,数据还是会存放在接收缓冲区里,但如果缓冲区溢出,之后的数据就进不来了,因此接收方就接收不到后面的包了 因此,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,来避免上述情况的发生 ##### 具体工作方式 接收方将数据暂存到接收缓冲区中并执行接收操作 当接收完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了 这时接收方会通过TCP头部中的 `窗口字段` 将自己能接收的数据量告知发送方 这样发送方就不会发送过多的数据,导致接收缓存区溢出 和序号、ACK号一样,发送操作也是双向进行的 前面提到的能接收的最大数据量称为窗口大小 (一般与接收缓冲区大小一致),它是TCP调优参数中非常有名的一个 ![web_p87.jpg](https://cdn2.feczine.cn/2022/11/14/637177c2bcc71.jpg) #### ACK 与 窗口 的合并 每收到一个包,就向发送方分别发送ACK号和窗口更新这两个单独的包 接收方给发送方发送的包太多了,会导致网络效率下降 因此接收方在发送ACK号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间 在这个过程中成功很可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里发送了 当需要连续发送多个ACK号时,也可以减少包的数量,这是因为ACK号表示的是已接收到的数据量 也就是说,它是告诉发送方目前已经接收的数据的最后为在哪里,因此需要连续发送ACK号时,只要发送最后一个ACK号就可以了,中间的可以全部省略 当需要连续发送多个窗口更新时也可以减少包的数量,因为连续发生窗口更新说明应用程序连续请求了数据,接收缓冲区的剩余空间连续增加了,这时也只需要发送中间的结果就可以了 #### 接收 HTTP 响应消息 发送HTTP请求消息后,需要等待Web服务器返回响应消息,浏览器需要对其进行接收,这一操作需要协议栈的参与 浏览器在委托协议栈发送请求消息之后,会调用 read 程序,和发送数据一样,接收数据需要将数据暂存到接收缓冲区 首先,协议栈尝试从接收缓冲区取出数据传递给应用程序,但这时候数据刚发出去,响应消息可能还没返回,这时,协议栈会将应用程序的委托,也就是从缓冲区取出数据并传递给应用程序的工作暂时挂起,等服务器返回的响应消息到达之后再继续执行接收操作 首先,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有问题则返回ACK号 然后,协议栈将数据块暂存到数据缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序,之后,协议栈要找到合适的时机向发送方发送窗口更新 ### 从服务器断开并删除套接字 #### 数据发送完毕后断开连接 收发数据的时间点应该是应用程序判断所有数据都已经发送完毕的时候 数据发送完毕的一方会发起断开过程,不同的应用程序会选择不同的断开时机 协议栈允许任意一方发起断开过程 以服务器发起断开过程为例 服务器 首先服务器的应用程序调用Socket库的close程序 然后服务器的协议栈生成包含断开信息的TCP头部,即 将控制位的FIN位设为1 并委托IP模块向客户端发送数据 客户端 首先,收到服务器发来的FIN位为1的TCP头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态 然后,未告知服务器已收到FIN位为1的包,客户端会向服务器返回一个ACK号 注:客户端的应用程序可能在收到FIN包之前就来读取数据,这时读取操作会被挂起,直到FIN包到达 之后应用程序来读取数据,如果接收缓冲区中还有数据,那么这些数据将会传递给应用程序,协议栈会告知应用程序来自服务器的数据已经全部收到了 接着客户端应用程序会调用close来结束数据收发操作,客户端也会像服务器一样发送一个FIN位为1的包,然后服务器会返回一个ACK号给客户端 之后,服务器和客户端的通信就全部结束了 ![web_p91.jpg](https://cdn2.feczine.cn/2022/11/20/637a3e5c65447.jpg) #### 删除套接字 和服务器的通信结束之后,套接字并不会立即被删除,而是会等待一段时间之后再被删除 等待的这段时间是为了防止误操作 例如:客户端返回的ACK号丢失了,服务器没有收到,可能会重发一次FIN包,如果这时客户端的套接字已经删除,端口被释放,而恰巧又有新的套接字使用了这个端口,收到了服务器重发的FIN包,新的套接字开始执行断开操作,错误就发生了 至于具体的等待时间,协议没有明确规定,这和包的重传方式有关,通常持续几分钟 一般来说等待几分钟之后再删除套接字 #### 数据收发小结 ![web_p91.jpg](https://cdn2.feczine.cn/2022/11/20/637a3e5c65447.jpg) ### IP与MAC的包收发操作 #### 网络包的基本知识 包是由头部和数据两部分构成的 头部包含目的地址等控制信息 头部后面就是委托方发送给对方的数据 首先,发送方的网络设备会负责创建包,创建包的过程就是生成含有正确控制信息的头部,然后附上要发送的数据 接着,包会被发往最近的网络转发设备,转发设备会根据头部中的信息判断接下来应该发往哪里,这个过程需要用到一张表 (路由表),这张表里记录了每一个地址对应的发送方向,也就是按照头部里记录的目的地址在表里进行查询,并根据查询到的信息判断接下来应该发往哪个方向 经过多个转发设备的接力之后,包最终就会到达接收方的网络设备 发送方和接收方是相对的,因此我们不需要明确区分,在这里将发送方和接收方统称为 `终端节点` ,相应的,转发设备被称为 `转发节点` 或 `中间节点` 路由器是按照IP规则传输包的设备 集线器是按照以太网协议传输包的设备 即: IP协议根据目标地址判断下一个IP转发设备的位置 子网中的以太网协议将包传输到下一个转发设备 ![web_p96.jpg](https://cdn2.feczine.cn/2022/11/21/637ad1517acf3.jpg) 如图 2.14(b) 所示,TCP/IP包有两个头部,分别是MAC头部和IP头部 这两个头部有不同的作用 首先,发送方将包的目的地,即服务器IP写入IP头部中,IP协议根据这一地址查找包的传输方向,从而找到下一个路由器的地址,并委托以太网协议将包传输过去。这时IP协议会查找下一个路由器的MAC地址,并将这个地址写入MAC头部,这样一来,以太网协议就知道要将这个包发到哪个路由器上了 集线器是根据以太网协议工作的设备。为了判断包接下来应该向哪里传输,集线器内有一张表(用于以太网协议的表)可以根据以太网头部中记录目的信息查出相应的传输方向 当存在多个集线器时,网络包会按顺序逐一通过这些集线器进行传输 包会到达下一个路由器,路由器中有一张用于IP协议的表,可根据这张表和IP头部中记录的目的地信息查出接下来要发往那个路由器 为了将包发到下一个路由器,我们还需要查处下一个路由器的MAC地址,并覆盖记录到MAC头部中,可以理解为改写了MAC头部 注:收到包的时候MAC头部会被舍弃,而当再次发送的时候又会加上包含新的MAC地址的新MAC头部 ![web_p99.jpg](https://cdn2.feczine.cn/2022/11/23/637d8c8844eee.jpg) 上文讲了IP和以太网的分工,其中以太网的部分也可以替换成其他的东西,例如无线局域网、ASDL、FTTH等,它们都可以替代以太网的觉得帮助IP协议来传输网络包 因此,将IP和负责传输的网络分开,可以更好地根据需要使用各种通信技术。像互联网这样庞大复杂的网络,在架构上需要保证灵活性 注:使用除以太网之外的其他网络进行传输时,MAC头部也会被替换为适合所选通信规格的其他头部 #### 包收发操作概览 实际上将包从发送方传输到接收方的工作是由集线器、路由器等网络设备完成的,因此IP模块仅仅是整个包传输过程的入口而已 IP模块的工作 包收发操作的起点是TCP模块委托IP模块发送包的操作,这个委托的过程就是TCP模块在数据块前面加上TCP头部,然后传递给IP模块。与此同时,TCP模块还需要制定通信对象的IP地址 收到委托后,IP模块会将包的内容当做一整块数据,在前面加上包含块控制信息的头部 IP模块会添加IP头部和MAC头部这两种头部 IP头部中包含IP协议规定的、根据IP地址将包发往目的地所需的控制信息 MAC头部包含通过以太网的局域网将包传输至最近的路由器所需的控制信息 加上这两个头部之后,包就封装好了,这就是IP模块负责的工作 注:凡是局域网所使用的头部豆角MAC头部,但其内容根据局域网的类型有所不同 封装好的包会被交给网络硬件,后文将它们统称为“网卡” 传递给网卡的网络包是由一连串的 0 和 1 组成的数字信息,网卡会将这些数字信息转换为电信号或光信号,并通过网线(或光纤)发送初期,然后这些信号就会到达集线器、路由器等转发设备,再由转发设备一步一步地送达接收方 接受过程和发送过程是相反的,信息先以电信号的形式传进来,然后网卡将其转换为数字信息并传递给IP模块。接下来,IP模块会将MAC头部和IP头部后面的内容,也就是TCP头部加上数据块,传递给TCP模块 因为IP模块并不关心数据的内容,也不关心TCP阶段的操作 因此,接下来的关于IP的工作方式,可以使用于任何TCP委派的收发数据 #### 生成包含接收方IP地址的IP头部 ![web_p103.jpg](https://cdn2.feczine.cn/2022/11/23/637d9217255d1.jpg) IP模块会生成IP头部附加在TCP头部前面,其中最重要的内容就是IP地址,这个地址是TCP模块告知的 IP不会自行判断包的目的地,而是将包发往应用程序指定的接收方,即便有应用程序指定了错误的IP地址,IP模块也只能照做 IP头部中还需要填写发送方的IP地址,可以认为是发送方计算机的IP地址,但这种说法并不准确 更准确的说是发送这个包的网卡的IP地址 在填写发送方IP地址时就需要判断到底应该填写哪个地址,这个判断相当于在多块网卡中判断应该使用哪一块网卡来发送这个包,也就相当于判断应该把包发往哪个路由器 因此只要确定了目标路由器,也就确定了应该使用哪块网卡,也就确定了发送方的IP地址 ![web_p105.jpg](https://cdn2.feczine.cn/2022/11/23/637d9216bb0fd.jpg) 上文提到路由器中有一张IP协议的表,这张表叫做 `路由表` 首先,对套接字中记录目的地IP地址与路由器左侧的 Network Destination 栏进行比较 例如,目标地址为 192.168.1.21,那么就对应第6行,因为它和 192.168.1 的部分匹配 目标地址为 10.10.1.166,那么就和 10.10.1 的部分匹配,所以对应第3行 我们只需要找到与IP地址左边部分相匹配的条目,找到相应的条目之后,接下来看从右数第2列和第3列的内容 右起第 2 列 Interface ,表示网卡等网络接口, 右起第 3 列 Gateway ,表示下一个路由器的地址,将包发送给这个IP地址,该地址对应的路由器就会将包转发到目标地址 注:如果Gateway和Interface列的IP地址相同,就表示不需要路由器进行转发,可以直接将包发给接收方的IP地址 路由表的第一行中,目标地址和子网掩码都是 `0.0.0.0`,这表示默认网关,如果其它所有条目都无法匹配,就会自动匹配这一行 注:子网掩码 用来判断IP地址中网络号与主机号分界线的值 这样我们就可以判断用哪块网卡来发包了,然后在IP头部的发送方IP地址中填上这块网卡对应的IP地址 接下来还需要填写协议号,它表示包的内容是来自哪个模块的 如果是TCP模块委托的内容,则设置为 06 (十六进制),如果是UDP模块委托的内容,则设置为 17 (十六进制) 浏览器中,HTTP请求消息都是通过TCP来传输的,因此这里就会填写表示TCP的 06 (十六进制) #### 生成以太网用的MAC头部 IP模块还需要在IP头部前面加上MAC头部 以太网在判断网络包目的地时和TCP/IP的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而MAC头部就是干这个用的 MAC头部包含了接收方和发送方的MAC地址等信息 ![web_p107.jpg](https://cdn2.feczine.cn/2022/11/27/63837441af9f3.jpg) MAC头部的开头是接收方和发送方的MAC地址,可以认为它们和IP头部中的接收方和发送方IP地址的功能差不多,只不过IP地址的长度为 32位,而MAC地址的长度为 48位 此外,IP地址是层次化结构,而MAC地址的 48位 可看做一个整体 但从表示接收方和发送方的意义上来说,它们之间是没有区别的 ![web_p110.jpg](https://cdn2.feczine.cn/2022/11/28/6384768568177.jpg) 在IP中,协议号表示IP头部后面的包内容的类型 而在以太网中,我们可以认为以太网类型后面就是以太网包的内容,而以太类型就表示后面内容的类型 以太网包的内容可以是IP、ARP等协议的包,他们都有对应的值,这也是根据规则来确定的 在生成MAC头部时,只要设置上表中的三个字段就可以了 `以太类型`,填写表示IP协议的值 `0800`(十六进制) 发送方MAC地址,填写网卡本身的MAC地址 接收方MAC地址,对方的MAC地址 网卡的MAC地址是出厂时写入ROM的,只需要经这个值读出来写入即可 注:实际上只有在操作系统启动过程中对网卡进行初始化时才会读取MAC地址,读取出来之后会存放在内存中 读取MAC地址的操作是由网卡驱动程序来完成的,因此网卡驱动也可以不从网卡ROM中读取地址,而是将配置文件中设定的MAC地址拿出来放到内存中并用于设定MAC头部,或者也可以通过命令输入MAC地址 #### 通过ARP查询目标路由器的MAC地址 在以太网中,有一种叫做广播的方法(前文提过),可以将包发给连接在同一以太网中的所有设备 ARP就是利用广播对所有设备提问:“XX这个IP地址是谁的”,然后就会有人回答:“XX这个IP是我的,我的MAC地址是XXXX” 如果对方和自己处在同一子网中,那么通过上面的操作就可以得到对方的MAC地址 然后,我们将这个MAC地址写入MAC头部,MAC头部就完成了 注:不是这个IP地址的设备会忽略广播,什么都不回答 如果路由表设置的正确,双方应该在同一子网,否则对方无法作出ARP响应,这是只能认为对方不存在,包的发送操作就会失败 如果每次发包都要这样查询一次,网络中就会增加很多ARP包,因此我们会将查询结果放到一块叫作ARP缓存的内存空间中 在发送包的时候,先查询一下ARP缓存没如果其中已经保存了对方的MAC地址,就不需要发送ARP查询,直接使用ARP缓存中的地址,而当ARP缓存中不存在时,发送ARP查询 ARP缓存中的值会在几分钟左右被删除,这个策略能够在几分钟后消除缓存和现实的差异,但IP地址刚刚发生改变的时候,ARP缓存中依然会保留老的地址,这时就会发生通信异常 #### 以太网的基本知识 以太网是一种为多台计算机能够彼此自由和廉价的相互通信而设计的通信技术 ![web_p112.jpg](https://cdn2.feczine.cn/2022/11/28/638479899733e.jpg) 这种网络的本质其实就是一根网线,如图(a)所示,图上还有种叫做收发器的小设备,它的功能只是将不同网线之间的信号连接起来而已 因此,当一台计算机发送信号时,信号就会通过网线流过整个网络,最终到达所有的设备 为了控制这一操作,就需要MAC头部,通过头部中接收方的MAc地址,就能够知道包是发送给谁的通过发送方MAC地址,就能知道包是谁发出的 注:多台设备同时发送信号会造成碰撞,当然也有相应的解决方案 随着交换机的普及,信号已经不会发生碰撞了 这个原型后来变成了图(b)中的结构 这个结构是将主干网线替换成了一个中继式集线器(集线器),收发器网线替换成了双绞线 虽然网络的结构有所变化,但信号会发送给所有设备这一基本性质并没有改变 后来,图(c)这样的使用交换式集线器(交换机)的结构普及开来,现在我们说的以太网指的都是这样的结构 这个结构看上去和(b)很像,但其实里面有一个重要的变化,即信号会发送给所有设备 这一性质变了,现在信号只会流到根据MAC地址指定的设备,而不会到达其它设备了 根据MAC地址传输包这一点并没有变,因此MAC头部的设计也得以保留 尽管以太网经历了多次变迁,但其基本的3个性质至今仍未改变,即 1.将包发送到MAC头部的接收方MAC地址代表的目的地 2.用发送方MAC地址识别发送方 3.用以太类型识别包的内容 因此可以认为具备这三个性质的网络就是以太网 以太网和IP一样,并不关心网络包的实际内容,因此以太网的收发操作也和TCP的工作阶段无关,都是共同的 #### 将IP包转换成电信号或光信号发送出去 IP生成的网络包只是存放在内存中的一串数字信息,没办法直接发送给对方 我们需要将数字信息转换为电信号或光信号,才能在网线或光纤上传输,这才是真正的数据发送过程 负责执行这一操作的是网卡,但网卡也无法单独工作,要控制网卡还需要网卡驱动程序 如下图所示,这是一张网卡主要构成要素与的概念图,并不代表硬件的实际结构 ![web_p115.jpg](https://cdn2.feczine.cn/2022/11/28/6384bc19116da.jpg) 网卡并不是通上电之后就可以马上开始工作的,而是和其他硬件一样都需要进行初始化 也就是打开计算机启动操作系统的时候,网卡驱动程序会对硬件进行初始化操作,然后硬件才进入可以使用的状态,操作包括硬件错误检查、初始设置等步骤,这些步骤对于很多其他硬件也是共通的 但也有一些操作是以太网特有的,那就是在控制以太网收发操作的MAC模块中设置MAC地址 注:MAC Media Access Control 网卡的ROM中保存着全世界唯一的MAC地址,将这个值读出之后就可以对MAC模块进行设置 旁注:之前在某个群里见过两块MAC地址相同的网卡,运气也确实不错 也有一些特殊的方法,比如从命令或配置文件中读取MAC地址并分给MAC模块,这种情况下,网卡会忽略ROM中的MAC地址 注:设置MAC地址时,必须注意不能喝网络中其它设备的MAC地址重复,否则网络将无法正常工作 #### 给网络包再加 3 个控制数据 网卡驱动从IP模块获取包之后,会将其复制到网卡内的缓冲区中,然后向MAC模块发送发送包的命令 首先,MAC模块会将包从缓冲区中取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的FCS(帧校验序列) 注:IEEE处于历史原因使用了“帧”而不是“包”,因此在以太网数据中都说是“帧”,可以认为包和帧是一回事 ![web_p117_1.jpg](https://cdn2.feczine.cn/2022/11/29/6385ae24d799e.jpg) 报头是一串像 `10101010...` 这样1和0交替出现的比特序列,长度为 `56` 位 它的作用是确定读取包的时机 当这些 1010 的比特序列被转换为电信号后,会形成下图这样的波形 接收方在收到信号时,遇到这样的波形就可以判断读取数据的时机 ![web_p117_2.jpg](https://cdn2.feczine.cn/2022/11/29/6385ae275470e.jpg) 用电信号来表达数字信息时,我们需要让 0 和 1 两种比特分别对应特定的电压和电流 通过电信号来读取数据的过程就是将这种对应关系颠倒过来 也就是说,通过测量信号中的电压和电流变化,还原出 0 和 1 两种比特的值 实际的信号并不像下图那样有分割每个比特的辅助线,因此在测量电压和电流是必须先判断出每个比特的界限在哪里 但是像下图(a)右边这种连续的信号,由于电压和电流没有变化,我们就没办法判断出其中每个比特到底该从哪里去切分 ![web_p118.jpg](https://cdn2.feczine.cn/2022/11/29/6385aebbcfa4d.jpg) 要解决这个问题,最简单的方法就是在数据信号之外再发送一组用来区分比特间隔的`时钟信号` 如上图(b)所示,当时钟信号从下往上变化时,读取电压和电流的值,然后和 0 或 1 进行对应就可以了 但这种方法也存在问题,当距离较远,网线较长时,两条线路的长度会发生差异,数据信号和时钟信号的传输会产生时间差,时钟就会发生偏移 要解决这个问题,可以采用将数据信号和时钟信号叠加在一起的方法,如上图(c)所示 由于时钟信号是像上图(b)那样按固定频率进行变化的,只要能够找到这个变化的周期,就可以从接收到的信号(c)中提取出时钟信号(b),进而计算出数据信号(a),这和发送方将数据信号和时钟信号叠加的过程正好相反 然后,只要根据时钟信号(b)的变化周期,我们就可以从数据信号(a)中读取相应的电压和电流值,并将其还原为 0 或 1 的比特 重点在于如何判断时钟信号的变化周期,时钟信号是以 10Mbit/s 或 100 Mbit/s 这种固定频率进行变化的,只要对信号进行一段时间的观察,就可以找到其变化的周期 因此我们不能一开始就发送包的数据,而是要在前面加上一段用来测量时钟信号的特殊信号,这就是报头的作用 注:如果在包信号结束之后,继续传输时钟信号,就可以保持时钟同步的状态,下一个包就无需重新进行同步。有些通信方式采用了这样的设计,但以太网的包结束之后时钟信号也跟着结束了,没有通过这种方式来保持时钟同步,因此需要在每个包的前面加上报头,用来进行时钟同步 以太网根据速率和网线类型的不同分为许多派生方式,每种方式的信号形态也有差异,并不都是像本例中讲的这样,单纯通过电压和电流来表达 0 和 1 的。 因此,101010……这样的报头数字信息在转换成电信号后,其波形也不一定都是图2.25的样子,而是根据方式的不同而不同,但报头的作用和基本思路是一致的 起始帧分界符的末尾比特排列有少许变化,接收方以这一变化作为标记,从这里开始提取网络包数据 末尾的帧校验序列(FCS)用来检查包传输过程中因噪声导致的波形紊乱、数据错误 它是一串 32 位的序列,是通过一个公式对包中从头到尾的所有内容进行计算而得出来的 它和磁盘等设备中使用的CRC错误校验码是同一种东西,当原始数据中某一个比特发生变化时,计算出来的结果就会发生变化 接收方计算出的FCS和发送方计算出的FCS就会不同,这样我们就可以判断出数据有没有错误 #### 向集线器发送网络包 发送信号的操作分为两种,一种是使用集线器的半双工模式,另一种是使用交换机的全双工模式 注:发送和接收同时并行的方式叫做全双工;发送和接收某一时刻只能执行一个的方式叫做半双工 在半双工模式中,为了避免信号碰撞,首先要判断网线中是否存在其他设备发送的信号,如果有,则需等待该信号传输完毕 首先,MAC模块从报头开始将数字信息按每个比特位转换成电信号,然后由叫做 `MHU(PHY)` 的信号收发模块发送出去 将数字信息转换为电信号的速率就是网络的传输速率,例如每秒将 10Mbit 的数字信息转换为电信号发送出去,则速率就是 10Mbit/s 接下来,MAU模块会将信号转换为可在网线上传输的格式,并通过网线发送出去 以太网规格中对不同的网线类型和速率以及其对应的信号格式进行了规定,但MAC模块并不关心这些区别,而是将可转换为任意格式的通用信号发送给MAU模块,然后MAU模块再将其转换为可在网线上传输的格式 可以认为MAU模块的功能就是对MAC模块产生的信号进行格式转换 网线中实际传输的信号就是这个样子的 下图是一个梨子 ![web_p121.jpg](https://cdn2.feczine.cn/2022/11/29/6385c45b764b6.jpg) MAU的职责并不是仅仅将MAC模块传递进来的信号通过网线发送出去,它还需要监控接收线路中有没有信号进来 以太网不会确认发送的信号对方有没有收到。根据以太网的规格,两台设备之间的网线不能超过100米,采用光纤则可以更长,在这个距离内极少会发生错误,万一发生错误,协议栈的TCP协议也会负责搞定 在半双工模式中,如果在发送信号的同时有其他信号进来,两组信号就会相互叠加,无法彼此区分出来,这就是所谓的信号碰撞 这种情况下,继续发送信号是没有意义的,因此发送操作会终止 为了通知其它设备当前线路已发生碰撞,还会发送一段时间的阻塞信号,然后所有的发送操作会全部停止。为了通知其他设备当前线路已发生碰撞,还会发送一段时间的`阻塞信号`,然后所有的发送操作会全部停止 等待一段时间之后,网络中的设备会尝试重新发送信号 但如果所有设备的等待时间都相同,那肯定还会发生碰撞,因此必须让等待时间互相错开 具体来说,等待时间是根据MAC地址生成一个随机数计算出来的 当网络拥塞时,发生碰撞的可能性就会提高没重新发送的时候可能又会和另一台设备的发送操作冲突,这时会将等待时间延长一倍,最多重试 10 次,如果还是不行就报告通信错误 全双工模式会在第三章介绍,在全双工模式中,发送和接收可以同时进行,不会发生碰撞 #### 接收返回包 在使用集线器的半双工模式以太网中,一台设备发送的信号会到达连接在集线器上的所有设备 这意味着无论是不是发给自己的信号都会通过接收线路传进来,因此接受操作的第一步就是把这些信号全部接收进来 信号的开头是报头,通过报头的波形同步时钟,然后遇到起始帧分界符时开始将后面的信息转换成数字信息 这个操作和发送时是相反的,即 MAU模块会将信号转换成通用格式并发送给MAC模块,MAC模块再从头开始将信号转换为数字信息,并存放到缓冲区中 当到达信号的末尾时,还需要检查FCS,具体地说就是将包开头到结尾的比特位套用到公式中,计算出FCS,然后和包末尾的FCS进行对比,正常情况下两者应该是一致的,如果中途收到噪声感染而导致波形发生紊乱,则两者的值会产生差异,这时这个包会被当做错误包而被丢弃 如果FCS校验没问题,接下来就要比对MAC头部中接收方的MAC地址和网卡初始化过程中分配的MAC地址是否一致,如果不一致则直接丢弃;如果一致,则将包放入缓冲区中 接下来网卡会通知计算机收到了一个包 注:有一种特殊的模式 “混杂模式” 可以让网卡不检查包的接收方地址 通知计算机的操作会使用一个叫作 `中断` 的机制 在网咖执行接受报的操作过程中,计算机并不是一直监控着网卡的活动,而是去继续执行其他的任务 因此,如果网卡不通知计算机,计算机是不知道包已经收到了这件事的 网卡驱动也是在计算机中运行的一个程序,因此它也不知道包到达的状态 在这种情况下,我们需要一种机制能够打断计算机正在执行的任务,让计算机注意到网卡中发生的事情,这种机制就是 `中断` 中断的工作过程是这样的 首先,网卡想拓展总线中的中断信号线发送信号,该信号线通过计算机中的中断控制器连接到CPU 当产生中断信号时,CPU会暂时挂起正在处理的任务,切换到操作系统中的中断处理程序 然后,中断处理程序会调用网卡驱动,控制网卡执行相应的接收操作 中断是有编号的,网卡在安装的时候就砸硬件中设置了 `中断号`,在中断处理程序中则是将硬件的中断号和相应的驱动程序绑定 现在的硬件设备都遵循即插即用规范自动设置中断号,我们没有必要去关心中断号了 注:即插即用规范 PnP(Plug and Play),是一种自动对拓展卡和周边设备进行配置的功能 网卡驱动被中断处理程序调用后,会从网卡的缓冲区中取出收到的包,并通过MAC头部中的以太类型字段判断协议的类型 除了TCP/IP外还有很多协议,这些协议都被分配了不同的以太协议,网卡会把这样的包交给对应的协议栈,前提是操作系统内部存在以太类型所对应的协议栈,如不存在,则会视为错误,直接丢弃这个包 #### 将服务器的响应包从 IP 传递给 TCP 假设Web服务器返回一个网络包,服务器返回的包的类型应该是0800 (TCP),因此网卡驱动会将其交给TCP/IP协议栈处理 接下来就轮到IP模块,第一步是检查IP头部,确认格式是否正确 如果没有问题,下一步就是查看接收方IP地址,服务器返回的包的接收方IP地址应该与客户端网卡的地址一致,检查确认后我们就可以接收这个包了 如果接收方IP地址不是自己的地址,那一定是发生了什么错误 客户端计算机不负责对包进行转发,因此不应该收到不是发给自己的包 注:服务器的操作系统具备与路由器相同的包转发功能,当打开这一功能时,就可以像路由器一样对包进行转发 当发生这样的错误时,IP模块会通过ICMP消息将错误告知发送方 ![web_p126.jpg](https://cdn2.feczine.cn/2022/11/29/6385de6e6dcaf.jpg) 当我们遇到这个错误时,IP模块会通过上表中的 `Destination unreachable` 消息通知对方 从这张表的内容中我们可以看到在包的接收和转发过程中能够遇到的各种错误 如果接收方的IP地址正确,则这个包会被接收下来,这时还需完成另一项工作 IP协议有一个叫作 `分片` 的功能 简单来说,网线和局域网中只能传输小包,因此需要将大的包却分成多个小包。如果接收到的包是经过分片的,那么IP模块会将它们还原成原始的包 分片的包会在IP头部的标志字段中进行标记,当收到分片的包时,IP模块会将其暂存在内部的内存空间中,然后等待IP头部中具有相同ID的包全部到达,这是因为同一个包的所有分片都具有相同的ID 此外,IP头部还有一个 `分片偏移量` 字段,它到达后,表示当前分片在整个包中所处的位置 根据这些信息,在所有分片全部收到之后,就可以将它们还原成原始的包,这个操作叫做 `分片重组` 接下来包会被交给TCP模块,TCP模块会根据IP头部中的接收方和发送方IP地址,一级TCP头部中的接收方和发送方端口来查找对应的套接字 找到对应的套接字之后,就可以根据套接字中记录的通信状态,执行相应的操作 注:因为IP头部是IP模块负责的,TCP模块去查询它等于是越权了 然而,如果根据这种阉割的划分来开发程序的话,IP模块和TCP模块之间的交互过程必然会产生成本,而IP模块和TCP模块进行类似交互的场景其实非常多,总体的交互成本就会很高,程序的运行效率就会下降。因此就像之前提到的一样,不妨将责任范围划分得宽松一点,将TCP和IP作为一个整体来看待,这样可以带来更大的灵活性 ### UDP 协议的收发操作 #### 不需要重发的数据用 UDP 重发更高效 有些程序不使用TCP协议,而是使用UDP协议来收发数据,像DNS服务器查询IP地址 TCP为什么设计的如此复杂呢? 因为我们需要将数据高效且可靠地发送给对方 为了实现可靠性,我们就需要确认对方是否收到了我们发送的数据,如果没有还需要重发一遍 实现上面的要求,最简单的方法是数据全部发送完毕之后让接收方返回一个接收确认,这样一来,如果没收到直接全部发送一遍就好了 但是,如果漏掉了一个包就要全部重发一遍,怎么看都很低效 为了实现高效的传输,我们要避免重发已送达的包,而只是重发那些出错的或未送达的包 TCP之所以复杂,是为了实现这一点 不过,在某种情况下,即便没有TCP这样复杂的机制,我们也能够高效的重发数据 这种情况就是数据很短,用一个包就能装得下 如果只有一个包,就不用考虑哪个包未送达了,因为全部重发也只是重发一个包而已,这种情况下我们就不需要TCP这样复杂的机制 而且,如果不使用TCP,也不需要发送那些用来建立和断开连接的控制包 此外,我们发送了数据,对方一般都会给出回复,只要将回复的数据当做接收确认就行了,也就不需要专门的接收确认包了 #### 控制用的短数据 UDP没有TCP的接收确认、窗口等机制,因此在收发数据之前也不需要交换控制信息,也就是说不需要建立和断开连接的步骤,只要在从应用程序获取的数据前面加上UDP头部,然后交给IP模块发送就可以了 接收也很简单,只要根据IP头部中的接收方和发送方IP地址,以及UDP头部中的接收方和发送方端口号,找到相应的套接字并将数据交给相应的应用程序就可以了 ![web_p130.jpg](https://cdn2.feczine.cn/2022/11/29/6385e6e1a9e86.jpg) 除此之外,UDP协议没有其他功能了,遇到错误或丢包也一概不管 因为UDP只是负责发送包而已,并不像TCP协议一样会对包的送达进行监控,所以协议栈也不知道有没有发生错误,因此出错时就收不到来自对方的回复,应用程序会注意到这个问题,并重新发送一遍数据 UDP可发送的数据的最大长度为IP包的最大长度减去IP头部和UDP头部的长度 不过这个长度与MTU、MSS不是一个层面上的概念,MTU和MSS是基于以太网和通信线路上网络包的最大长度来计算的,而IP包的最大长度是由IP头部中的“全场”字段决定的 全长字段的长度为16位,因此从IP协议规范来看,IP包的最大长度为65535字节,再减去IP头部和UDP头部的长度,就是UDP协议所能发送的数据最大长度 如果不考虑可选字段的话,一般来说IP头部为20字节,UDP头部为8字节,因此UDP协议的最大数据长度为65507字节 当然这个数据长度已经超过了以太网和通信线路的最大传输长度,因此需要让IP模块使用分片功能后再传输 #### 音频和视频数据 还有一个场景会使用UDP,就是发送音频和视频数据的时候 音频和视频数据必须在规定的时间内送达,一旦送达晚了,就会错过播放时机,导致声音和图像卡顿 如果像TCP一样通过接收确认响应来检查错误并重发,重发的过程需要耗费一定时间,因此重发的数据很可能已经错过了播放的时机 当然,我们可以用高速线路让重发的数据能够在规定的时间内送达,但这样需要增加几倍的带宽才行 注:UDP经常会被防火墙阻止,因此当需要穿越防火墙传输音频和视频数据时,尽管需要消耗额外的带宽,但有时也只能使用TCP 此外,音频和视频数据中缺少了某些包并不会产生严重的问题,只是会产生一些失真或者卡顿而已,一般都是可接受的 在这些无需重发,或是重发了也没什么意义的情况下,使用UDP发送数据的效率会更高 ### 其他 施乐(Xerox)公司过去在研究未来的计算机架构,计算机和以太网只是其中一个环节的产物 ## 第三章 从网络到网线设备 ### 信号在网线和集线器中传输 #### 每个包都是独立传输的 转发设备在进行转发时不看数据的内容。因此,无论包里面装的是应用程序的数据或者是 TCP 协议的控制信息 A,都不会对包的传输操作本身产生影响。换句话说,HTTP请求的方法,TCP 的确认响应和序号,客户端和服务器之间的关系,这一切都与包的传输无关。因此,所有的包在传输到目的地的过程中都是独立的,相互之间没有任何关联。 ![3.1.png](https://cdn2.feczine.cn/2023/03/25/641e9e440ae43.png) #### 防止网线中的信号衰减很重要 网卡中的 PHY(MAU)A 模块负责将包转换成电信号,信号通过 RJ-45 接口进入双绞线。 以太网信号的本质是正负变化的电压,大家可以认为网卡的 PHY(MAU)模块就是一个从正负两个信号端子输出信号的电路。 但是, 信号到达集线器的时候并不是跟刚发送出去的时候一模一样。集线器收到的信号有时会出现衰减。 信号在网线的传输过程中,能量会逐渐损失。网线越长,信号衰减就越严重。 而且,信号损失能量并非只是变弱而已。以太网中的信号波形是方形的,但损失能量会让 信号的拐角变圆,这是因为电信号的频率越高,能量的损失率越大。信号的拐角意味着电压发生剧烈的变化,而剧烈的变化意味着这个部分的信号频率很高。高频信号更容易损失能量,因此本来剧烈变化的部分就会变成缓慢的变化,拐角也就变圆了。 即便线路条件很好, 没有噪声, 信号在传输过程中依然会发生失真,如果再加上噪声的影响,失真就会更厉害。噪声根据强度和类型会产生不同的影响, 无法一概而论, 但如果本来就已经衰减的信号再进一步失真,就会出现对 0 和 1 的误判,这就是产生通信错误的原因。 ![3.3.png](https://cdn2.feczine.cn/2023/03/25/641e9f5a3eae2.png) #### “双绞”是为了抑制噪声 局域网网线使用的是双绞线,其中“双绞”的意思就是以两根信号线为一组缠绕在一起,这种拧麻花一样的设计是为了抑制噪声的影响。 ![3.4.png](https://cdn2.feczine.cn/2023/03/25/641e9f5a3656f.png) 通过将信号线缠绕在一起的方式,噪声得到了抑制,从结果来看提升了网线的性能,除此之外还有其他一些工艺也能够帮助提升性能。例如在信号线之间加入隔板保持距离, 以及在外面包裹可阻挡电磁波的金属屏蔽网等。 #### 集线器将信号发往所有线路 ![3.2.png](https://cdn2.feczine.cn/2023/03/25/641ea0150277a.png) 当信号到达集线器后,会被广播到整个网络中。以太网的基本架构是将包发到所有的设备,然后由设备根据接收方 MAC 地址来判断应该接收哪些包,而集线器就是这一架构的忠实体现,它就是负责按照以太网的基本架构将信号广播出去。 集线器的内部结构如图 3.2 左侧部分所示。 首先, 在每个接口的后面装有和网卡中的 PHY(MAU)功能相同的模块,但如果它们像网卡端一样采用直连式接线,是无法正常接收信号的。要正常接收信号,必须将“发送线路” 和“接收线路” 连接起来才行。 在图 3.2 中, 集线器中的 PHY (MAU)模块与接口之间采用交叉接线的原因正是在于此。 MDI 就是对 RJ-45 接口和信号收发模块进行直连接线,而 MDI-X 则是交叉接线。由于集线器的接口一般都是 MDI-X 模式,要将两台集线器相连时, 就需要将其中一台改成 MDI 模式。 如果集线器上没有 MDI 切换开关,而且所有的接口又都是 MDI-X 时,可以用交叉网线连接两台集线器。所谓交叉网线,就是一种将发送和接收信号线反过来接的网线。 网卡不仅可以连接集线器,因为网卡的 PHY(MAU)模块和集线器都是一样的,所以两台计算机的网卡也可以相互连接,只要将一侧的发送信号线和另一侧的接收信号线连起来就可以收发数据了。 信号到达集线器的 PHY(MAU)模块后,会进入中继电路。中继电路的基本功能就是将输入的信号广播到集线器的所有端口上。当然,也有一些产品具有信号整形、错误抑制等功能,但基本上就是将输入的信号原封不动地输出到网线接口。 接下来,信号从所有接口流出,到达连接在集线器上的所有设备。 由于集线器只是原封不动地将信号广播出去,所以即便信号受到噪声的干扰发生了失真,也会原样发送到目的地。这时,接收信号的设备,也就是交换机、路由器、服务器等,会在将信号转换成数字信息后通过 FCSA 校验发现错误,并将出错的包丢弃。当然,丢弃包并不会影响数据的传输,因为丢弃的包不会触发确认响应。因此协议栈的 TCP 模块会检测到丢包,并对该包进行重传。 ### 交换机的包转发操作 #### 交换机根据地址表进行转发 交换机的设计是将网络包原样转发到目的地,图 3.7 就是它的内部结构。 ![3.7.png](https://cdn2.feczine.cn/2023/03/25/641ea1e4de4eb.png) 首先,信号到达网线接口,并由 PHY(MAU)模块进行接收,这一部分和集线器是相同的。 也就是说, 它的接口和 PHY(MAU)模块也是以MDI-X 模式进行连接的,当信号从双绞线传入时,就会进入 PHY(MAU)模块的接收部分。 接下来,PHY(MAU)模块会将网线中的信号转换为通用格式, 然后传递给 MAC 模块。MAC 模块将信号转换为数字信息,然后通过包末尾的 FCS 校验错误,如果没有问题则存放到缓冲区中。这部分操作和网卡基本相同,大家可以认为交换机的每个网线接口后面都是一块网卡。网线接口和后面的电路部分加在一起称为一个端口,也就是说交换机的一个端口就相当于计算机上的一块网卡。但交换机的工作方式和网卡有一点不同。 交换机的端口不核对接收方 MAC 地址,而是直接接收所有的包并存放到缓冲区中。因此,和网卡不同,交换机的端口不具有 MAC 地址。 以图 3.7 中的地址表为例,MAC 地址和端口是一一对应的,通过这张表就能够判断出收到的包应该转发到哪个端口。 ![3.8.png](https://cdn2.feczine.cn/2023/03/25/641ea465094bd.png) 交换电路的结构如图 3.8 所示,它可以将输入端和输出端连接起来。其中,信号线排列成网格状,每一个交叉点都有一个交换开关,交换开关是电子控制的,通过切换开关的状态就可以改变信号的流向。交换电路的输入端和输出端分别连接各个接收端口和发送端口,网络包通过这个网格状的电路在端口之间流动。每个交叉点上的交换开关都可以独立工作,因此只要路径不重复,就可以同时传输多路信号。 当网络包通过交换电路到达发送端口时,端口中的 MAC 模块和 PHY(MAU)模块会执行发送操作,将信号发送到网线中,这部分和网卡发送信号的过程是一样的。 #### MAC地址表的维护 交换机在转发包的过程中, 还需要对 MAC 地址表的内容进行维护,维护操作分为两种: - 第一种是收到包时,将发送方 MAC 地址以及其输入端口的号码写入 MAC 地址表中。 - 另一种是删除地址表中某条记录的操作,这是为了防止设备移动时产生问题。 比如,我们在开会时会把笔记本电脑从办公桌拿到会议室,这时设备就发生了移动。从交换机的角度来看,就是本来连接在某个端口上的笔记本电脑消失了。这时如果交换机收到了发往这台已经消失的笔记本电脑的包,那么它依然会将包转发到原来的端口,通信就会出错,因此必须想办法删除那些过时的记录。然而,交换机没办法知道这台笔记本电脑已经从原来的端口移走了。 因此地址表中的记录不能永久有效,而是要在一段时间不使用后就自动删除。 总之,交换机会自行更新或删除地址表中的记录,不需要手动维护。当地址表的内容出现异常时,只要重启一下交换机就可以重置地址表,也不需要手动进行维护。 #### 特殊操作 当交换机发现一个包要发回到原端口时,就会直接丢弃这个包。 还有另外一种特殊情况, 就是地址表中找不到指定的 MAC 地址。 这可能是因为具有该地址的设备还没有向交换机发送过包,或者这个设备一段时间没有工作导致地址被从地址表中删除了。这种情况下,交换机无法判断应该把包转发到哪个端口,只能将包转发到除了源端口之外的所有端口上,无论该设备连接在哪个端口上都能收到这个包。 此外,如果接收方 MAC 地址是一个广播地址,那么交换机会将包发送到除源端口之外的所有端口。 #### 全双工模式可以同时进行发送和接收 全双工模式是交换机特有的工作模式,它可以同时进行发送和接收操作,集线器不具备这样的特性。 使用集线器时,如果多台计算机同时发送信号,信号就会在集线器内部混杂在一起,进而无法使用,这种现象称为碰撞,是以太网的一个重要特征。不过,只要不用集线器,就不会发生碰撞。 如果不存在碰撞,也就不需要半双工模式中的碰撞处理机制了。于是,人们对以太网规范进行了修订,增加了一个无论网络中有没有信号都可以发送信号的工作模式, 同时规定在这一工作模式下停用碰撞检测 。 这种工作模式就是全双工模式。在全双工模式下,无需等待其他信号结束就可以发送信号,因此它比半双工模式速度要快。由于双方可以同时发送数据,所以可同时传输的数据量也更大,性能也就更高。 #### 自动协商:确定最优的传输速率 随着全双工模式的出现,如何在全双工和半双工模式之间进行切换的问题也产生了。在全双工模式刚刚出现的时候,还需要手动进行切换,但这样实在太麻烦,于是后来出现了自动切换工作模式的功能。此外,除了能自动切换工作模式之外,还能探测对方的传输速率并进行自动切换。这种自动切换的功能称为 自动协商。 在以太网中,当没有数据在传输时,网络中会填充一种被称为连接脉冲的脉冲信号。在没有数据信号时就填充连接脉冲,这使得网络中一直都有一定的信号流过,从而能够检测对方是否在正常工作,或者说网线有没有正常连接。以太网设备的网线接口周围有一个绿色的 LED 指示灯,它表示是否检测到正常的脉冲信号。如果绿灯亮,说明 PHY(MAU)模块以及网线连接正常。 在双绞线以太网规范最初制定的时候,只规定了按一定间隔发送脉冲信号,这种信号只能用来确认网络是否正常。后来,人们又设计出了如图 3.11 这样的具有特定排列的脉冲信号,通过这种信号可以将自身的状态告知对方。自动协商功能就利用了这样的脉冲信号,即通过这种信号将自己能够支持的工作模式 A 和传输速率相互告知对方,并从中选择一个最优的组合 B。 ![3.11.png](https://cdn2.feczine.cn/2023/03/25/641ea9d04e0a7.png) ### 路由器的包转发操作 #### 路由器的基本知识 路由器和交换机是有区别的。因为路由器是基于 IP 设计的,而交换机是基于以太网设计的。 ![3.12.png](https://cdn2.feczine.cn/2023/03/25/641eab6da5476.png) 首先,路由器的内部结构如图 3.12 所示。这张图画得非常简略。 其中转发模块负责判断包的转发目的地,端口模块负责包的收发操作。这一分工模式在第 2 章介绍计算机内部结构的时候也出现过,换句话说,路由器转发模块和端口模块的关系,就相当于协议栈的 IP 模块和网卡之间的关系。 因此,大家可以将路由器的转发模块想象成 IP 模块,将端口模块想象成网卡。 通过更换网卡,计算机不仅可以支持以太网,也可以支持无线局域网,路由器也是一样。 如果路由器的端口模块安装了支持无线局域网的硬件,就可以支持无线局域网了。 路由器的端口模块支持除局域网之外的多种通信技术,如 ADSL、FTTH,以及各种宽带专线等,只要端口模块安装了支持这些技术的硬件即可 。 路由器在转发包时,首先会通过端口将发过来的包接收进来,这一步的工作过程取决于端口对应的通信技术。 接下来,转发模块会根据接收到的包的 IP 头部中记录的接收方 IP 地址,在路由表中进行查询,以此判断转发目标。 然后,转发模块将包转移到转发目标对应的端口,端口再按照硬件的规则将包发送出去,也就是转发模块委托端口模块将包发送出去的意思。 刚才我们讲到端口模块会根据相应通信技术的规范来执行包收发的操作,这意味着*端口模块是以实际的发送方或者接收方的身份来收发网络包的。 * 以以太网端口为例,路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方。端口还具有 IP 地址,从这个意义上来说,它和计算机的网卡是一样的。当转发包时,首先路由器端口会接收发给自己的以太网包,然后查询转发目标,再由相应的端口作为发送方将以太网包发送出去。 这一点和交换机是不同的,交换机只是将进来的包转发出去而已,它自己并不会成为发送方或者接收方。 #### 路由表中的信息 在“查表判断转发目标”这一点上,路由器和交换机的大体思路是类似的,不过具体的工作过程有所不同。交换机是通过 MAC 头部中的接收方 MAC 地址来判断转发目标的, 而路由器则是根据 IP 头部中的 IP 地址来判断的。由于使用的地址不同,记录转发目标的表的内容也会不同。 路由器中的表叫作路由表,其中包含的信息如图 3.13 所示。 ![3.13.png](https://cdn2.feczine.cn/2023/03/25/641ead7789d79.png) 最左侧的目标地址列记录的是接收方的信息。 实际上这里的 IP 地址只包含表示子网的网络号部分的比特值,而表示主机号部分的比特值全部为 0。 路由器会将接收到的网络包的接收方 IP地址与路由表中的目标地址进行比较,并找到相应的记录。 交换机在地址表中只匹配完全一致的记录,而路由器则会忽略主机号部分,只匹配网络号部分。 在匹配地址的过程中,路由器需要知道网络号的比特数,因此路由表中还有一列子网掩码。子网掩码的含义和第 1 章的图 1.9(b)中介绍的子网掩码基本相同,通过这个值就可以判断出网络号的比特数。 **路由聚合** 刚才我们说过, 目标地址列中的 IP 地址表示的是子网, 但也有一些例外, 有时地址本身的子网掩码和路由表中的子网掩码是不一致的, 这是路由聚合的结果。 路由聚合会将几个子网合并成一个子网, 并在路由表中只产生一条记录。 要搞清楚这个问题 , 我们还是看一个例子。如图 3.14 所示,我们现在有 3 个子网,分别 `10.10.1.0/24`、`10.10.2.0/24`、`10.10.3.0/24` ,路由器 B 需要将包发往这 3 个子网。在这种情况下,路由器 B 的路由表中原本应该有对应这 3 个子网的 3条记录,但在这个例子中,无论发往任何一个子网,都是通过路由器 A 来进行转发,因此我们可以在路由表中将这 3 个子网合并成 `10.10.0.0/16`,这样也可以正确地进行转发,但我们减少了路由表中的记录数量,这就是路由聚合。经过路由聚合,多个子网会被合并成一个子网,子网掩码会发生变化,同时,目标地址列也会改成聚合后的地址。 ![3.14.png](https://cdn2.feczine.cn/2023/03/25/641eaf1b303f4.png) 相对地,还有另外一些情况,如将一个子网进行细分并注册在路由表中,然后拆分成多条记录从结果上看,路由表的子网掩码列只是用来在匹配目标地址时告诉路由器应该匹配多少个比特。而且,目标地址中的地址和实际子网的网络号可能并不完全相同,但即便如此,路由器依然可以正常工作。 *路由表的子网掩码列只表示在匹配网络包目标地址时需要对比的比特数量。* 在子网掩码的右边还有网关和接口两列,它们表示网络包的转发目标。根据目标地址和子网掩码匹配到某条记录后,路由器就会将网络包交给接口列中指定的网络接口(即端口),并转发到网关列中指定的 IP 地址最后一列是跃点计数,它表示距离目标 IP 地址的距离是远还是近。这个数字越小,表示距离目的地越近;数字越大,表示距离目的地越远。 路由表记录维护的方式和交换机也有所不同。交换机中对 MAC 地址表的维护是包转发操作中的一个步骤,而路由器中对路由表的维护是与包转发操作相互独立的,也就是说,在转发包的过程中不需要对路由表的内容进行维护。 #### 路由器的包接收操作 首先,路由器会接收网络包。路由器的端口有各种不同的类型,这里我们只介绍以太网端口是如何接收包的。以太网端口的结构和计算机的网卡基本相同,接收包并存放到缓冲区中的过程也和网卡几乎没有区别。 首先, 信号到达网线接口部分, 其中的 PHY(MAU)模块和 MAC 模块将信号转换为数字信息,然后通过包末尾的 FCS 进行错误校验,如果没问题则检查 MAC 头部中的接收方 MAC 地址,看看是不是发给自己的包,如果是就放到接收缓冲区中, 否则就丢弃这个包。 #### 查询路由表确定输出端口 完成包接收操作之后,路由器就会丢弃包开头的 MAC 头部。MAC 头部的作用就是将包送达路由器,其中的接收方 MAC 地址就是路由器端口的 MAC 地址。 因此, 当包到达路由器之后,MAC 头部的任务就完成了,于是 MAC 头部就会被丢弃。 *通过路由器转发的网络包,其接收方 MAC 地址为路由器端口的 MAC 地址。* 接下来,路由器会根据 MAC 头部后方的 IP 头部中的内容进行包的转发操作。转发操作分为几个阶段,首先是查询路由表判断转发目标。 判断转发目标的第一步,就是根据包的接收方 IP 地址查询路由表中的目标地址栏,以找到相匹配的记录。就像前面讲过的一样,这个匹配并不是匹配全部 32 个比特,而是根据子网掩码列中的值判断网络号的比特数,并匹配相应数量的比特。 例如,子网掩码列为 255.255.255.0,就表示需要匹配从左起 24 个比特。网络包的接收方 IP 地址和路由表中的目标地址左起 24 个比特的内容都是 192.168.1,因此两者是匹配的,该行记录就是候选转发目标之一。 按照这样的规则, 我们可能会匹配到多条候选记录。 在这个例子中,第 3、4、5 行都可以匹配 B。其中,路由器首先寻找网络号比特数最长的一 条记录。网络号比特数越长,说明主机号比特数越短,也就意味着该子网内可分配的主机数量越少,即子网中可能存在的主机数量越少,这一规则的目的是尽量缩小范围,所以根据这条记录判断的转发目标就会更加准确。 然而,有时候路由表中会存在网络号长度相同的多条记录,例如考虑到路由器或网线的故障而设置的备用路由就属于这种情况。这时,需要根据跃点计数的值来进行判断。跃点计数越小说明该路由越近,因此应选择跃点计数较小的记录。 如果在路由表中无法找到匹配的记录,路由器会丢弃这个包,并通过 ICMP 消息告知发送方。 #### 找不到匹配路由时选择默认路由 既然如此, 那么是不是所有的转发目标都需要配置在路由表中才行呢?如果是公司或者家庭网络,这样的做法也没什么问题,但互联网中的转发目标可能超过 20 万个,如果全部要配置在路由表中实在是不太现实。 其实,大家不必担心,因为之前的图 3.13 路由表中的最后一行的作用就相当于把所有目标都配置好了。这一行的子网掩码为 0.0.0.0,关键就在这里,*子网掩码 0.0.0.0 的意思是网络包接收方 IP 地址和路由表目标地址的匹配中需要匹配的比特数为 0*,换句话说,就是根本不需要匹配。只要将子网掩码设置为 0.0.0.0,那么无论任何地址都能匹配到这一条记录,这样就不会发生不知道要转发到哪里的问题了。 > 由于匹配的比特数越长优先级越高(最长匹配原则),因此子网掩码为 0.0.0.0 的记录优先级是最低的,只有当找不到其他匹配的记录时,才会选择这条记录。 只要在这一条记录的网关列中填写接入互联网的路由器地址,当匹配不到其他路由时,网络包就会被转发到互联网接入路由器。因此这条记录被称为 `默认路由`, 这一行配置的网关地址被称为 `默认网关`。 #### 包的有效期 更新 IP 头部中的 TTL(Time to Live, 生存时间)字段。 TTL 字段表示包的有效期,包每经过一个路由器的转发,这个值就会减 1,当这个值变成 0 时,就表示超过了有效期,这个包就会被丢弃。 这个机制是为了防止包在一个地方陷入死循环。 发送方在发送包时会将 TTL 设为 64 或 128, 也就是说包经过这么多路由器后就会“寿终正寝”。 现在的互联网即便访问一台位于地球另一侧的服务器,最多也只需要经过几十个路由器,因此只要包被正确转发,就可以在过期之前到达目的地。 #### 通过分片功能拆分大网络包 路由器的端口并不只有以太网一种,也可以支持其他局域网或专线通信技术。不同的线路和局域网类型各自能传输的最大包长度也不同,因此输出端口的最大包长度可能会小于输入端口。即便两个端口的最大包长度相同, 也可能会因为添加了一些头部数据而导致包的实际长度发生变化,ADSL、FTTH 等宽带接入技术中使用的 PPPoEB 协议就属于这种情况。 无论哪种情况,一旦转发的包长度超过了输出端口能传输的最大长度,就无法直接发送这个包了。 遇到这种情况,可以使用 IP 协议中定义的分片功能对包进行拆分,缩短每个包的长度。 需要注意的是,这里说的分片和第 2 章介绍的 TCP 对数据进行拆分的机制是不同的。 分片是对一个完整的包再进行拆分的过程。 分片操作的过程如图 3.15 所示。 首先, 我们需要知道输出端口的 MTU,看看这个包能不能不分片直接发送。最大包长度是由端口类型决定的,用这个最大长度减掉头部的长度就是 MTU,将 MTU 与要转发的包长度进行比较。如果输出端口的 MTU 足够大,那么就可以不分片直接发送;如果输出端口的 MTU 太小,那么就需要将包按照这个 MTU 进行分片,但在此之前还需要看一下 IP 头部中的标志字段,确认是否可以分片。 > 一般来说都是可以分片的,但下面两种情况不能分片:1)发送方应用程序等设置了不允许分片;2)这个包已经是经过分片后的包。 ![3.15.png](https://cdn2.feczine.cn/2023/03/25/641ed455729f7.png) 如果查询标志字段发现不能分片, 那么就只能丢弃这个包, 并通过ICMP 消息通知发送方。否则,就可以按照输出端口 MTU 对数据进行依次拆分了。 在分片中,TCP 头部及其后面的部分都是可分片的数据, 尽管 TCP 头部不属于用户数据,但从 IP 来看也是 TCP 请求传输的数据的一部分。数据被拆分后,每一份数据前面会加上 IP 头部,其大部分内容都和原本的 IP 头部一模一样,但其中有部分字段需要更新,这些字段用于记录分片相关的信息。 #### 路由器的发送操作和计算机相同 以太网的包发送操作是根据以太网规则来进行的,即便设备种类不同,规则也是相同的。也就是说,其基本过程和协议栈中的 IP 模块发送包的过程是相同的,即在包前面加上 MAC 头部,设置其中的一些字段,然后将完成的包转换成电信号并发送出去。 为了判断 MAC 头部中的 MAC 地址应该填写什么值, 我们需要根据路由表的网关列判断对方的地址。如果网关是一个 IP 地址,则这个 IP 地址就是我们要转发到的目标地址;如果网关为空,则 IP 头部中的接收方 IP 地址就是要转发到的目标地址。知道对方的 IP 地址之后,接下来需要通过 ARP 根据 IP 地址查询 MAC 地址, 并将查询的结果作为接收方 MAC 地址。 路由器也有 ARP 缓存,因此首先会在 ARP 缓存中查询,如果找不到则发送 ARP 查询请求。 接下来是发送方 MAC 地址字段, 这里填写输出端口的 MAC 地址。 网络包完成后,接下来会将其转换成电信号并通过端口发送出去。这一步的工作过程和计算机也是相同的。 如果输出端口为以太网,则发送出去的网络包会通过交换机到达下一个路由器。由于接收方 MAC 地址就是下一个路由器的地址,所以交换机会根据这一地址将包传输到下一个路由器。接下来,下一个路由器会将包转发给再下一个路由器,经过层层转发之后,网络包就到达了最终的目的地。 #### 路由器与交换机的关系 要理解两者之间的关系,关键点在于计算机在发送网络包时,或者是路由器在转发网络包时,都需要在前面加上 MAC 头部。 也就是说,给包加上 MAC 头部并发送,从本质上说是将 IP 包装进以太网包的数据部分中,委托以太网去传输这些数据。IP 协议本身没有传输包的功能,因此包的实际传输要委托以太网来进行。路由器是基于 IP 设计的,而交换机是基于以太网设计的,因此 IP 与以太网的关系也就是路由器与交换机的关系。换句话说,*路由器将包的传输工作委托给交换机来进行*。 当然,这里讲的内容只适用于原原本本实现 IP 和以太网机制的纯粹的路由器和交换机,实际的路由器有内置交换机功能的,比如用于连接互联网的家用路由器就属于这一种,对于这种路由器,上面内容可能就不适用了。 ### 路由器的附加功能 现在的路由器除了这些基本功能之外,还有一些附加功能。下面来介绍两种最重要的功能——地址转换和包过滤。 #### 通过地址转换有效利用 IP 地址 解决地址不足的问题,利用的就是这样的性质,即公司内部设备的地址不一定要和其他公司不重复。这样一来,公司内部设备就不需要分配固定地址了,从而大幅节省了 IP 地址。当然,就算是公司内网,也不是可以随便分配地址的,因此需要设置一定的规则,规定某些地址是用于内网的,这些地址叫作私有地址,而原来的固定地址则叫作公有地址。 私有地址的规则其实并不复杂,在内网中可用作私有地址的范围仅限以下这些: ```text 内网网段: 10.0.0.0 ~ 10.255.255.255 172.16.0.0 ~ 172.31.255.255 192.168.0.0 ~ 192.168.255.255 ``` 尽管这样的确能节省一部分地址,但仅凭这一点还无法完全解决问题。 公司内网并不是完全独立的,而是需要通过互联网和其他很多公司相连接,所以当内网和互联网之间需要传输包的时候,问题就出现了,因为如果很多地方都出现相同的地址,包就无法正确传输了。 于是, 当公司内网和互联网连接的时候, 需要采用图 3.17 这样的结构,即将公司内网分成两个部分,一部分是对互联网开放的服务器,另一部分是公司内部设备。其中对互联网开放的部分分配公有地址,可以和互联网直接进行通信,这一部分和之前介绍的内容是一样的。相对地,内网部分则分配私有地址,内网中的设备不能和互联网直接收发网络包,而是通过一种特别的机制进行连接,这个机制就叫地址转换。 ![3.17.png](https://cdn2.feczine.cn/2023/03/25/641ed99d41c10.png) #### 路由器的包过滤功能 包过滤就是在对包进行转发时,根据 MAC 头部、IP 头部、TCP 头部的内容,按照事先设置好的规则决定是转发这个包,还是丢弃这个包。我们通常说的防火墙设备或软件,大多数都是利用这一机制来防止非法入侵的。 ## 通过接入网进入互联网内部 本章略过,主要详细介绍 ADSL,FTTH等 ### 互联网的基本结构和家庭、公司网络是相同的 互联网是一个遍布世界的巨大而复杂的系统,但其基本工作方式却出奇地简单。 和家庭、 公司网络一样, 互联网也是通过路由器来转发包的,而且路由器的基本结构和工作方式也并没有什么不同(图 4.1)。 因此, 我们可以将互联网理解为家庭、公司网络的一个放大版。 ![4.2.png](https://cdn2.feczine.cn/2023/03/25/641edfacf0e5e.png) 当然,互联网也有一些和家庭、公司网络不同的地方,其中之一就是与转发设备间的距离。 除了距离之外,路由器在如何控制包的转发目标上也不一样。尽管从基本原理来看,互联网也是根据路由表中的记录来判断转发目标的,但路由表记录的维护方式不同。 互联网中的路由器上有超过 10 万条路由记录,而且这些记录还在不断变化,当出现线路故障时,或者新的公司加入互联网时,都会引发路由的变化。人工维护这些路由信息是不现实的,必须实现自动化。公司的路由器也有自动维护路由表的机制,但出于各种原因,互联网中采用的机制和公司有所区别。 ## 第5章 服务端的局域网中有什么玄机 ![5.1.png](https://cdn2.feczine.cn/2023/03/25/641f03c752482.png) ### 防火墙的结构和原理 #### 主流的包过滤方式 防火墙的基本思路刚才已经介绍过了,即只允许发往特定服务器中的特定应用程序的包通过,然后屏蔽其他的包。不过,特定服务器上的特定应用程序这个规则看起来不复杂,但网络中流动着很多各种各样的包,如何才能从这些包中分辨出哪些可以通过,哪些不能通过呢?为此,人们设计了多种方式,其中任何一种方式都可以实现防火墙的目的,但出于性能、价格、易用性等因素,现在最为普及的是包过滤方式。 #### 通过防火墙 当包到达防火墙时,会根据这些规则判断是允许通过还是阻止通过如果判断结果为阻止,那么这个包会被丢弃并被记录下来。这是因为这些被丢弃的包中通常含有非法入侵的痕迹,通过分析这些包能够搞清楚入侵者使用的手法,从而帮助我们更好地防范非法入侵。 如果包被判断为允许通过,则该包会被转发出去,这个转发的过程和路由器是相同的。 在防火墙允许包通过之后,就没有什么特别的机制了,因此包过滤并不是防火墙专用的一种特殊机制,而是应该看作在路由器的包转发功能基础上附加的一种功能。只不过当判断规则比较复杂时,通过路由器的命令难以维护这些规则,而且对阻止的包进行记录对于路由器来说负担也比较大,因此才出现了专用的硬件和软件。如果规则不复杂,也不需要记录日志, 那么用内置包过滤功能的普通路由器来充当防火墙也是可以的。 ### 通过将请求平均分配给多台服务器来平衡负载 当服务器的访问量上升时,增加服务器线路的带宽是有效的,但并不是网络变快了就可以解决所有的问题。高速线路会传输大量的网络包,这会导致服务器的性能跟不上。 要解决这个问题, 大家可能首先想到的是换一台性能更好的服务器,但当很多用户同时访问时,无论服务器的性能再好,仅靠一台服务器还是难以胜任的。 在这种情况下, 使用多台服务器来分担负载的方法更有效。这种架构统称为分布式架构,其中对于负载的分担有几种方法,最简单的一种方法就是采用多台 Web 服务器,减少每台服务器的访问量。 假设现在我们有 3 台服务器,那么每台服务器的访问量会减少到三分之一,负载也就减轻了。要采用这样的方法,必须有一个机制将客户端发送的请求分配到每台服务器上。具体的做法有很多种,最简单的一种是通过 DNS 服务器来分配。 但这种方式是有缺点的。假如多台 Web 服务器中有一台出现了故障,这时我们希望在返回 IP 地址时能够跳过故障的 Web 服务器, 然而普通的DNS 服务器并不能确认 Web 服务器是否正常工作,因此即便 Web 服务器宕机了,它依然可能会返回这台服务器的 IP 地址。 此外,轮询分配还可能会引发一些问题。在通过 CGI 等方式动态生成网页的情况下,有些操作是要跨多个页面的,如果这期间访问的服务器发生了变化,这个操作就可能无法继续。(Session没法共享) #### 使用负载均衡器分配访问 为了避免出现前面的问题,可以使用一种叫作负载均衡器的设备。使用负载均衡器时, 首先要用负载均衡器的 IP 地址代替 Web 服务器的实际地址注册到 DNS 服务器上。 客户端会认为负载均衡器就是一台 Web 服务器,并向其发送请求,然后由负载均衡器来判断将请求转发给哪台 Web 服务器(图 5.4)。这里的关键点不言而喻,那就是如何判断将请求转发给哪台 Web 服务器。 ![5.4.png](https://cdn2.feczine.cn/2023/03/26/641ffad1891de.png) 判断条件有很多种,根据操作是否跨多个页面,判断条件也会有所不同。如果操作没有跨多个页面,则可以根据 Web 服务器的负载状况来进行判断。负载均衡器可以定期采集 Web 服务器的 CPU、内存使用率,并根据这些数据判断服务器的负载状况,也可以向 Web 服务器发送测试包,根据响应所需的时间来判断负载状况。 人们想出了一些方案来判断请求之间的相关性。例如,可以在发送表单数据时在里面加上用来表示关联的信息,或者是对 HTTP 规格进行扩展,在 HTTP 头部字段中加上用来判断相关性的信息。这样,负载均衡器就可以通过这些信息来作出判断,将一系列相关的请求发送到同一台 Web 服务器,对于不相关的请求则发送到负载较低的服务器了。 ### 使用缓存服务器分担负载 #### 如何使用缓存服务器 除了使用多台功能相同的 Web 服务器分担负载之外,还有另外一种方法,就是将整个系统按功能分成不同的服务器,如 Web 服务器、数据库服务器。缓存服务器就是一种按功能来分担负载的方法。 缓存服务器是一台通过代理机制对数据进行缓存的服务器。代理介于 Web 服务器和客户端之间,具有对 Web 服务器访问进行中转的功能。当进行中转时, 它可以将 Web 服务器返回的数据保存在磁盘中, 并可以代替 Web 服务器将磁盘中的数据返回给客户端。这种保存的数据称为缓存,缓存服务器指的也就是这样的功能。 不过,如果在缓存了数据之后,We b 服务器更新了数据,那么缓存的数据就不能用了,因此缓存并不是永久可用的。此外,CGI 程序等产生的页面数据每次都不同,这些数据也无法缓存。 #### 最原始的代理——正向代理 刚才讲的是在 Web 服务器一端部署一个代理,然后利用其缓存功能来改善服务器的性能,还有一种方法是在客户端一侧部署缓存服务器。 缓存服务器使用的代理机制最早就是放在客户端一侧的,这才是代理的原型,称为正向代理(forward proxy)。 正向代理刚刚出现的时候,其目的之一就是缓存,这个目的和服务器端的缓存服务器相同。不过,当时的正向代理还有另外一个目的,那就是用来实现防火墙。 此外,由于代理在转发过程中可以查看请求的内容,所以可以根据内容判断是否允许访问。也就是说,通过代理可以禁止员工访问危险的网站,或者是与工作内容无关的网站。包过滤方式的防火墙只能根据 IP 地址和端口号进行判断,因此无法实现这一目的。 在使用正向代理时,一般需要在浏览器的设置窗口中的“代理服务器”一栏中填写正向代理的 IP 地址。 #### 正向代理的改良版——反向代理 正如前面讲过的,使用正向代理需要在浏览器中进行设置,这可以说是识别正向代理的一个特征。 于是,我们可以对这种方法进行改良,使得不需要在浏览器中设置代理也可以使用。也就是说,我们可以通过将请求消息中的 URI 中的目录名与 Web 服务器进行关联,使得代理能够转发一般的不包含完整网址的请求消息。我们前面介绍的服务器端的缓存服务器采用的正是这种方式。 这种方式称为反向代理(reverse proxy)。 #### 透明代理 缓存服务器判断转发目标的方法还有一种,那就是查看请求消息的包头部。因为包的 IP 头部中包含接收方 IP 地址,只要知道了这个地址,就知道用户要访问哪台服务器了。这种方法称为透明代理(transparent proxy)。 这种方法也可以转发一般的请求消息,因此不需要像正向代理一样设置浏览器参数,也不需要在缓存服务器上设置转发目标,可以将请求转发给任意 Web 服务器。 透明代理集合了正向代理和反向代理的优点,是一个非常方便的方式,但也需要注意一点,那就是如何才能让请求消息到达透明代理。 我们必须将透明代理放在请求消息从浏览器传输到 Web 服务器的路径中,当消息经过时进行拦截,只有这样才能让消息到达透明代理,然后再转发给 Web 服务器。 如果请求消息有多条路径可以到达 Web 服务器,那么就必须在这些路径上都放置透明代理,因此一般是将网络设计成只有一条路可以走的结构,然后在这一条路径上放置透明代理。 连接互联网的接入网就是这样一个关口,因此可以在接入网的入口处放置反向代理。使用透明代理时,用户不会察觉到代理的存在,也不会注意到 HTTP 消息是如何被转发的,因此大家更倾向于将透明代理说成是缓存。 ### 内容分发服务(CDN) #### 利用内容分发服务分担负载 ![5.10.png](https://cdn2.feczine.cn/2023/03/26/64200656dd4a8.png) 像图 5.10(c)这样,Web 服务器运营者和网络运营商签约,将可以自己控制的缓存服务器放在客户端的运营商处。 这样一来,我们可以把缓存服务器部署在距离用户很近的地方,同时 Web 服务器运营者还可以控制这些服务器,但这种方式也有问题。对于在互联网上公开的服务器来说,任何地方的人都可以来访问它,因此如果真的要实现这个方式,必须在所有的运营商 POP 中都部署缓存服务器才行,这个数量太大了,非常不现实。 要解决这个问题也有一些办法。首先,我们可以筛选出一些主要的运营商,这样可以减少缓存服务器的数量。尽管这样做可能会导致有些用户访问到缓存服务器还是要经过很长的距离,但总比直接访问 Web 服务器的路径要短多了,因此还是可以产生一定的效果。 接下来这个问题更现实,那就是即便减少了数量,作为一个 Web 服务器运营者,如果自己和这些运营商签约并部署缓存服务器,无论是费用还是精力都是吃不消的。为了解决这个问题,一些专门从事相关服务的厂商出现了,他们来部署缓存服务器,并租借给 Web 服务器运营者。这种服务称为内容分发服务(Content Delivery Network,CDN)。 #### 如何找到最近的缓存服务器 在使用内容分发服务时, 如图 5.11 所示, 互联网中有很多缓存服务器,如何才能从这些服务器中找到离客户端最近的一个,并让客户端去访问那台服务器呢? ![5.11.png](https://cdn2.feczine.cn/2023/03/26/6420076c31558.png) 这样的方法有几种,下面我们按顺序来介绍: 第一个方法是像负载均衡一样用 DNS 服务器来分配访问。也就是说,我们可以在 DNS 服务器返 回 Web 服务器 IP 地址时, 对返回的内容进行一些加工, 使其能够返回距离客户端最近的缓存服务器的 IP 地址。 #### 通过重定向服务器分配访问目标 还有另一个让客户端访问最近的缓存服务器的方法。HTTP 规格中定义了很多头部字段,其中有一个叫作 Location 的字段。这种将客户端访问引导到另一台 Web 服务器的操作称为重定向,通过这种方法也可以将访问目标分配到最近的缓存服务器。 #### 缓存的更新方法会影响性能 还有一个因素会影响缓存服务器的效率,那就是缓存内容的更新方法。 缓存本来的思路是曾经访问过的数据保存下来,然后当再次访问时拿出来用,以提高访问操作的效率。不过,这种方法对于第一次访问是无效的,而且后面的每次访问都需要向原始服务器查询数据有没有发生变化,如果遇到网络拥塞,就会使响应时间恶化。 要改善这一点,有一种方法是让 Web 服务器在原始数据发生更新时,立即通知缓存服务器,使得缓存服务器上的数据一直保持最新状态,这样就不需要每次确认原始数据是否有变化了,而且从第一次访问就可以发挥缓存的效果。内容分发服务采用的缓存服务器就具备这样的功能。 ## 请求到达 Web 服务器,响应返回浏览器 ### 服务器概览 #### 客户端与服务器的区别 网络相关的部分,如网卡、协议栈、Socket 库等功能和客户端却并无二致。 不过,它们的功能相同,不代表用法也相同。在连接过程中,客户端发起连接操作,而服务器则是等待连接操作,因此在 Socket 库的用法上还是有一些区别的,即应用程序调用的 Socket 库的程序组件不同。 此外,服务器的程序可以同时和多台客户端计算机进行通信,这也是一点区别。因此,服务器程序和客户端程序在结构上是不同的。 #### 服务器程序的结构 服务器需要同时和多个客户端通信,但一个程序来处理多个客户端的请求是很难的,因为服务器必须把握每一个客户端的操作状态。因此一般的做法是,每有一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的状态。 ![6.1.png](https://cdn2.feczine.cn/2023/03/26/642034ea8a4c2.png) 具体来说,服务器程序的结构如图 6.1 所示。 首先, 我们将程序分成两个模块, 即等待连接模块(a) 和 负责与客户端通信的模块(b)。 当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块(a)。 这个模块会创建套接字, 然后进入等待连接的暂停状态。 接下来, 当客户端连发起连接时, 这个模块会恢复运行并接受连接,然后启动客户端通信模块(b), 并移交完成连接的套接字。 接下来, 客户端通信模块(b)就会使用已连接的套接字与客户端进行通信,通信结束后,这个模块就退出了。 每次有新的客户端发起连接,都会启动一个新的客户端通信模块(b),因此(b)与客户端是一对一的关系。 这样,( b)在工作时就不必考虑其他客户端的连接情况,只要关心自己对应的客户端就可以了。 当然,这种方法在每次客户端发起连接时都需要启动新的程序,这个过程比较耗时,响应时间也会相应增加。因此,还有一种方法是事先启动几个客户端通信模块,当客户端发起连接时,从空闲的模块中挑选一个出来将套接字移交给它来处理。 #### 服务器端的套接字和端口号 从数据收发的角度来看,区分“客户端”和“服务器”这两个固定的角色似乎不是一个好办法。现在大多数应用都是由客户端去访问服务器,但其实应用的形态不止这一种。为了能够支持各种形态的应用,最好是在数据收发层面不需要区分客户端和服务器,而是能够以左右对称的方式自由发送数据。TCP 也正是在这样的背景下设计出来的。 不过,这其中还是存在一个无法做到左右对称的部分,那就是连接操作。 连接这个操作是在有一方等待连接的情况下, 另一方才能发起连接。从数据收发的角度来看,这就是客户端与服务器的区别,也就是说,发起连接的一方是客户端,等待连接的一方是服务器。 这个区别体现在如何调用 Socket 库上。首先,客户端的数据收发需要经过下面 4 个阶段: - (1)创建套接字(创建套接字阶段) - (2)用管道连接服务器端的套接字(连接阶段) - (3)收发数据(收发阶段) - (4)断开管道并删除套接字(断开阶段) 相对地,服务器是将阶段(2)改成了等待连接,具体如下: - (1)创建套接字(创建套接字阶段) - (2-1)将套接字设置为等待连接状态(等待连接阶段) - (2-2)接受连接(接受连接阶段) - (3)收发数据(收发阶段) - (4)断开管道并删除套接字(断开阶段) 下面我们像前面介绍客户端时一样,用伪代码来表示这个过程,如图 6.2 所示。 ![6.2.png](https://cdn2.feczine.cn/2023/03/26/642038aa432be.png) 首先,协议栈调用 socket 创建套接字。 接下来调用 bind 将端口号写入套接字中。 在客户端发起连接的操作中,需要指定服务器端的端口号,这个端口号也就是在这一步设置的。具体的编号是根据服务器程序的种类,按照规则来确定的,例如 Web 服务器使用 80 号端口。 设置好端口号之后, 协议栈会调用 listen 向套接字写入等待连接状态这一控制信息。这样一来,套接字就会开始等待来自客户端的连接网络包。 然后,协议栈会调用 accept 来接受连接。 在执行 accept 的时候,一般来说服务器端都是处于等待包到达的状态,这时应用程序会暂停运行。在这个状态下,一旦客户端的包到达,就会返回响应包并开始接受连接操作。接下来,协议栈会给等待连接的套接字复制一个副本, 然后将连接对象等控制信息写入新的套接字中(图 6.3)。 刚才我们介绍了调用 accept 时的工作过程, 到这里, 我们就创建了一个新的套接字,并和客户端套接字连接在一起了。 当 accept 结束之后,等待连接的过程也就结束了,这时等待连接模块会启动客户端通信模块,然后将连接好的新套接字转交给客户端通信模块,由这个模块来负责执行与客户端之间的通信操作。之后的数据收发操作和刚才说的一样,与客户端的工作过程是相同的。 在复制出一个新的套接字之后,原来那个处于等待连接状态的套接字会怎么样呢? 它还会像这样每次为新的连接创建新的套接字。 如果不创建新副本,而是直接让客户端连接到等待连接的套接字上,那么就没有套接字在等待连接了,这时如果有其他客户端发起连接就会遇到问题。为了避免出现这样的情况,协议栈采用了这种创建套接字的新副本,并让客户端连接到这个新副本上的方法。 此外,创建新套接字时端口号也是一个关键点。 新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号。 但是这样一来又会引发另一个问题。端口号是用来识别套接字的,如果一个端口号对应多个套接字,就无法通过端口号来定位到某一个套接字了。当客户端的包到达时,如果协议栈只看 TCP 头部中的接收方端口号,是无法判断这个包到底应该交给哪个套接字的。 这个问题可以用下面的方法来解决,即要确定某个套接字时,不仅使用服务器端套接字对应的端口号,还同时使用客户端的端口号再加上 IP 地址,总共使用 4 种信息来进行判断。 - 客户端 IP 地址 - 客户端端口号 - 服务器 IP 地址 - 服务器端口号 说句题外话, 既然通过客户端 IP 地址、 客户端端口号、 服务器 IP 地址、服务器端口号这 4 种信息可以确定某个套接字,那么要指代某个套接字时用这 4 种信息就好了,为什么还要使用描述符呢?这个问题很好,不过我们无法用上面 4 种信息来代替描述符。原因是,在套接字刚刚创建好, 还没有建立连接的状态下,这 4 种信息是不全的。此外,为了指代一个套接字, 使用一种描述符比使用 4 种信息要简单。 出于上面两个原因,应用程序和协议栈之间是使用描述符来指代套接字的。 > 使用描述符来指代套接字的原因如下: > (1)等待连接的套接字中没有客户端 IP 地址和端口号 > (2)使用描述符这一种信息比较简单 ### 服务器的接收操作 #### 网卡将接收到的信号转换成数字信息 到达服务器的网络包其本质是电信号或者光信号,接收信号的过程和客户端是一样的。 接收操作的第一步是网卡接收到信号,然后将其还原成数字信息。 局域网中传输的网络包信号是由 1 和 0 组成的数字信息与用来同步的时钟信号叠加而成的,因此只要从中分离出时钟信号,然后根据时钟信号进行同步,就可以读取并还原出 1 和 0 的数字信息了。 接下来需要根据包末尾的帧校验序列(FCS)来校验错误, 即根据校验公式计算刚刚接收到的数字信息,然后与包末尾的 FCS 值进行比较。 当 FCS 一致,即确认数据没有错误时,接下来需要检查 MAC 头部中的接收方 MAC 地址,看看这个包是不是发给自己的。以太网的基本工作方式是将数据广播到整个网络上,只有指定的接收者才接收数据,因此网络中还有很多发给其他设备的数据在传输,如果包的接收者不是自己,那么就需要丢弃这个包。 到这里,接收信号并还原成数字信息的操作就完成了,还原后的数字信息被保存在网卡内部的缓冲区中。上面这些操作都是由网卡的 MAC 模块来完成的。 ![6.5.png](https://cdn2.feczine.cn/2023/03/26/64203f444779d.png) 在这个过程中,服务器的 CPU 并不是一直在监控网络包的到达,而是在执行其他的任务,因此 CPU 并不知道此时网络包已经到达了。但接下来的接收操作需要 CPU 来参与,因此网卡需要通过中断将网络包到达的事件通知给 CPU。 接下来,CPU 就会暂停当前的工作,并切换到网卡的任务。 #### IP 模块的接收操作 当网络包转交到协议栈时,IP 模块会首先开始工作,检查 IP 头部。IP 模块首先会检查 IP 头部的格式是否符合规范, 然后检查接收方 IP 地址,看包是不是发给自己的。当服务器启用类似路由器的包转发功能时,对于不是发给自己的包,会像路由器一样根据路由表对包进行转发。 确认包是发给自己的之后,接下来需要检查包有没有被分片。 到这里,我们就完成了包的接收。 接下来需要检查 IP 头部的协议号字段,并将包转交给相应的模块。 #### TCP 模块如何处理连接包 首先,我们来看一下发起连接的包是如何处理的。 当 TCP 头部中的控制位 SYN 为 1 时, 表示这是一个发起连接的包。 这时,TCP 模块会执行接受连接的操作, 不过在此之前, 需要先检查包的接收方端口号,并确认在该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字。如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包。 如果存在等待连接的套接字,则为这个套接字复制一个新的副本,并将发送方 IP 地址、端口号、序号初始值、窗口大小等必要的参数写入这个套接字中,同时分配用于发送缓冲区和接收缓冲区的内存空间。然后生成代表接收确认的 ACK 号,用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小,并用这些信息生成 TCP 头部,委托 IP 模块发送给客户端。 这个包到达客户端之后, 客户端会返回表示接收确认的 ACK 号, 当 这个 ACK 号返回服务器后,连接操作就完成了。 ![6.7.png](https://cdn2.feczine.cn/2023/03/26/6420414fe2859.png) #### TCP 模块如何处理数据包 接下来我们来看看进入数据收发阶段之后,当数据包到达时 TCP 模块是如何处理的。 首先,TCP 模块会检查收到的包对应哪一个套接字。 找到 4 种信息全部匹配的套接字之后,TCP 模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配,以确定数据收发操作是否正常。 如果两者一致,就说明包正常到达了服务器,没有丢失。这时,TCP 模块会从包中提出数据,并存放到接收缓冲区中,与上次收到的数据块连接起来。这样一来,数据就被还原成分包之前的状态了。 当收到的数据进入接收缓冲区后,TCP 模块就会生成确认应答的 TCP头部,并根据接收包的序号和数据长度计算出 ACK 号,然后委托 IP 模块发送给客户端。 收到的数据块进入接收缓冲区,意味着数据包接收的操作告一段落了。接下来,应用程序会调用 Socket 库的 read 来获取收到的数据,这时数据会被转交给应用程序。 然后,控制流程会转移到服务器程序,对收到的数据进行处理,也就是检查 HTTP 请求消息的内容,并根据请求的内容向浏览器返回相应的数据。 #### TCP 模块的断开操作 在 TCP 协议的规则中, 断开操作可以由客户端或服务器任何一方发起,具体的顺序是由应用层协议决定的。We b 中,这一顺序随 HTTP 协议版本不同而不同,在 HTTP1.0 中,是服务器先发起断开操作。 这时, 服务器程序会调用 Socket 库的 close,TCP 模块会生成一个控制位 FIN 为 1 的 TCP 头部, 并委托 IP 模块发送给客户端。 当客户端收到这个包之后, 会返回一个 ACK 号。 接下来客户端调用 close, 生成一个 FIN 为 1 的 TCP 头部发给服务器, 服务器再返回 ACK 号, 这时断开操作 就完成了。HTTP1.1 中,是客户端先发起断开操作,这种情况下只要将客户端和服务器的操作颠倒一下就可以了。 无论哪种情况,当断开操作完成后,套接字会在经过一段时间后被删除。 ### Web 服务器程序解释请求消息并作出响应 #### 返回响应消息 当服务器完成对请求消息的各种处理之后, 就可以返回响应消息了。这里的工作过程和客户端向服务器发送请求消息时的过程相同。 首先,Web 服务器调用 Socket 库的 write, 将响应消息交给协议栈。这时,需要告诉协议栈这个响应消息应该发给谁,但我们并不需要直接告知客户端的 IP 地址等信息,而是只需要给出表示通信使用的套接字的描述符就可以了。套接字中保存了所有的通信状态,其中也包括通信对象的信 息,因此只要有描述符就万事大吉了。 接下来,协议栈会将数据拆分成多个网络包,然后加上头部发送出去。这些包中包含接收方客户端的地址, 它们将经过交换机和路由器的转发,通过互联网最终到达客户端。 ### 浏览器接收响应消息并显示内容 #### 通过响应的数据类型判断其中的内容 我们需要一些信息才能判断数据类型,原则上可以根据响应消息开头的 Content-Type 头部字段的值来进行判断。 #### 浏览器显示网页内容!访问完成! 判断完数据类型,我们离终点就只有一步之遥了。接下来只要根据数据类型调用用于显示内容的程序,将数据显示出来就可以了。对于 HTML 文档、纯文本、图片这些基本数据类型,浏览器自身具有显示这些内容的功能,因此由浏览器自身负责显示。 到这里,浏览器的显示操作就完成了,可以等待用户的下一个动作了。当用户点击网页中的链接,或者在网址栏中输入新的网址时,访问 Web 服务器的操作就又开始了。 最后修改:2023 年 03 月 26 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 3 本作品采用 CC BY-NC-SA 4.0 International License 进行许可。