文章

TCP/IP 教程 | 网络层

网络层是为传输层提供服务的,传送的协议数据单元称为数据包或分组。 该层的主要作用是解决如何使数据包通过各结点传送的问题,即通过路径选择算法(路由)将数据包送到目的地。 另外,为避免通信子网中出现过多的数据包而造成网络阻塞,需要对流入的数据包数量进行控制(拥塞控制)。 当数据包要跨越多个通信子网才能到达目的地时,还要解决网际互连的问题。

IP 协议

IP协议是TCP/IP协议的核心,所有的TCP,UDP,IMCP,IGCP的数据都以IP数据格式传输。要注意的是,IP不是可靠的协议,这是说,IP协议没有提供一种数据未传达以后的处理机制, 这被认为是上层协议。

image.png

IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。

字段说明
4位版本字段对于IPv4,4位版本字段是4。
4位首部长度该数值是以4字节为单位的,最小值为5,也就是说首部长度最小是4x5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。
8位TOS字段8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大呑吐量、最大可靠性、最小成本),还有一个位总是0。总长度是整个数据报(包括IP首部和IP层payload)的字节数。每传一个IP数据报,
16位的标识加1,可用于分片和重新组装数据报。
3位标志13位片偏移用于分片。
TTL(Time to live)源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。这个字段的最大值也就是255,也就是说一个协议包也就在路由器里面穿行255次就会被抛弃了,根据系统的不同,这个数字也不一样,一般是32或者是64.
协议字段上层协议是TCPUDPICMP 还是 IGMP 等。
校验和校验IP首部,数据的校验由更高层协议负责。IPv4的IP地址长度为32位。

协议字段重要取值详情

取值含义
1Internet控制消息 (ICMP)
2Internet组管理 (IGMP)
6传输控制 (TCP)
8外部网关协议 (EGP)
9任何私有内部网关(Cisco在它的IGRP实现中使用) (IGP)
17用户数据报文 (UDP)

网络

为了方便寻址与主机地址分配跟扩展, 世界上的所有主机IP根据一定的规则划分成一个一个的网络区域,所以每个主机都拥有一个网络地址与ip地址。 网络地址用来标识同一个网络下的主机网络号。

image.png

IP 网络的划分总共经历了三个阶段 :

  1. 分类的IP地址
  2. 划分子网
  3. 构成超网

构成超网又叫无分类编址CIDR,是现在IPv4的地址网络划分方式。

CIDR 将IP地址分为两部分 : **{<网络前缀>,<主机号>}** CIDR把网络前缀都相同的连续IP地址组成一个“**CIDR地址块**”,即强化路由聚合(构成超网)。 一个CIDR地址块中有很多地址,所以在路由表中就利用**CIDR地址块**来查找目的网络。

CIDR

在 CIDR 中, 我们使用三部分描述一个ip : ip地址子网掩码, 网络地址.

网络地址 = 子网掩码 & ip地址

每个ip地址与子网掩码都是使用长度为4字节的无符号证书来表示, 为了辩识方便, 才使用.来将每个字节分割开来。

例如, 上面的ip 地址 : 172.31.31.147, 其子网掩码为 : 255.255.240.0

则其存储方式为 :

1
2
3
4
5
6
IP    : 0xAC1F1F93
MASK  : 0xFFFFF000

network = IP & MASK = 0xAC1F1F93 & 0xFFFFF000 
      : 0xAC1F1000
      : 172.31.16.0

注意的是:

子网掩码从左到右必须连续1

为了表示方便,CIDR 使用斜线技法来表示ip跟网络, 例如 : 172.31.31.147/20

特殊的IP地址:

在 IP地址中有几个特殊的情况需要留意:

image.png


ICMP:Internet控制报文协议

ICMP 报文通常被IP层或更高层协议(TCP或UDP)使用。一些ICMP报文把差错报文返回给用户进程。主要用于传输一些协议控制信息

其数据包封装在IP包内传输:

image.png

ICMP 包的格式为:

image.png

各种类型的ICMP报文如图所示,不同类型由报文中的类型字段和代码字段来共同决定。

image.png

图中的最后两列表明ICMP报文是一份查询报文还是一份差错报文。因为对ICMP差错报文有时需要作特殊处理,因此我们需要对它们进行区分。例如,在对ICMP差错报文进行响应时,永远不会生成另一份ICMP差错报文(如果没有这个限制规则,可能会遇到一个差错产生另一个差错的情况,而差错再产生差错,这样会无休止地循环下去)。

下面各种情况都不会导致产生ICMP差错报文:

  1. ICMP差错报文(但是,ICMP查询报文可能会产生ICMP差错报文)。
  2. 目的地址是广播地址或多播地址的IP数据报。
  3. 作为链路层广播的数据报。
  4. 不是IP分片的第一片。
  5. 源地址不是单个主机的数据报。这就是说,源地址不能为零地址、环回地址、广播地址或多播地址。

IP 选路

选路是IP最重要的功能之一。需要进行选路的数据报可以由本地主机产生,也可以由其他主机产生。在后一种情况下,主机必须配置成一个路由器,否则通过网络接口接收到的数据报,如果目的地址不是本机就要被丢弃(例如,悄无声息地被丢弃)。

我们首先描述一个路由守护程序,通常这是一个用户进程。

image.png

路由表经常被IP访问(在一个繁忙的主机上,一秒钟内可能要访问几百次),但是它被路由守护程序更新的频度却要低得多(可能大约3 0秒种一次)。当接收到ICMP重定向,报文时,路由表也要被更新。

我们之前概述篇也有讲到, 当数据包跨网络传输时,就是需要使用目的IP地址查询路由表, 从中找出数据包的下一站地址。

搜索路由表的过程大致为 :

1
2
3
4
5
1) 搜索路由表,寻找能与目的IP地址完全匹配的表目(网络号和主机号都要匹配)。如果找到,则把报文发送给该表目指定的下一站路由器或直接连接的网络接口(取决于标志字段的值)。

2) 搜索路由表,寻找能与目的网络号相匹配的表目。如果找到,则把报文发送给该表目指定的下一站路由器或直接连接的网络接口(取决于标志字段的值)。目的网络上的所有主机都可以通过这个表目来处置。例如,一个以太网上的所有主机都是通过这种表目进行寻径的。这种搜索网络的匹配方法必须考虑可能的子网掩码。

3) 搜索路由表,寻找标为“默认(default)”的表目。如果找到,则把报文发送给该表目指定的下一站路由器。

路由表介绍

image.png

Destination 为目的地址 Gateway 为数据包发到目的地址需要经过的中间路由地址 0.0.0.0 表示直达。 Flags 为路由标志

1
2
3
4
5
U 该路由可以使用。
G 该路由是到一个网关(路由器)。如果没有设置该标志,说明目的地是直接相连的。用于区分路由为目的地址,还是间接路由地址。
H 该路由是到一个主机,也就是说,目的地址是一个完整的主机地址。如果没有设置该标志,说明该路由是到一个网络,而目的地址是一个网络地址:一个网络号,或者网络号与子网号的组合。
D 该路由是由重定向报文创建的。
M 该路由已被重定向报文修改。

Iface : 用来表示到达Gateway的出发接口。

没有到达目的地的路由

结果取决于该IP数据报是由主机产生的还是被转发的。 如果数据报是由本地主机产生的,那么内核就给发送该数据报的应用程序返回一个差错,或者是“主机不可达差错”或者是“网络不可达差错”。

如果是被转发的数据报,那么就给原始发送端发送一份ICMP主机与网络不可达差错。

当路由器收到一份IP数据报但又不能转发时,就要发送一份ICMP“主机不可达”差错报文

ICMP重定向差错

当IP数据报应该被发送到另一个路由器时,收到数据报的路由器就要发送ICMP重定向差错报文给IP数据报的发送端。

  1. 我们假定主机发送一份IP数据报给R1。这种选路决策经常发生,因为R1是该主机的默认路由。
  2. R1收到数据报并且检查它的路由表,发现R2是发送该数据报的下一站。当它把数据报发送给R2时,R1检测到它正在发送的接口与数据报到达接口是相同的(即主机和两个路由器所在的LAN)。这样就给路由器发送重定向报文给原始发送端提供了线索。
  3. R1 发送一份ICMP重定向报文给主机,告诉它以后把数据报发送给R2而不是R1。

image.png

重定向一般用来让具有很少选路信息的主机逐渐建立更完善的路由表。主机启动时路由表中可以只有一个默认表项。一旦默认路由发生差错,默认路由器将通知它进行重定向,并允许主机对路由表作相应的改动。ICMP重定向允许TCP/IP主机在进行选路时不需要具备智能特性,而把所有的智能特性放在路由器端

ICMP路由器发现报文

一般认为,主机在引导以后要广播或多播传送一份路由器请求报文。一台或更多台路由器响应一份路由器通告报文。另外,路由器定期地广播或多播传送它们的路由器通告报文,允许每个正在监听的主机相应地更新它们的路由表。

路由器在一份报文中可以通告多个地址。地址数指的是报文中所含的地址数。地址项大小指的是每个路由器地址32 bit字的数目,始终为2。生存期指的是通告地址有效的时间(秒数)

请求报文格式:

image.png

通告报文格式 :

image.png

接下来是一对或多对IP地址和优先级。IP地址必须是发送路由器的某个地址。优先级是一个有符号的32 bit整数,指出该IP地址作为默认路由器地址的优先等级,这是与子网上的其他路由器相比较而言的。值越大说明优先级越高。优先级为0x80000000说明对应的地址不能作为默认路由器地址使用,尽管它也包含中通告报文中。优先级的默认值一般为0。

路由表的动态构建协议

IP 选路过程必须依赖路由表, 所以路由表的构建对于通信非常重要。

在构建路由表过程中可以以默认方式生成路由表项, 通过route命令增加表项,或者通过ICMP重定向生成表项 这几种方式被称为静态选路过程。

当然还有一种动态选路机制的支持, 就是在路由器运行过程中,通过一定的协议彼此之间共享路由信息,从而构建更完整的路由表的过程。

Internet 是以一组自治系统(AS) 的方式组织的,每个自治系统通常由单个实体管理。常常将一个公司或大学校园定义为一个自治系统。

每个自治系统可以选择该自治系统中各个路由器之间的选路协议。这种协议我们称为内部网关协议IGP或域内选路协议

不同自治系统之间的路由器我们称为外部网关协议EGP.新EGP是当前NSFNET骨干网和一些连接到骨干网的区域性网络上使用的是边界网关协议BGP.

IGP 域内选路协议

RIP
报文格式

RIP 报文包含在UDP数据包中

image.png

其中 RIP 数据包的报文格式为

image.png

字段说明
命令字段1表示请求,2表示应答。请求表示要求其他系统发送全部或部分路由表,应答表示包含发送者全部或部分路由表。
版本字段1表示版本1,即RIP-1。版本字段为2表示版本2,即RIP-2。
地址系列紧跟在后面的20字节指定的地址系列,对于IP地址来说,其值为2。采用这种20字节格式的RIP报文最多可以通告25条路由,这是因为为了RIP报文长度小于512字节。
度量字段所谓度量就是以跳计算路由器之间的链路数。

到目的网络的距离以跳为单位。最大距离为15,距离16表示无穷大,即目的网络不可达。一条有限的路径长度不得超过15。正是这一规定限制了RIP的使用范围,使RIP局限于小型的局域网络中。

初始时每个RIP路由器只有到直连网的路由,他们的距离为1。

每30sRIP路由器把它的整个路由表发送给邻居。具体实现时每个邻居会错开发送,30s的时间也会随机变化一点。

当收到邻居发来的路由表(update packet),路由器将更新它的路由表<目的网络,开销,下一跳>

  1. 收到的路由的距离全部加1(即一跳的距离)。
  2. 把路由表中不存在的路由加入路由表
  3. 如果比路由表中的路由的距离更小,则更新该路由的距离为新距离,把下一跳改为邻居
  4. 如果路由存在,就要重置失效定时器。

缺陷

  1. RIP没有子网的概念,如果出现B类地址,主机号不为0,没法区分这是子网号还是主机地址。
  2. 链路故障后很长时间才能稳定下来。
  3. 可能发生路由环回。
  4. 度量值最大15,限制了使用RIP的网络大小。
RIP2
报文格式

image.png

改进
字段说明
路由域守护程序的进程号,该域允许管理者在单个路由器上运行多个RIP实例。
路由标记自治系统号,为了支持外部网关协议而存在。
子网掩码应用于相应的IP上,这就解决了RIP-1不能区分子网的缺陷。
下一站IP地址指明了发往目的IP地址的报文经过的第一个路由器是谁。该字段为0表示发给发送RIP报文系统。

RIP-2提供了一种简单的鉴别机制。可以指定前20字节表项地址系列为0xffff,路由标记为2。其余16字节为明文口令。 RIP-2还支持多播,这可以减少不收听RIP-2报文主机的负载。

OSPF

链路状态路由协议使用Dijkstra算法,也称SPF(Shortest Path First,最短路径优先)算法。 常见的链路状态路由协议有:OSPFv2、OSPFv3等。这篇文章主要针对OSPFv2,OSPFv3是面向IPv6的且不兼容IPv4,暂不介绍。

包格式

image.png

字段说明
Version版本字段,占1个字节,指出所采用的OSPF协议版本号,目前最高版本为OSPF v4,即值为4(对应二进制就是0100)
Packet Type报文类型字段,标识对应报文的类型。前面说了OSPF有5种报文,分别是:Hello报文DD报文LSR报文LSU报文LSAck报文
Packet Length包长度字段,占2个字节。它是指整个报文(包括OSPF报头部分和后面各报文内容部分)的字节长度。
Router ID器ID字段,占4个字节,指定发送报文的源路由器ID。
Area IDID字段,占4个字节,指定发送报文的路由器所对应的OSPF区域号。
Checksum校验和字段,占2个字节,是对整个报文(包括OSPF报头和各报文具体内容,但不包括下面的Authentication字段)的校验和,用于对端路由器校验报文的完整性和正确性。
AuType认证类型字段,占2个字节,指定所采用的认证类型,0为不认证1为进行简单认证2采用MD5方式认证
工作过程
  1. 每台路由器学习激活的直接相连的网络。
  2. 每台路由器和直接相连的路由器互交,发送Hello报文,建立邻居关系。
  3. 每台路由器构建包含直接相连的链路状态的LSA(Link-State Advertisement,链路状态通告)。链路状态通告(LSA)中记录了所有相关的路由器,包括邻路由器的标识、链路类型、带宽等。
  4. 每台路由器泛洪链路状态通告(LSA)给所有的邻路由器,并且自己也在本地储存邻路由发过来的LSA,然后再将收到的LSA泛洪给自己的所有邻居,直到在同一区域中的所有路由器收到了所有的LSA。每台路由器在本地数据库中保存所有收到的LSA副本,这个数据库被称作”链路状态数据库(LSDB,Link-State Database)”
  5. 每台路由器基于本地的”链路状态数据库(LSDB)”执行”最短路径优先(SPF)”算法,并以本路由器为根,生成一个SPF树,基于这个SPF树计算去往每个网络的最短路径,也就得到了最终的路由表。

分片

互联网协议使网络互相通信。设计要迎合不同物理性质的网络; 它是独立于链路层使用的基础传输技术。具有不同硬件的网络通常会发生变化,不仅在传输速度,而且在最大传输单元(MTU)。 当一个网络要的数据报发送到具有较小MTU的一个网络,它可能片段的数据报。 在IPv4中,这个功能被放置在因特网层,并且在IPv4路由器,这因此只需要这个层作为最高的一个在其设计中实现的处理。 与此相反,IPv6的,下一代互联网协议的,不允许的路由器来执行分片; 发送数据包之前,主机必须确定路径MTU。

所用到的字段说明

标识符(16位) : 协议栈应该保证来自同一个数据报的若干分片必须有一样的值。

标志位(3位) : 分别是R(保留位,未使用)位DF(Do not Fragment,不允许分段)位和MF(More Fragment)位。MF位为1表示当前数据报还有更多的分片,为0表示当前分片是该数据报最后一个分片。

偏移量(13位) : 表示当前数据报分片数据起始位置在完整数据报的偏移,注意这里一个单位代表8个字节。即这里的值如果是185,则代表该分片在完整数据报的偏移是185*8=1480字节。

数据分片

IP协议理论上允许的最大IP数据报为65535字节(16位来表示包总长)。 但是因为协议栈网络层下面的数据链路层一般允许的帧长远远小于这个值,例如以太网的MTU(即Maximum Transmission Unit,最大传输单元)通常在1500字节左右。 而且收到网络硬件的的原因, 每个网络的MTU都不确定。

当链路层接受到一个超过本网络支持上限大小的数据包时,就要重新将数据包分片成不同大小的子IP包,然后把包发出。

这时来自同一个IP包的所有子包将被赋予相同的标识符, 根据子包在原来包的偏移位置设置偏移量, 完成拆包工作。

分片重组

接收方在收到经过IP层分片的数据报文后。 首先根据分片标志中的 MF位 判断是否是最后一个分片报文,如果是,则根据分片偏移量计算各个分片报文在原始数据报中的位置,进行重组。

如果不是最后一个分片,则需等待所有分片到达后再完成重组。

避免分片

IP层是没有超时重传机制的 ,如果IP层对一个数据包进行了分片,只要有一个分片丢失了,只能依赖于传输层进行重传,结果是所有的分片都要重传一遍,这个代价有点大。由此可见,IP分片会大大降低传输层传送数据的成功率,所以我们要避免IP分片。

对于TCP协议,应用层就不需要考虑这个问题了,因为传输层已经帮我们做了。在建立连接的TCP三次握手的过程中,连接双方会相互通告MSS,MSS一般是 MTU-IP首部-TCP首部,每次发送的TCP数据都不会超过双方MSS的值,所以就保证了IP数据报不会超过MTU,避免了IP分片。

对于UDP包,我们需要在应用层去限制每个包的大小,一般不要超过1472字节,即以太网MTU-IP首部-UDP首部


广播

广播仅应用于UDP

首先网卡查看由信道传送过来的帧, 确定是否接受该帧,若接收就将它传网设备驱动程序。 通常网卡仅接收目的地址为网卡物理地址或广播地址的帧。

广播地址帧格式为 : ff:ff:ff:ff:ff:ff

受限的广播地址

受限的广播地址是255.255.255.255

在任何情况下,路由器都不转发目的地址为受限的广播地址的数据报,这样的数据报仅出现在本地网络中。

一个未解的问题

如果一个主机是多接口的,当一个进程向本网广播地址发送数据报时,为实现广播,是否应该将数据报发送到每个相连的接口上? 如果不是这样,想对主机所有接口广播的应用必须确定主机中支持广播的所有接口,然后向每个接口发送一个数据报复制。

大多数BSD系统将255.255.255.255看作是配置后第一个接口的广播地址,并且不提供向所属具备广播能力的接口传送数据报的功能。 不过,routed 和 rwhod 是向每个接口发送UDP数据报的两个应用程序。这两个应用程序均用相似的启动过程来确定主机中的所有接口,并了解哪些接口具备广播能力。 同时,将对应于那种接口的指向网络的广播地址作为发往该接口的数据报的目的地址。

指向网络的广播地址

指向网络的广播地址是主机号为全1的地址。

网络地址主机号
netId111…

我们可以通过 ifconfig 简单的获取我们主机所在的网络的广播地址

1
2
3
4
5
6
7
8
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 9001
        inet 172.31.8.168  netmask 255.255.255.0  broadcast 172.31.8.255
        inet6 fe80::cec:96ff:fe51:fc58  prefixlen 64  scopeid 0x20<link>
        ether 0e:ec:96:51:fc:58  txqueuelen 1000  (Ethernet)
        RX packets 13815  bytes 59932423 (57.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 9948  bytes 1412784 (1.3 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

多播

多播仅应用于UDP, 协议规定为传统的D类IP地址为多播地址。

image.png

多播组地址包括为111 0的最高4bit和多播组号。它们通常可表示为点分十进制数,范围从224.0.0.0239.255.255.255

能够接收发往一个特定多播组地址数据的主机集合称为主机组(host group)。一个主机组可跨越多个网络。主机组中成员可随时加入或离开主机组。主机组中对主机的数量没有限制,同时不属于某一主机组的主机可以向该组发送信息。

一些多播组地址被IANA确定为知名地址。它们也被当作永久主机组,这和TCP及UDP中的熟知端口相似。同样,这些知名多播地址在RFC最新分配数字中列出。 注意这些多播地址所代表的组是永久组,而它们的组成员却不是永久的。

举例特殊的多播组

地址用途
224.0.0.1该子网内的所有系统组
224.0.0.2该子网内的所有路由器组
224.0.1.1网络时间协议NTP
224.0.0.9RIP-2
224.0.1.2SGI公司的dogfight应用

多播组地址到以太网地址的转换

IANA拥有一个以太网地址块,即高位24 bit为00:00:5e,这意味着该地址块所拥有的地址范围从00:00:5e:00:00:0000:00:5e:ff:ff:ff。 IANA将其中的一半分配为多播地址。为了指明一个多播地址,任何一个以太网地址的首字节必须是01. 这意味着与IP多播相对应的以太网地址范围从01:00:5e:00:00:0001:00:5e:7f:ff:ff

这种地址分配将使以太网多播地址中的23bit与IP多播组号对应起来,通过将多播组号中的低位23bit映射到以太网地址中的低位23bit实现

image.png

由于多播组号中的最高5bit在映射过程中被忽略,因此每个以太网多播地址对应的多播组是不唯一的。

既然地址映射是不唯一的,那么设备驱动程序或IP层就必须对数据报进行过滤。 因为网卡可能接收到主机不想接收的多播数据帧。 另外,如果网卡不提供足够的多播数据帧过滤功能,设备驱动程序就必须接收所有多播数据帧,然后对它们进行过滤。

IGMP 协议

当涉及多个网络并且多播数据必须通过路由器转发时,情况会复杂得多。 所以就需要用于支持主机和路由器进行多播的Internet组管理协议(IGMP)。

它让一个物理网络上的所有系统知道主机当前所在的多播组。多播路由器需要这些信息以便知道多播数据报应该向哪些接口转发。

IGMP 报文格式:

image.png

字段说明
版本IGMP 版本
类型1 说明是由多播路由器发出的查询报文, 2说明是主机发出的报告报文。
组地址D类IP地址。在查询报文中组地址设置为0,在报告报文中组地址为要参加的组地址。
加入一个多播组

进程必须以某种方式在给定的接口上加入某个多播组。 进程也能离开先前加入的多播组。这些是一个支持多播主机中任何API所必需的部分。 使用限定词“接口”是因为多播组中的成员是与接口相关联的。 一个进程可以在多个接口上加入同一多播组。

一个主机通过组地址接口来识别一个多播组。 主机必须保留一个表,此表中包含所有至少含有一个进程的多播组以及多播组中的进程数量。

IGMP报告和查询

多播路由器使用IGMP报文来记录与该路由器相连网络中组成员的变化情况。

  1. 当第一个进程加入一个组时,主机就发送一个IGMP报告。如果一个主机的多个进程加入同一组,只发送一个IGMP报告。这个报告被发送到进程加入组所在的同一接口上。
  2. 进程离开一个组时,主机不发送IGMP报告,即便是组中的最后一个进程离开。主机知道在确定的组中已不再有组成员后,在随后收到的IGMP查询中就不再发送报告报文。
  3. 多播路由器定时发送IGMP查询来了解是否还有任何主机包含有属于多播组的进程。多播路由器必须向每个接口发送一个IGMP查询。因为路由器希望主机对它加入的每个多播组均发回一个报告,因此IGMP查询报文中的组地址被设置为0。
  4. 主机通过发送IGMP报告来响应一个IGMP查询,对每个至少还包含一个进程的组均要发回IGMP报告。

使用这些查询和报告报文,多播路由器对每个接口保持一个表,表中记录接口上至少还包含一个主机的多播组。当路由器收到要转发的多播数据报时,它只将该数据报转发到(使用相应的多播链路层地址)还拥有属于那个组主机的接口上。


参考内容

CCNA图文笔记 OSPF协议详解 QingSword.COM

document/TCPIP详解卷1协议.pdf at master · Aiden-Dong/document · GitHub

本文由作者按照 CC BY 4.0 进行授权

© . 保留部分权利。

本站采用 Jekyll 主题 Chirpy