一、二层基础知识

1.1 vlan介绍

本小节重点:

  • vlan的含义

  • vlan的类型

  • 交换机端口类型

  • vlan的不足

1.1.1 vlan的含义

局域网LAN的发展是VLAN产生的基础,因而先介绍一下局域网LAN

由Hub、网桥或交换机等网络设备连接同一网段内的所有节点形成局域网(LAN),通常是一个单独的广播域

处于同一个局域网LAN之内的网络节点之间可以直接通信

处于不同局域网段的设备之间的通信则必须经过路由器才能通信

上述传统拓扑结构的关键在于用三层设备,即路由器,来隔离不同共的LAN,在网络规模增大的情况下存在两个缺陷:

1.路由器数量需要增多,网络时延随之增加,进而导致网络数据传输速度的下降。这主要是因为数据在从一个局域网传递到另一个局域网时,必须经过路由器的路由操作:路由器根据数据包中的相应信息确定数据包的目标地址,然后再选择合适的路径转发出去。

2.用户是按照它们的物理连接被自然地划分到不同的用户组(广播域)中。这种分割方式并不是根据工作组中所有用户的共同需要和带宽的需求来进行的。因此,尽管不同的工作组或部门对带宽的需求有很大的差异,但它们却被机械地划分到同一个广播域中争用相同的带宽。

综上两点,必须选出一种隔离广播域的方式,兼备下述两点

1.可以不用通过路由器来隔离不同广播域

2.可以突破地理位置的限制,再逻辑上划分出不同的广播域

这就是VLAN,IEEE 802.1Q 标准定义了VLAN Header 的格式。它再普通以太网帧结构的SA(srcaddr)之后加入了4bytes的VLAN Tag/Header 数据,其中包括12-bits的VLAN ID。VLAN ID最大值为4096,但是有效值范围是1-4094

带VLAN的交换机分为两类:

  • Access port:这些端口被打上了VLAN Tag。离开交换机的Access port进入计算机的以太帧中没有VLAN Tag,这意味着连接到access port的机器不会察觉到VLAN的存在。离开计算机进入这些端口的数据帧都被打上了VLAN Tag。

  • Trunk port: 有多个交换机时,组A中的部分机器连接到 switch 1,另一部分机器连接到 switch 2。要使得这些机器能够相互访问,你需要连接两台交换机。 要避免使用一根电缆连接每个 VLAN 的两个端口,我们可以在每个交换机上配置一个 VLAN trunk port。Trunk port 发出和收到的数据包都带有 VLAN header,该 header 表明了该数据包属于那个 VLAN。因此,只需要分别连接两个交换机的一个 trunk port 就可以转发所有的数据包了。通常来讲,只使用 trunk port 连接两个交换机,而不是用来连接机器和交换机,因为机器不想看到它们收到的数据包带有 VLAN Header。

单台交换机上划分VLAN

多态交换机上划分VLAN

1.1.2 vlan的类型

(1)基于端口的VLAN(untagged VLAN - 端口属于一个VLAN,数据帧中中没有VLAN tag)

这种模式下,在交换机上创建若干个VLAN,在将若干端口放在每个VLAN 中。每个端口在某一时刻只能属于一个VLAN。一个 VLAN 可以包含所有端口,或者部分端口。每个端口有个PVID (port VLAN identifier)。这种模式下,一个端口上收到的 frame 是 untagged frame,因此它不包含任何有关 VLAN 的信息。VLAN 的关系只能从端口的 PVID 上看出来。交换机在转发 frame 时,只将它转发到相同 PVID 的端口。

如上图所示,连接两个交换机的同一个 VLAN 中的两个计算机需要通信的话,需要在两个交换机之间连两根线:

  • 一根从 Switch A 端口4 到 Switch B 端口 4 (VLAN 1)

  • 一根从 Switch A 端口8 到 Switch B 端口 8 (VLAN 2)

(2)Tagged VLANs (数据帧中带有 VLAN tag)

这种模式下,frame 的VLAN 关系是它自己携带的信息中保存的,这种信息叫 a tag or tagged header。当交换机收到一个带 VLAN tag 的帧,它只将它转发给具有同样 VID 的端口。一个能够接收或者转发 tagged frame 的端口被称为 a tagged port。所有连接到这种端口的网络设备必须是 802.1Q 协议兼容的。这种设备必须能处理 tagged frame,以及添加 tag 到其转发的 frame。

1.1.3 vlan的不足

  1. VLAN 使用 12-bit 的 VLAN ID,所以 VLAN 的第一个不足之处就是它最多只支持 4096 个 VLAN 网络(当然这还要除去几个预留的),对于大型数据中心的来说,这个数量是远远不够的。

  2. VLAN 是基于 L2 的,所以很难跨越 L2 的边界,在很大程度上限制了网络的灵活性。

  3. VLAN 操作需手工介入较多,这对于管理成千上万台机器的管理员来说是难以接受的。

1.2 二层交换的基础知识

1.2.1 二层交换机最基本的功能

二层交换机最基本的功能包括:

  • mac地址学习:当交换机从它的某个端口收到数据帧时,它将端口的 ID 和帧的源 MAC 地址保存到它的内部MAC表中。这样,当将来它收到一个要转发到该 MAC 地址的帧时,它就知道直接从该端口转发出去了。

  • 数据帧转发:交换机在将从某个端口收到数据帧,再将其从某个端口转发出去之前,它会做一些逻辑判断:

    • 如果帧的目的 MAC 地址是广播或者多播地址的话,将其从交换机的所有端口(除了传入端口)上转发。

    • 如果帧的目的MAC地址在它的内部MAC表中能找到对应的输出端口的话(MAC 地址学习过程中保存的),将其从该端口上转发出去。

    • 对其它情况,将其从交换机的所有端口(除了传入端口)上转发。

  • 加VLAN标签/去VALAN标签:

    • 帧接收:从 trunk port 上收到的数据帧必须是加了标签的。从 access port 上收到的数据帧必须是没有加标签的,否则该帧将会被抛弃。

    • 帧处理:根据上述转发流程决定其发出的端口。

    • 帧发出:从 trunk port 发出的帧是加了标签的。从 access port 上发出的帧必须是没加标签的。

默认情况下,交换机的所有端口都处于VLAN 1 中,也就相当于没有配置 VLAN。该机制说明如下:

  1. PC A 发一个帧到交换机的 1 端口,其目的MAC地址为 PC B 的 MAC。

  2. 交换机比较其目的 MAC 地址和它的内部 MAC Table,发现它不存在(此时表为空)。在决定泛洪之前,它把端口 1 和 PC A 的 MAC 地址存进它的 MAC Table。

  3. 交换机将帧拷贝多份,分别从2和3端口发出。

  4. PC B 收到该帧以后,发现其目的 MAC 地址和他自己的 MAC 地址相同。它发出一个回复帧进入端口3。

  5. 交换机将 PC B 的 MAC地址和端口3 存在它的 MAC 表中。

  6. 因为该帧的目的地址为PC A 的 MAC 地址它已经在 MAC 表中,交换机直接将它转发到端口1,达到PC A。

配置了 VLAN 的交换机的该机制类似,只不过:

(1)MAC 表格中每一行有不同的 VLAN ID。做比较的时候,拿传入帧的目的 MAC 地址和 VLAN ID 和此表中的行数据相比较。如果都相同,则选择其 Ports 作为转发出口端口。

(2)如果没有吻合的表项,则将此帧从所有有同样 VLAN ID 的 Access ports 和 Trunk ports 转发出去。

1.2.2 ARP协议

二层网络使用 MAC (media access control address)地址作为硬件的唯一标识。基于 TCP/IP 协议的软件使用 ARP 来将 IP 地址转化为 MAC 地址。

\1. 目的 IP 地址在同一网段的话

该示例中,Host A 和 B 在同一个网段中。A 的 IP 地址是 10.0.0.99,B 的 IP 地址是 10.0.0.100。当 A 要和 B 通信时,A 需要知道 B 的 MAC 地址。该过程经过以下步骤:

(1)A 上的 IP 协议栈知道通过B 的 IP 地址可以直接到达 B。A 检查它的本地 ARP 缓存来看B 的 MAC 地址是否已经存在。

(2)如果A 没有发现B 的 MAC 地址,它发出一个 ARP 广播请求,来询问“10.0.0.100 的 MAC 地址是什么?”,该数据包:

SRC MAC: A 的 MAC
DST MAC:FF:FF:FF:FF:FF:FF
SRA IP: A 的 IP
DST IP: B 的 IP

(3)该网段中所有的电脑都将收到该包,并且会检查 DST IP 和自己的IP 是否相同。如果不同,则丢弃该包。Host B 发现其IP 地址和 DST IP 相同,它将 A 的 IP/MAP 地址加入到自己的ARP 缓存中。

(4)B 发出一个 ARP 回复消息

SRC MAC: B 的 MAC
DST MAC:A 的 MAC
SRA IP: B 的 IP
DST IP: A 的 IP

(5)交换机直接将该包交给 host A。A 收到后,将 B 的 MAC/IP 地址缓存到 ARP 缓存中。

(6)A 使用 B 的 MAC 作为目的 MAC 地址发出 IP 包。

\2. 目的IP 地址不在同一个网段的话

本例子中,A 的地址是 10.0.0.99, B 的地址是 192.168.0.99。Router 的 interface 1 和 A 在同一个网段,其IP 地址为10.0.0.1;interface 2 和 B 在同一个网段,其IP地址为 192.168.0.1。

A 使用下面的步骤来获取 Router 的 interface 1 的 MAC 地址。

(1)根据其路由表,A 上的 IP 协议知道需要通过它上面配置的 gateway 10.0.0.1 才能到达到 B。经过上面例子中的步骤,A 会得到 10.0.0.1 的 MAC 地址。

(2)当 A 收到 Router interface 1 的 MAC 地址后,A 发出了给B 的数据包:

SRC MAC: A 的 MAC
DST MAC:Router 的 interface 1 的 MAC 地址
SRA IP: A 的 IP
DST IP: B 的 IP

(3)路由器的 interface1 收到该数据包后,根据其路由表,首先经过同样的ARP 过程,路由器根据 B 的 IP 地址通过 ARP 获得其 MAC 地址,然后将包发给它。

SRC MAC: Router interface 2 的 MAC
DST MAC:B 的 MAC
SRA IP: A 的 IP
DST IP: B 的 IP

二、使用OpenvSwitch(OVS)+VLAN组网

Neutron 基于 VLAN 模式的 tenant network 同 provider network 一样,都必须使用物理的 VLAN 网络

2.1 物理VLAN网络配置

本例子中,交换机上划分了三个 VLAN 区域:

  1. 管理网络,用于 OpenStack 节点之间的通信,假设 VLAN ID 范围为 50 - 99.

  2. 数据网络,用于虚拟机之间的通讯。由于Vlan模式下,租户建立的网络都具有独立的 Vlan ID,故需要将连接虚机的服务器的交换机端口设置为 Trunk 模式,并且设置所允许的 VLAN ID 范围,比如 100~300。

  3. 外部网络,用于连接外部网络。加上 VLAN ID 范围为 1000-1010。

关于网段之间的路由:

  • 如果该物理交换机接到一个物理路由器并做相应的配置,则数据网络可以使用这个物理路由器,而不需要使用 Neutron 的虚拟路由器。

  • 如果不使用物理的路由器,可以在网络节点上配置虚拟路由器。

2.2 Neutron配置

2.2.1 配置进行

控制节点上:
# vim /etc/neutron/plugins/ml2/ml2_conf.ini
[ml2]
type_drivers = flat,vlan tenant_network_types = vlanmechanism_drivers = openvswitch
[ml2_type_flat]
flat_networks = external
[ml2_type_vlan]
network_vlan_ranges = physnet1:100:300

网络节点上:

#为连接物理交换机的网卡 eth2 和 eth3 建立 OVS physical bridge,其中,eth2 用于数据网络,eth3 用于外部网络
ovs-vsctl add-br br-eth2ovs-vsctl add-br br-ex
ovs-vsctl add-port br-eth2 eth2ovs-vsctl add-port br-ex eth3

# vim /etc/neutron/plugins/ml2/ml2_conf.ini [m12]
type_drivers = flat,vlantenant_network_types = vlanmechanism_drivers = openvswitch[ml2_type_flat]flat_networks = external
[ml2_type_vlan]
network_vlan_ranges = physnet1:100:300,external:1000:1010

[ovs]
bridge_mappings = physnet1:br-eth2,external:br-ex

计算节点上:

#为连接物理交换机的网卡 eth2 建立 OVS physical bridge
ovs-vsctl add-br br-eth2
ovs-vsctl add-port br-eth2 eth2

# vim /etc/neutron/plugins/ml2/ml2_conf.ini [m12]
type_drivers = vlantenant_network_types = vlanmechanism_drivers = openvswitch
[ml2_type_vlan]
network_vlan_ranges = physnet1:100:300

[ovs]
bridge_mappings = physnet1:br-eth2

注意:

  • network_vlan_ranges 中的 VLAN ID 必须和物理交换机上的 VLAN ID 区间一致。

  • bridge_mappings 中所指定的 bridge 需要和在个节点上手工创建的 OVS bridge 一致。

然后重启相应的 Neutron 服务。

2.2.2 配置生效过程

当 Neutron L2 Agent (OVS Agent 或者 Linux Bridge agent)在计算和网络节点上启动时,它会根据各种配置在节点上创建各种 bridge。以 OVS Agent 为例,

(1)创建 intergration brige(默认是 br-int);如果 enable_tunneling = true 的话,创建 tunnel bridge (默认是 br-tun)。

(2)根据 bridge_mappings,配置每一个 VLAN 和 Flat 网络使用的 physical network interface 对应的预先创建的 OVS bridge。

(3)所有虚机的 VIF 都是连接到 integration bridge。同一个虚拟网络上的 VM VIF 共享一个本地 VLAN (local VLAN)。Local VLAN ID 被映射到虚拟网络对应的物理网络的 segmentation_id。

(4)对于 GRE 类型的虚拟网络,使用 LSI (Logical Switch identifier)来区分隧道(tunnel)内的租户网络流量(tenant traffic)。这个隧道的两端都是每个物理服务器上的 tunneling bridge。使用 Patch port 来将 br-int 和 br-tun 连接起来。

(5)对于每一个 VLAN 或者 Flat 类型的网络,使用一个 veth 或者一个 patch port 对来连接 br-int 和物理网桥,以及增加 flow rules等。

(6)最后,Neutron L2 Agent 启动后会运行一个RPC循环任务来处理 端口添加、删除和修改。管理员可以通过配置项 polling_interval 指定该 RPC 循环任务的执行间隔,默认为2秒。

2.3 创建虚拟网络和子网

2.3.1 创建命令

s1@controller:~$ neutron net-create net1 (或者 Admin 用户运行 neutron net-create net1 --provider:network_type vlan --provider:physical_network physnet1 --provider:segmentation_id 101。效果相同)
Created a new network:
+---------------------------+--------------------------------------+
| Field                     | Value                                |
+---------------------------+--------------------------------------+
| admin_state_up            | True                                 |
| id                        | dfc74f44-a9f2-4497-a53d-1723804a49a8 |
| name                      | net1                                  |
| provider:network_type     | vlan                                 |
| provider:physical_network | physnet1                             |
| provider:segmentation_id  | 101                                  |
| router:external           | False                                |
| shared                    | False                                |
| status                    | ACTIVE                               |
| subnets                   |                                      |
| tenant_id                 | 74c8ada23a3449f888d9e19b76d13aab     |
+---------------------------+--------------------------------------+  
s1@controller:~$ neutron subnet-create subnet1 10.0.0.0/24 --name net1

2.3.2 Neutron代码实现

做完以上的步骤之后,用户就可以在 subnet 上 boot 虚机了。

boot 虚机的过程中,Nova 依次会:

(1)调用 Neutron REST API 申请一个或者多个 port。Neutron 会根据数据库中的配置来进行分配。

(2)在计算节点上,Nova 调用 ovs-vsctl 命令将虚机的 VIF 被 plug 到 br-int 上。

(3)启动虚机。

Neutron L2 Agent 的循环任务每隔两秒会依次:

(1)调用 ”ovs-vsctl list-ports“ 命令获取到 br-int 上的 port,再根据上次保存的历史数据,生成所有变更端口的列表(包括添加的、更新的、删除的端口)。比如:

{'current': set([u'04646b21-78a0-429e-85be-3167042b77be', u'592740b0-0768-4e57-870d-6495e6c22135']), 'removed': set([]), 'added': set([u'04646b21-78a0-429e-85be-3167042b77be', u'592740b0-0768-4e57-870d-6495e6c22135'])}

(2)为每一个待处理端口,根据其 ID 从 DB 中取得其详细信息。比如:

{u'profile': {}, u'admin_state_up': True, u'network_id': u'e2022937-ec2a-467a-8cf1-f642a3f777b6', u'segmentation_id': 4, u'device_owner': u'compute:nova', u'physical_network': phynet1, u'mac_address': u'fa:16:3e:fd:ed:22', u'device': u'592740b0-0768-4e57-870d-6495e6c22135', u'port_id': u'592740b0-0768-4e57-870d-6495e6c22135', u'fixed_ips': [{u'subnet_id': u'13888749-12b3-462e-9afe-c527bd0a297e', u'ip_address': u'91.1.180.4'}], u'network_type': u'vlan'}

(3)针对每一个增加或者变更的 port,设置 local VLAN Tag;调用 ”ovs-ofctl mod-flows “ 命令来设置 br-tun 或者 物理 bridge 的 flow rules;并设置 db 中其状态为 up。

(4)针对每一个被删除的 port,设置 db 中其状态为 down。

2.4 Neutron虚拟网络

1)一个计算节点上的网络实例

它反映的网络配置如下:

  1. Neutron 使用 Open vSiwtch。

  2. 一台物理服务器,网卡 eth1 接入物理交换机,预先配置了网桥 br-eth1。

  3. 创建了两个 neutron VLAN network,分别使用 VLAN ID 101 和 102。

  4. 该服务器上运行三个虚机,虚机1 和 2 分别有一个网卡接入 network 1;虚机2 和 3 分别有一个网卡接入 network 2.

Neutron在该计算节点上做的事情:

  创建了OVS Integration bridge br-int。它的四个Access口中,两个打上了内部vlan Tag1,连接接入network1的两个网卡;另外两个端口打上的是vlan tag 2

  创建一对patch port连接br-int和br-eth1

  设置br-int中的flow rules。对从access ports进入的数据帧,加上相应的vlan tag,转发到patch port;从patch port进入的数据帧,将vlan id 101修改为1,102修改为2,再转发到相应的access ports

  设置br-eth1中的flow rules。从patch port进入的数据帧,将内部vlan id 1修改为101,内部vlan id 2修改为102,再从eth1端口发出。对从eht1进入的数据帧做相反的处理

(2)再加上另一个连接到同一个物理交换机的服务器(加上 neutron 网络使用的 VLAN ID 为 100,物理 brige 为 br-eth0):

Neutron 实现了基于物理 VLAN 交换机的跨物理服务器二层虚拟网络。

(3)连接到同一物理交换机的网络节点的情况

(4)网络流向

  • 不同物理服务器上的虚机,如果 VM1 和 VM2 属于同一个 tenant network 的同一个subnet,那么两者的通信直接经过 物理交换机 进行,不需要做到网络节点。如图10 所示。

  • 相同物理服务器上的虚机,如果 VM1 和 VM2 属于同一个 tenant network 的同一个subnet,那么两者的通信直接经过 br-int 进行。

对其他虚机之间数据交换情形,都算作跨子网的数据流向,都需要经过网络节点中的 Router 进行 IP 包的路由。(也可以直接使用连接物理交换机的物理路由器)。

Linux基础:VLAN篇的更多相关文章

  1. Linux 基础学习篇 序篇

    读序篇可以知道的: 1.有些指令知道前和知道后,自己的操作是完全不同的,可能知道前,会用reset把系统重新启动一遍,而知道后会使用ps和kill来关闭进程. 2.如果对Linus的学习知识" ...

  2. linux基础一篇就够了

    Linux学习笔记 粗略笔记第一版,全文约2000行50000字 1. 时间和日历 date:查看当前时间 cal:查看当月日历 cal 2018:查看年日历 cal 10 2018:指定某年某月日历 ...

  3. Linux 基础学习篇笔记 Linux基础知识

    哎呀,翻到第一篇,映出眼帘的标题:从Unix到Linux(我就知道学习不能急,不能像我,看个简介,就赶忙去查了,原来作者在这里给出详细的介绍了) 1.1根据书上写的,原来linux的内核是被Linus ...

  4. linux基础-安装篇

    linux现在的版本已经升级到7.x的版本了,现在虽然企业还是使用6.x的版本,但是未来的趋势肯定是往7的方向发展的,很多的培训机构或者一些学校的课程也都更新的7的版本进行教学,下面的安装实例就是以7 ...

  5. Linux 基础——开山篇

    为什么要开始学习Linux命令? 首先当然是因为工作需要了,现在的工作是负责银行调度的系统的源系统接入的工作,经常要到生产部署版本.所以……买了一本<Linux命令行与shell脚本编程大全&g ...

  6. linux基础 作业篇

    1.自动部署反向代理 web nfs #!/usr/bin/python #-*- coding:utf-8 -*- #开发脚本自动部署及监控 #1.编写脚本自动部署反向代理.web.nfs: #!/ ...

  7. Docker学习--Linux基础准备篇

    1.docker命令不需要附带敲sudo的解决办法 由于docker daemon需要绑定到主机的Unix socket而不是普通的TCP端口,而Unix socket的属主为root用户,所以其他用 ...

  8. 第一天 Linux基础篇

    课程介绍 1.认识Linux的不同版本 2.以及应用领域 3.文件和目录 4.Linux命令概述 5.Linux命令-文件 6.Linux命令-系统管理-磁盘管理 认识Linux 什么是操作系统  生 ...

  9. 鸟哥Linux私房菜基础学习篇学习笔记3

    鸟哥Linux私房菜基础学习篇学习笔记3 第十二章 正则表达式与文件格式化处理: 正则表达式(Regular Expression) 是通过一些特殊字符的排列,用以查找.删除.替换一行或多行文字字符: ...

  10. 鸟哥Linux私房菜基础学习篇学习笔记2

    鸟哥Linux私房菜基础学习篇学习笔记2 第九章 文件与文件系统的压缩打包: Linux下的扩展名没有什么特殊的意义,仅为了方便记忆. 压缩文件的扩展名一般为: *.tar, *.tar.gz, *. ...

随机推荐

  1. ModelForm has no model class specified

    未指定模型类,错误发生在把model拼写错误 来自为知笔记(Wiz)

  2. sqlserver - 某字段数据为json串, 获取该json串里的值 的详细方法

    1.前言 某字段的数据为json 但是我想只获取里面的某一个值,该怎么操作? 2.笔记 (1)用 JSON_VALUE(参数1,参数2)函数 ,有两个参数, (2)参数1 为 列名 ,参数2 为 js ...

  3. Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256M; support was removed in 8.0

    目录 启动一个Java Standalone程序时报错 解决办法 解释 参考 启动一个Java Standalone程序时报错 Java HotSpot(TM) 64-Bit Server VM wa ...

  4. 关于 用 js 实现 快照功能

    1.前言 前段时间有个需求,想要 打印一个小票凭证 ,实现这个功能,我首先想到了快照, 就是将数据内容排版好,然后截图或者用其他方式将内容 制作成图片 ,然后下载下来打印即可. 2.探讨 为何不直接以 ...

  5. gradle学习(一)

    projects和tasks 任何一个Gradle构建都是由一个或者多个project组成 每个project都有多个tasks构成 每个task都代表了构建执行过程中的一个原子性操作.例如 编译 打 ...

  6. vue3.0+vite+ts项目搭建-分环境打包(四)

    分环境打包配置 新建.env.dev(或者.env) VITE_NODE_ENV = 'dev' VITE_HOST = 'http://local.host.com' 执行yarn dev ,控制台 ...

  7. idea 个人settings和好看的主题推荐

    idea  个人settings和好看的主题推荐 配置和主体搭配使用,效果最佳!!! 配置文件: 链接:https://pan.baidu.com/s/1K-oW9UNxUz_5XWz4Ru3_3w  ...

  8. 【重构前端知识体系之HTML】讲讲对HTML5的一大特性——语义化的理解

    [重构前端知识体系之HTML]讲讲对HTML5的一大特性--语义化的理解 引言 在讲什么是语义化之前,先看看语义化的背景. 在之前的文章中提到HTML最重要的特性,那就是标签.但是项目一大,标签多的看 ...

  9. SQL查询字段,起别名,列参与数学运算

    13.简单查询 13.1.查询一个字段? select 字段名 from 表名: 其中要注意: select和from都是关键字 字段名和表名都是标识符. 强调: 对于SQL语句说,是通用的 所有的S ...

  10. Python 使用 Windows10 桌面通知

    前言 Win10 没有提供简单命令行方式来触发桌面通知,所以使用 Python 来写通知脚本. 一番搜索,找到 win10toast .但这开源仓库已无人维护,通过 github fork 的关系图, ...