幻影彭的彩虹

记录青春的扇区

在无数久的 🐦咕咕咕 后一个博客它建成了!

在无数久的 🐦咕咕咕 后这个博客它复活了!

博客主要会收录这些东西:

  • OI/ACM 相关

    • 考试技巧
    • 题解
    • 算法理解
  • 学习笔记

  • 面向各种人群的科普

  • 我的开源项目

  • 生活中有趣的事

  • 一些奇思妙想

我是谁:

  • 如果你线下认识我,可以叫我 "毛Ker" 或者 "老毛"
  • 如果你线上认识我,可以叫我 "彭彭"。
  • 这些 ID 都是我:huan-yp幻影彭huan_yphuan_yp2002

联系我:

  • QQ:3051561876

背景

被班主任 push,然后自己也有点想法,想搞一次班级团建活动。当班长一年多了,前面两次集体活动其实不太成功,这次想好好筹备一下。

调研阶段

准备主要是三个点:吃、玩、交通。这三个点都需要围绕 "去哪儿" 展开。确定好 "去哪儿",其它都不是大问题。

先期策划

首先决定玩什么以及去哪儿玩,南京这块我有点抓瞎,问了 Kimi,给了轰趴馆、游船、滑雪馆、围炉煮茶四个建议。我花了一个小时先研究了下初步可行性。

基本没问题,然后组织班委开了个小会,讨论了下,排除了滑雪馆和围炉煮茶。安排几位班委下去进一步调研。

调研结果显示划船不太合适,最终选了轰趴馆。

和班主任沟通

我们做出来的初步策划案人均是 200 元。有一说一,从我角度看,我不觉得贵。然后和班主任沟通了一下,被批评了一顿。然后换个角度想想,对参与的同学来说,花 200 块和一段固定时间参与一个意义不太明确的活动确实不合理。

班主任觉得围炉煮茶的的方案其实不错,我思考了一下,确实,不一定非得 "围炉煮茶",简单把人聚起来也行。

去哪儿

我询问 AI,从策划组那里征集意见,得知有 "矿坑公园"、"龙山赏心谷" 等选项,去百度地图上查到了更多选项。考虑到距离和交通便利性,最终选定了 "龙山赏心谷"。

玩什么

这块直接从策划组里拉了一组人,让他们去想。想了一些集体活动,又计划购买一些桌游、飞盘等。

吃什么

同理,从策划组里拉一组人,让他们去想,然后班级投票表决。最后确定是烧烤。

理论更优解

  • 调研阶段可以欢迎同学来提大概想法(地点、吃啥、玩啥),把积极提想法的同学拉进策划组。
  • 围绕各个地点各自研究吃啥玩啥(简单看看理论是否可行即可)。
  • 把调研结果拉出来设计一个多选投票(让参与者有选择权)。

操作建议

  • 最好有多个人承担 Leader 的角色,多个人的看法会更全面,但是务必要有一个主导来最终拍板。
  • Leaders 需要把策划组拉起来,然后推进好调研工作,促进生成各种意见和方案。然后按照自己的想法初步看看各个方案的可行性。避免出现 "想法很好但是不可行"。因为参与投票的同学没有全面的信息,尤其是交通问题。
  • 多和值得信赖的第三者沟通方案,例如班主任。
  • 研究理论可行性时应该多关注有关社交媒体上的信息,例如百度地图,大众点评,美团等,多找照片和攻略参考。
  • 投票时关于每个地点应该给一些预览图

一些体会

  • 对于活动组织者来说,必须要站在参与者的角度去想想。参与活动本来就是有时间成本的,又要花钱。如果活动本身比较单调,参与者的积极性就会降低。
    • 其实我个人作为参与者来说,科协赛事部的聚餐如果不是 hhy 和 dyx 也要去的话,我个人不太愿意去的。毕竟也真的不能说关系特别好,而且人均 100 的餐饮消费,加上 KTV 和来回车费,成本大概 200,还得考虑时间成本。
  • 一般来说,考虑我所在高校和专业学生的家庭情况,总体费用控制在 100 元以内应该比较合理。
    • 所在高校层次和家庭经济情况有一定的正相关关系。
  • 多和不同的人沟通,听取不同的意见。尤其要注意吸收反对的建议,合理设计交流方式以便能够收到不懂意见
    • 例如作为 leader 时,尽量让 collaborator 来做具体细节,这样 leader 可以看到 collaborator 的想法,至少能有两个人提意见,而不是 leader 提出,collaborator 附和。
  • 各种问题可以多考虑 AI 的意见(让它提出不足然后自己思考)。AI 是一个靠谱的 collaborator
  • 多个 Leader 这个事情,必须是要主导者对其它 Leader 有相当的信任才能行,所有人都靠谱,才能推进好活动。

准备阶段

沟通场地商家

从百度地图上找到了龙山赏心谷的联系方式,打电话沟通了下,后续交给组织委员来继续协商细节。报价 850 元。

沟通食材商家

考虑到烧烤食材需要解冻,且保质期较短。网购没有地方保存。问 AI 之后得知有 "美团买菜" 功能。完美解决,商家表示可以定时解冻送货。

采购

  • 零食:10r/人,散装+袋装组合。
  • 饮料:6r/人,纯净水按件买,饮料买大瓶+一次性纸杯。
  • 户外照明:实际上买了 2+3=5 个户外露营灯。
  • 其它器材:剪刀、A4 纸。

交通安排

30 个人,还是包车吧。问了辅导员,学校有车队,可以包车,非常方便。

报销准备

和班主任沟通,表示可以报销车费,支持一部分活动物资费用。

实际操作建议

  • 和一个商家沟通时,只经由一个人来主要沟通(一定要找沉稳的同学来对外)。
  • 由一位同学负责所有的付费,让沟通的同学让商家发付款方式,实际付费由财务负责。
  • 如果不熟悉商家,订金不宜超过 25%(车队是学校的车队,值得信任)。

一些体会

  • 30 个人的团建,吃、玩都是比较大的项目,商家这边肯定会比较主动配合的,无需太担心。
    • 例如场地商家和我们沟通时实际上比较好说话,合理的诉求都能得到满足
    • 例如食材商家能够为我们安排送货到场地
  • 一定一定要去主动争取优惠,尤其是一些模棱两可的东西,例如场地按人报价,团体报的时候可以适当少报一点人数(例如 30 人报 25 人),压力商家不要报太高的价格。 这个一定要控制好,如果价格压的太狠,商家会在一些服务上打折。
  • 和班主任沟通的时候我态度有点差,深刻检讨了。主要原因还是信息差,班主任没有充分了解活动情况,我也没有充分说明,产生了一些想法上的冲突。 实际上无论如何和上级领导沟通时态度要稍微好点,抬头不见低头见。
  • 分工的时候小组各自有对应的负责人,这个负责人要找擅长组织和沟通的同学来,不要用错人

实施阶段

时间、通知和确认

  • 众所周知,当代大学生比较原子化,通知和确认是比较困难的事。
  • 确定时间实质上是第一次确认、"确认" 是最终确认。
  • 确定时间这块,一般要提前五天左右完成,确认完时间后开始通知,活动前一天完成最终确认过程。

调查时间

这块可以安排两位同学来研究,然后 Leaders 确认时间安排。

  • 需要避开学院、学校的固定活动。
  • 需要避开大概率会有私事的时间(比如我们这边的周三下午因为固定没课所以很多组织都会安排上一定日程)
  • 户外情况,必须避开雨天。

理论上的确定时间

  • 调查好可选时间,在班级群里发多选投票,私戳没有投票的同学投票,要求务必多选。
  • 根据结果确定一个初步时间段,私戳没有选这个同学,询问原因。
  • 根据反馈调整,达到预期参与率后确定最终时间。
实际情况
  • 调研没做好,时间确定不合理。然后某天我突然意识到一个大概率可行的时间。
  • 班长和副班长各自私戳了一半的同学确认这个时间没问题。
  • 班级群里发接龙要求最终确认,私戳没有确认的同学了解情况。

通知和确认

  • 确定时间后,班级群里发通知和确认接龙。
  • 跟进没有确认的同学,鼓励参与。

一些操作上的细节

  • 对于参与的同学来说,一般只有两个过程,投票时间和确认参加,心智负担较低。但是如果出现需要沟通的情况,可能会比较麻烦,建议是沟通后由组织者来直接改统计情况而不要麻烦参与者二次操作
  • Leader 理论上需要关注的细节:
    • 时间安排的调研结果
    • 根据反馈后确定的最终时间

集中

利用天然的上下课时机把人集中起来,正常集中会比较困难。

实际活动过程

  • 上车,意识到饮料不够,叫了美团外卖补充了一些。
  • 下车先组织拍照。
  • 和商家稍微沟通一下,组织者稍稍鼓励带动一下,自发组织起来开始玩。
  • 烧烤
  • 收拾场地

一些体会

  • 对于户外活动来说,饮品一定要充分准备(按平均一人 500ml 纯净水、500ml 饮料计算即可),我们这次出现了饮品不足的情况,联系了外卖店家补充。
  • 把人聚集起来是比较困难的,我这边是采用 "下课后别走,直接集合上车" 的策略,效果非常不错。
  • 户外活动务必考虑日落时间和照明情况
  • 实际上户外活动只要到了地方,同学们都有足够的自发组织能力,需要准备好器材物资。集体活动的话,除非特别有意义,否则还是算了吧。

收尾阶段

汇总照片

其它心理准备

  • 最主要的组织者做好经济兜底的准备,学院的报销可能会有问题。
  • 做好临时应变的准备,例如天气变化。
  • 保持一个良好的心态。

前情提要

发生该事故后,NcatBot 为了避免被卷入有关风波,进行了一些紧急操作以避险。由于是首次遇到此类事件,应对经验不足,导致丢失了约 6 h 的工作代码,同时也切实反映出数据安全的重要性。

本文就 321 原则,提供一个 Windows 操作系统下简易但切实可行的数据安全方案。

321 原则:三份数据、两块介质、一处异地,少一个都可能翻车!

321 原则是一种广泛应用于数据备份和数据安全领域的最佳实践。其核心思想是:

  • 3:至少保留 3 份数据副本(包括原始数据和备份)。
  • 2:将数据存储在 2 种不同的介质上(如硬盘、移动硬盘、云存储等)。
  • 1:至少有 1 份备份存放在异地(如云端或物理隔离的地点)。

通过遵循 321 原则,即使遇到硬件故障、误操作、自然灾害或勒索软件攻击等突发事件,也能最大程度地保障数据的完整性和可恢复性。这一原则简单易行,适用于个人用户、小型团队以及企业级的数据安全需求。

情况简析

对于个人开发者来说,三种情况的概率排序为 误操作 >> 硬件故障 > 其他所有情况。如果是开源代码,那么数据的 safety(数据完整性与可恢复性)要求是远大于 security(数据保密性与防止未授权访问)要求的。

对于开发者来说,数据风险主要在于劳动成果的损失,基于这个原则,我设计了以下的备份方案。

第一道防线:VS Code 的 Local-History,秒级回血

Local-History 是 Visual Studio Code 编辑器的一个插件,它能够自动记录你对文件的每次修改,形成本地的版本历史。这是防范误操作的第一道防线,简单粗暴但非常有效。

插件简介

10 s 即可快速完成安装,极大提升代码安全性。在 VSCode 中搜索并安装 "Local History" 插件即可。

每当你阶段性的需要保存文件时(按下 ctrl+s),插件会自动拷贝一个带时间戳的副本存储在本地,简单粗暴。

这种方式虽然简单,但对于日常的误删、误改等操作具有极强的防护作用。尤其是进行 git 有关的操作时。

最佳实践

  • 🚨 需要将 .history 加入 .gitignore

  • 建议 ctrl+, 找到 saveDelay 设置为 15 s 或者 30 s(连续多长时间无更改就保存),提高空间效率。

第二道防线:Restic 快照,防的是‘整盘蒸发’

有时候脑抽达到了一种地步————把整个项目文件夹都给扬了,这时候 Local History 就有些回天乏术了。

再或者,哪天物理机器突然抽风坏掉了,那又咋办?

我们需要一个更好的备份操作。

情况介绍

为了防止误删,我们需要在别处完整的建立一个备份区域。这个备份区域是不能采用 "同步" 策略的,而应该采用 "快照" 策略。

同步和快照分别是什么?
image-20250906190851595
image-20250906190904014

由于我们能够接受一定程度的数据损失(例如 1 h 的工作成果损失),所以采用时效性稍差的 "快照" 仍然可以有效的解决问题。

特别的,为了防止单盘损坏,我们更需要在与系统盘所在物理硬盘不同的另一个盘上做备份。这是所谓的两种介质

另外,为了防止极端情况发生,我们还需要一份异地。这里我采用了 SMA 共享文件夹方案,这种方案仍然可以用 restic 实现。

操作过程

设置工作区变量

🚨 首先设置工作区目录变量,方便后续使用:

1
2
3
4
5
6
7
8
9
10
# 设置工具安装目录变量
$TOOLS_DIR = "C:\tools"
# 设置本地仓库目录变量(先创建对应目录)
$BACKUP_DIR_LOCAL = "D:\restic\MyRepo"
# 设置远程备份目录变量(先创建对应目录)
$BACKUP_DIR_REMOTE = "\\192.168.1.111\share\MyRepo"
# 设置密码(请牢记)
$PASSWORD = "YourStrongPassword"
# 目标目录(写绝对路径)
$TARGET_DIR = "C:/Users/yourname/Desktop/Proj"

下载有关软件

1
2
3
4
5
6
mkdir $TOOLS_DIR
Invoke-WebRequest -Uri https://ghfast.top/https://github.com/restic/restic/releases/download/v0.17.1/restic_0.17.1_windows_amd64.zip ` -OutFile "$TOOLS_DIR\restic.zip"
Expand-Archive "$TOOLS_DIR\restic.zip" -DestinationPath $TOOLS_DIR
Rename-Item "$TOOLS_DIR\restic_0.17.1_windows_amd64.exe" -NewName "restic.exe"
Remove-Item "$TOOLS_DIR\restic.zip"
setx PATH "%PATH%;$TOOLS_DIR" # 永久加入 PATH

初始化有关仓库

1
2
3
4
5
6
7
cd $TOOLS_DIR
$Env:RESTIC_REPOSITORY_LOCAL = $BACKUP_DIR_LOCAL
$Env:RESTIC_PASSWORD = $PASSWORD # 仓库密码
restic init
$Env:RESTIC_REPOSITORY_REMOTE = $BACKUP_DIR_REMOTE
$Env:RESTIC_PASSWORD = $PASSWORD # 仓库密码
restic init

写入密码

1
cat $PASSWORD | Out-File -FilePath "$TOOLS_DIR\restic_password.txt" -Encoding ASCII

备份脚本

将脚本内容写入 $TOOLS_DIR 下的 backup.ps1

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@"
param(
[string]`$Repo,
[string]`$Pass
)
`$Env:RESTIC_REPOSITORY = `$Repo
`$Env:RESTIC_PASSWORD = `$Pass
# 实际备份
restic backup ``
--tag hourly ``
--exclude-caches ``
--exclude "*.log" ``
--exclude "node_modules" ``
$TARGET_DIR
# 清理旧快照
restic forget --prune ``
--keep-hourly 72 --keep-daily 30 --keep-weekly 16
"@ | Out-File -FilePath "$TOOLS_DIR\backup.ps1" -Encoding UTF8

设置 Windows 定时任务

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 本地备份任务 - 每小时
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 `
-Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD"
Register-ScheduledTask -TaskName "ResticLocal" -Trigger $trigger -Action $action `
-User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries)

# 2. NAS备份任务 - 每小时(错开 15 min 减轻网络突刺)
$trigger2 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(15) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration (New-TimeSpan -Days 3650)
$action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\backup.ps1 `
-Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD"
Register-ScheduledTask -TaskName "ResticNas" -Trigger $trigger2 -Action $action2 `
-User "$env:USERNAME" -Settings (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable)

再一道保险

需要保证备份程序的正确设置。

试运行备份

创建一个测试文件:

1
New-Item -Path $TARGET_DIR\test.txt -ItemType File

运行备份:

  • 打开 "任务计划程序",找到 "ResticLocal" 和 "ResticNas" 任务,点击运行。

  • 终端运行以下命令检查备份完整性:

1
2
echo $PASSWORD | restic snapshots -r $BACKUP_DIR_LOCAL
echo $PASSWORD | restic snapshots -r $BACKUP_DIR_REMOTE
  • 如果均显示了一张包含了备份时间、备份目录的表,则备份成功。

定期检查备份完整性

设置脚本:

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@'
param(
[string]$Repo,
[string]$Pass
)

$Env:RESTIC_REPOSITORY = $Repo
$Env:RESTIC_PASSWORD = $Pass

function Write-EventLogError {
param($Message)
Write-Error $Message
New-EventLog -LogName Application -Source "ResticVerify" -ErrorAction SilentlyContinue
Write-EventLog -LogName Application -Source "ResticVerify" -EntryType Error -EventId 1 -Message $Message
}

function Show-ToastNotification {
param(
[string]$Title,
[string]$Message
)

# 方法2: 系统托盘气球
Add-Type -AssemblyName System.Windows.Forms
$notify = New-Object System.Windows.Forms.NotifyIcon
$notify.Icon = [System.Drawing.SystemIcons]::Information
$notify.BalloonTipIcon = "Error"
$notify.BalloonTipTitle = $Title
$notify.BalloonTipText = $Message
$notify.Visible = $true
$notify.ShowBalloonTip(5000)
Start-Sleep -Seconds 6
$notify.Dispose()
}

# 统一错误处理函数
function Invoke-ResticCheck {
param(
[scriptblock]$Command,
[string]$StepName
)
try {
Write-Output "执行步骤: $StepName"
& $Command
if ($LASTEXITCODE -ne 0) { throw "restic 返回非零退出码 $LASTEXITCODE" }
}
catch {
$errMsg = "$StepName 失败: $($_.Exception.Message)"
Show-ToastNotification -Title "备份验证失败" -Message $errMsg
Write-EventLogError $errMsg
exit 1
}
}

# 1. 基础 check
Invoke-ResticCheck -Command { restic check } -StepName "restic check"

# 2. 抽检 10%
Invoke-ResticCheck -Command { restic check --read-data-subset=10% } -StepName "restic check --read-data-subset=10%"

Write-Output "TestPass"
'@ | Out-File -FilePath "$TOOLS_DIR\verify.ps1"

设置定时任务:

展开即可抄作业
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 本地仓库验证任务
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 04:30
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 `
-Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD"
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "ResticVerifyLocal" -Trigger $trigger -Action $action -Settings $settings -User $env:USERNAME

# NAS仓库验证任务(错开 30 min 执行)
$trigger2 = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 05:00
$action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File $TOOLS_DIR\verify.ps1 `
-Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD"
$settings2 = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "ResticVerifyNas" -Trigger $trigger2 -Action $action2 -Settings $settings2 -User $env:USERNAME

试运行验证

1
2
PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_LOCAL -Pass $PASSWORD
PowerShell.exe -File $TOOLS_DIR\verify.ps1 -Repo $BACKUP_DIR_REMOTE -Pass $PASSWORD
  • 如果显示 "TestPass",则验证成功。

总结

Why 定期验证

定期验证的目的是确保备份的完整性,事实上,备份由于偶然因素损坏的概率和硬件损坏的概率大致相当,定期验证可以及时发现备份损坏的问题。

即使备份发生损坏,同时发生主数据损坏的概率极低。只需要重新修复备份即可。

该方案的优点

  • 基本践行 321 原则:
    • Local-History 作为第一道防线,能够秒级恢复工作区数据。
    • Restic 作为第二和第三道防线,完成两种介质一份异地备份。
  • 一次配置,永久使用,备份操作无需任何手动介入。

该方案的不足

  • 密钥相关:
    • 🚨 没有提供密钥备份方案,需要自行多端备份恢复密钥。
    • 🚨 密钥明文存储,对于勒索性丢失有一定风险。
  • 🚨 需要自行配置一个支持 SMB 的 NAS,并且需要确保 NAS 的稳定性。
  • 备份触发时无法做到完全无感,会打断工作聚焦。
  • 告警机制不够完善,Windows 弹窗日志偶尔会被忽略,可以考虑发送邮件等更加完善的方式。

网络安全绝非儿戏!!!

事件简述

参考资料

2025年9月5日晚,公网上大量未配置访问令牌(token)的 OneBot 服务被攻击者批量调用,诱导 QQ 机器人发布不当内容,导致众多 Bot 账号和群聊被平台封禁。由于 NapCat 框架默认把服务监听在 0.0.0.0 且用户多为新手,未设 token 的实例最多,因此成为“重灾区”。事件暴露出 OneBot 协议“token 可选”以及部分框架默认配置过于宽松的安全缺陷。也提醒开发者必须把安全性置于易用性之前,强制或显著提示用户完成最小化安全配置。

NcatBot 本体相关代码审计

附带启动相关安全隐患

  • 沿用了 NapCat 的 WebUI 弱 Token,没有做强制修改。此次攻击主要针对 OneBot 端口,没有针对 NapCat WebUI 的入侵,故 NcatBot 系列软件幸免遇难。
  • 默认采用无 Token、强制监听本地的策略,但是仍然可以通过设置来监听 0.0.0.0。幸而能够正常查看文档获知 ws_listen_ip 的用户都具备一定网络安全意识,采用了强密码或者严格防火墙。强制监听本地策略是本次 NcatBot 用户群体免受攻击的主要原因

文档相关安全隐患

  • Linux 安装部分的文档中,直接不加风险说明的详细指导了用户开放防火墙端口,对于部分新手用户,存在极高的被攻击隐患。
  • 远端模式的文档中没有说明潜在的安全隐患,文档编写经验不足。

预期改进

  • 增强对启用远端模式的限制,对于有隐患的操作提高操作门槛。
  • 本地模式严格检查 WebUI 和 Websockets 的安全状态。
  • 文档设计充分考虑安全因素,避免有隐患的引导。

启发

网络安全不是儿戏,作为开源软件开发者,尤其是涉及到开放公网端口的软件开发,一定要认真审计代码中的潜在风险。不要把安全托付给用户的认知

当易用性和安全性要做出 TradeOff 时,安全性是绝对不可牺牲的;法律风险比用户减少问题大得多。

多听从社区的安全建议,不要固执己见;没有不值得利用的漏洞,只有你没想到的作用

网络安全就在身边,不要把 "小软件"、"个人配置" 的网络安全不当回事,明知有风险时不可抱有侥幸心理;来自公网的恶意无处不在,在 NAT 后生活了十几年的我们在初期也许还不能完全意识到这种恶意,那就让这次经历成为一次宝贵的教训。

守护网络安全,从你我做起。

谈谈操作日志

最近手上有好几项工作,都感觉是需要写日志的,随便聊聊吧。

哪些工作要写日志

  • 以后可能还需要重复操作,流程相对复杂的工作。

    • 我配置了一个 openwrt 的路由器,刷入 openwrt 的流程其实挺繁琐的,我自己查阅了很多资料才解决。此外,有些资源其实在网上已经很难获取(例如原版的 clash 内核,对应的预编译固件)我对这些资源做了留档,并在日志中记录了留档的位置。

    • 计软智学院赛事部,一些自动化脚本的操作文档。之前一直是手工处理,通过加人来减少单人的工作负担。我写好了 Python 脚本。由于又涉及到和 SEUOJ 数据库的交互,实际上走完一遍流程的耗时不算短(10min)左右,这种事情是间隔几个月来一次,需要写日志来告诉自己怎么做,避免多次阅读源代码

    这些日志应该在第一次操作时就写好,写的越早,节省的时间越多。此外,留档的话题是之后的事,我可能还会写一篇文章来谈留档。

  • 需要让他人接手的一的周期很长的工作。

    • SEUOJ 的运维日志,包括各个东西的配置放在哪里的,配置了哪些自动化任务,服务是怎么被启动的等等。事实上,一台状态不明的但是能用的服务器比一台全新的服务器更加可怕。这些元数据的丢失使得另外的人在接手时出现误操作的概率大大提升。我自己接手 SEUOJ 的初期就出了很多问题,现在已经半年了,摸清楚机子的脾性后胆子也就大了起来,开始维护起运维日志,减少下一任上手的门槛。
    • 赛事部操作脚本,老东西总有毕业的一天,也很难保证接下来的小东西有足够的技术能力从头搞一份,我这些代码大概率会变成祖传代码,写点稍微详细的操作步骤有利于后面的人接手。

实际上这两种日志还有细微的差别。操作日志和博客又有一定差别。具体的,我认为精细程度上,博客>给别人看的日志>给自己参考的日志。

虽然这么说,给自己参考的日志其实给到一个和自己水平差不多的人,他也能看懂的,比如何山直接拿我路由器刷机日志去自己操作了一遍,也没出什么大的幺蛾子。

openwrt 刷机日志

包含内容

  • 最重要的几篇参考资料,github 仓库地址等。
  • 资源留档的路径。
  • 大体操作流程,精细到和我同水平时可以构建出完整流程,识别出关键步骤。
  • 可能的误操作后的还原流程,踩的坑点。
  • 一些其它相关的配置方式(获取这些信息相对容易,但是记录下来可以显著减少时间)

留档

  • 一些花了很大力气才找到的资源(主要是二进制文件)

SEUOJ 运维日志

包含内容

  • 机器的密码。
  • 服务的配置路径主路径资源路径日志路径
  • 对服务做细微调整时,留下的操作日志(供后来者要做调整时参考)。
  • 常规操作时(调整数据库,查询数据)的记录以及 SQL 语句,方便后续继续使用。
  • 额外写了一篇 HTTPS 证书维护的日志,性质和 openwrt 的操作日志差不多。

闲聊

  • 有些东西是方便以后自己再做一些东西。
  • 有些是为了后来人接手(比如 HTTPS 证书的维护,我已经配置好了自动的东西,但是还是要写,毕竟后面的人不知道我干了啥)

赛事部脚本操作日志

包含内容

  • 写明哪些量是需要每次操作时更改的,哪些函数可能需要更改。
  • 写明运行脚本的环境,和所需数据,指明获取所需数据的方法,和上游工作流对接
  • 写明脚本运行前后应该进行哪些人工操作完成整个工作流。

闲聊

  • 后续小登不知道技术实力如何(但是有 AI 之后可以预见的是很难再找到一个和我同年龄时同水平的运维)。
  • 所以做前辈的要铺好路,少走弯路,降低门槛,让小登能打怪升级升上来。

关于日志

有些事情单独开一篇博客太浪费,但是确实不得不记录一下,所以有了这个日志。目前打算是按月开。

VSCode 远程连接到服务器上开发卡死

不要连接到东西太多的目录,有些插件会扫目录,消耗巨大多内存。

建议是单独开一个 workspace 来干活。

另外远程服务器起码有 4GB 内存,2GB 内存纯属冤大头。

Python slots

Python 的 __slots__ 机制,可以限制实例的属性,只允许在创建实例时定义的属性,不允许动态添加属性。

如果尝试为没有在 slots 中声明的变量赋值,会引发 AttributeError。

git 拉取远端其它分支并与本地关联

1
git checkout -b feature-branch origin/feature-branch

git 删除分支

删除本地分支:

1
git branch -d feature-branch

删除远端分支:

1
git push origin --delete feature-branch

Python lambda 闭包问题

lambda 做函数绑定的时候变量是绑定到上的变量的,而不是绑定到上的。

例如下面循环的例子:

1
2
3
4
funcs = []
for i in range(10):
funcs.append(lambda x: x + i)
print(funcs[0](10))

输出是 19,而不是 10,因为查找到的 i 是循环结束后域里面的 9。

正确的做法是:

1
2
3
4
funcs = []
for i in range(10):
funcs.append(lambda x, i=i: x + i)
print(funcs[0](10))

C++ 全局变量的声明和定义

全局变量的声明和定义是分开的,声明是在 .h 中使用 extern 关键字,定义是 .cpp 中使用 = 赋值。

另外就算是用了 #ifdef XXX_H,也不能在 .h 里面定义。不然多个 .cpp 都包含了 .h,还是分开编译的,就会报重定义的错误。

类的静态成员变量是不允许在声明的时候初始化的,只能定义的时候初始化。

Python 线程独立资源

有时候每个线程需要一些独立的资源来执行一些操作,这个可以用 threading.local() 来实现。

一般需要配合 concurrent.futures 的线程池来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

def initializer():
"""初始化线程独立资源"""
driver = uc.Chrome(use_subprocess=True, options=options, driver_executable_path=DRIVER_PATH)

def exact(i):
"""任务"""
driver.get(url)

def crawler():
global driver_pools
driver_pools = threading.local()
with concurrent.futures.ThreadPoolExecutor(max_workers=5, initializer=initializer) as executor:
# 提交任务
futures = [executor.submit(lambda i=i: exact(i)) for i in range(1, 100)]
# 等待任务完成
for future in concurrent.futures.as_completed(futures):
future.result()

Python 异步任务、线程任务

这两个任务一般来说只要开始阻塞执行后,都没办法强制中止的。

后台多线程任务可以设置成 daemon=True,关闭主程序的时候自动关闭后台任务。

异步任务一般要避免出现阻塞,否则主线程会卡死。

另外如果一定要强行中止线程,可以考虑 concurrent.futuresThreadPoolExecutor._threads.clear 方法。

Python Queue

线程安全的 Queue.get 在使用的时候不管是不是保证有东西,最好都加点 timeout。

nullptr 和 iterator::end()

处理指针类型一定要想好有没有可能是 nullptr

处理迭代器的时候一定要想好有没有可能是 end()

C++ 容器

迭代的时候别TM的做删除操作,会漏内存。

git 设置代理

有时候 git 走不了系统代理,可以配置一个全局的代理设置:

1
2
git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy http://127.0.0.1:7890

C++ iostream

istreamostream 是两个抽象类,不能直接实例化,用的时候应该把一个 ifstream 或者 ostream 的子类实例化,然后绑定到 istream 或者 ostream 上。

cerr, cout 这些都是 ostream 的子类,所以有时候

1
2
ifstream fin("in.txt");
istream &in = fin;

C++ Fsanitize

不要开 O2 优化,开了就定位不了错误。

VMWARE 无法打开内核设备

对应虚拟机 .vmx 文件中,将 vmci0.present 改为 "FALSE"

Win11 自定义快捷键反应慢

参考

就是开始菜单 (建议 Win+Q 搜索 设置)找到 "设置", 点击 "应用设置", 把 "后台组件权限" 改为 "从不" 即可。

简介

左偏树是一种可并堆,核心操作是 \(O(\log{n} + \log{m})\) 的 merge 操作。

通过 merge 操作来实现 push 和 pop 操作。

基本信息

  • 左偏树的结构是二叉树
  • 每个节点具有一个额外属性 dist。定义一个节点是边缘节点,当且仅当它的儿子个数不为 2。dist 表示该节点往儿子方向走,走到边缘节点需要经过的最小边数,空节点 dist 定义为 -1。
  • 左偏树每个节点的左子树的 dist 不小于右子树的 dist,所以显然有 dist = right_son->dist + 1

算法流程

push

创建一个新的堆,只分配一个节点,将新堆合并进原有堆。

pop

原有根节点删去,合并其左右儿子,得到新的根节点。

merge

  1. 找到根节点 val 较小的那个堆,将它的根节点作为新堆的根节点,它的左儿子作为新堆的左儿子,它的右儿子和另一个堆合并,作为新堆的右儿子。
  2. 合并时遇到一个堆为空时,非空堆即合并结果,可以直接返回。
  3. 如果合并后右儿子的 dist 大于左儿子的 dist,交换两个儿子。

复杂度分析

结论:

  • push:\(O(\log{n})\)
  • pop:\(O(\log{n})\)
  • merge:\(O(\log{n} + \log{m})\)

证明:

  1. 若一个节点的 dist 为 \(x\),则它及其子树至少有 \(2^x\) 个节点。故 dist 的值是 \(O(\log{n})\) 级别的。
  2. 进行合并时,每递归一层,参与合并的两个堆的 dist 之和减少 1,故递归层数为 \(O(\log{n} + \log{m})\)

实现

结构

  • 一个内部的类 Node 表示左偏树的一个节点,包含 distval 两个变量,以及 son[0], son[1] 两个指针,分别指向左儿子和右儿子。
  • 一个类 Heap,表示一个堆,包含一个 Node 指针,表示根节点。

声明

这里希望练习使用 C++11 中的智能指针移动语义,所以声明为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Heap{
private:
class Node{
public:
unique_ptr<Node> son[2];
int val, dist;
Node(int val=0){
this->son[0]=nullptr;
this->son[1]=nullptr;
this->val=val;
this->dist=0;
}
};
unique_ptr<Node> root;
public:
/*
其它部分暂时省略
*/
};

构造函数

1
2
3
4
5
class Heap{
Heap(unique_ptr<Node> root){
this->root=move(root);
}
};

通过传入一个 unique_ptr<Node>,构造一个 Heap 对象。

unique_ptr 是一种智能指针,它指向的对象是它独享的,不能被其它东西访问。

unique_ptr 仅支持移动语义,这用于转移对象的所有权,对象所有权转移后,原来的 unique_ptr 将失效。

unique_ptr 不支持拷贝和赋值操作,所以上面的构造函数严格来说是有问题的,如果传入的 unique_ptr 不是右值则会尝试调用不存在的拷贝构造函数,导致编译错误。故应该将参数声明为右值引用(注意区分右值引用常值引用)。

具体如下:

1
2
3
4
5
class Heap{
Heap(unique_ptr<Node> &&root){
this->root=move(root);
}
};

至于为什么第一份代码是正确的,是因为只要保证传入的 unique_ptr 是右值,编译器就会优先自动调用移动构造函数,所以不需要在调用函数时显式地写出 move

至于为什么第二份代码中右值引用root 仍然需要使用 move 来显式的转化为右值,这是因为右值在绑定到一个右值引用后,其本身在作用域内是一个具名变量,行为会退化退化为左值。,故需要显式的使用移动语义来调用移动构造函数。

merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Heap{
Heap& merge(Heap &&other){
if(root==nullptr){
root=move(other.root);
}
else if(other.root==nullptr){

}
else{
if(root->val > other.root->val){
swap(this->root, other.root);
}
root->son[1] = move(Heap(move(root->son[1])).merge(move(other.root)).root);
if(root->son[0] == nullptr ||
(root->son[1] != nullptr && root->son[0]->dist < root->son[1]->dist)){
swap(root->son[0], root->son[1]);
}
if(root->son[1] != nullptr)
root->dist = root->son[1]->dist + 1;
else
root->dist = 0;
}
return *this;
}
};

merge 返回一个左值引用是为了方便链式调用。

有几个细节:

  • swap: 可以用于交换 unique_ptr, 它应该是实现了这种移动语义的交换。

  • 隐式构造: Heap(xxx).merge(move(other.root)).root 这里使用了隐式构造Heap.merge 支持的参数是一个右值的 Heap,传入时传的一个右值的 unique_ptr,由于定义了右值 unique_ptrHeap 的构造函数,这里直接隐式调用了构造函数,构造了一个右值的 Heap 并作为参数传递给了 merge

  • 返回左值引用: return *this 返回左值引用的目的是方便链式调用,至于为什么临时构造的 Heap 能返回一个左值引用,是因为临时对象生存周期是到表达式结束为止,而临时对象在生命周期内可以被左值引用。

push && pop && top

1
2
3
4
5
6
7
8
9
10
11
12
13
class Heap{
int top() const{
return root->val;
}
void push(int x){
this->merge(unique_ptr<Node>(new Node(x)));
}
int pop(){
this->root = move(Heap(move(root->son[0])).merge(move(root->son[1])).root);
// 当 merge 函数返回时, 它返回的是临时对象的左值引用, 临时对象在生命周期内可以被左值引用.
return top();
}
};

比较简单, 不解释了.

左值右值的理解

简单的说:

  • 右值是只能放到表达式右边的值,左值是既能放到表达式右边,也能放到左边的值

  • 右值一般是没有命名的值,左值一般是有名字的值

  • 右值是临时的值,左值是持久的值

更具体一点:

左值的特性

  • 有固定内存地址:左值通常存储在堆或栈上,可以通过取地址操作符(&)获取其地址。

  • 可以被多次访问:左值的生命周期较长,可以在多个语句中被访问。

  • 可以被修改:左值通常可以被修改,例如变量赋值操作。

右值的特性

  • 没有固定内存地址:右值通常是临时对象,没有固定的存储位置,或者其存储位置在表达式结束后立即失效。

  • 不能被取地址:右值不能使用取地址操作符(&)获取其地址。

  • 不能被多次访问:右值的生命周期仅限于当前表达式,不能被多次访问。

  • 通常不可修改:右值通常是不可修改的,因为它们是临时的。

右值举例

C++ 的值除了右值就是左值,下面几个右值的例子:

  • 函数的返回值是一个右值。
  • 字面量,10, "hello" 是右值。
  • 临时对象是右值,例如 c = a + b; 中的 a + b 这个整体。

下面是左值的例子:

  • 命名变量名是左值,例如 int a = 10; 中的 aa = b = c = 10;a, b, c 都是左值。

左值引用和右值引用

名词解释

1
2
3
4
5
6
7
8
9
int a = 10; // 左值 被赋值为 右值 , a 是左值, 10 是右值.
int &&x1 = 10; // 右值引用 绑定到 右值, x1 是右值引用, 10 是右值, 允许.
int &&x2 = a; // 右值引用 绑定到 左值, x2 是右值引用, a 是左值, 允许, 一般用于模板编程.
int &y = a; // 左值引用 绑定到 左值, y 是左值引用, a 是左值, 允许.
int &y1 = 10; // 左值引用 绑定到 右值, y1 是左值引用, 10 是左值, 编译错误.
int &y2 = x1; // 左值引用 绑定到 右值引用, y2 是左值引用, x1 是右值引用, 编译错误.
int &&z1 = y; // 右值引用 绑定到 左值引用, z1 是右值引用, y 是左值引用, 允许, 并且, 支持引用折叠语法, 所以 z1 的实际行为是左值引用.
int &z2 = z1; // 左值引用 绑定到 可折叠为左值的 右值引用, z2 是左值引用, z1 是右值引用, 但可折叠到对 a 的左值引用, 允许.
int &z3 = x2; // 左值引用 绑定到 可折叠为左值的 右值引用, z3 是左值引用, x2 是右值引用 但可折叠到对 a 的左值引用, 允许.

右值引用的特性

右值引用似乎是命名变量吧?

所以:右值引用可以在不发生内存操作的前提下将临时右值变为左值,可以延长右值的生命周期,降低读写内存的开销。

左值引用的特性

左值引用等价于为变量取了一个别名。

特别的,在函数传参如果使用左值引用传参,函数类通过左值引用可以修改原变量的值。

引用折叠

在不发生编译错误的情况下, 可以支持引用折叠, 名词解释中的 z1, z2, z3 例子解释了这个特性。

具体的,在发生多次引用绑定时,按照以下规则折叠:

  • 左 + 左 = 左
  • 右 + 左 = 左
  • 左 + 右 = 左
  • 右 + 右 = 右

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include "bits/stdc++.h"

using namespace std;

class Heap{
private:
class Node{
public:
unique_ptr<Node> son[2];
int val, dist;
Node(int val=0){
this->son[0]=nullptr;
this->son[1]=nullptr;
this->val=val;
this->dist=0;
}
};
unique_ptr<Node> root;
public:
Heap(unique_ptr<Node> &&root){
this->root=move(root);
}
int top() const{
return root->val;
}
void push(int x){
this->merge(unique_ptr<Node>(new Node(x)));
}
Heap& merge(Heap &&other){
if(root==nullptr){
root=move(other.root);
}
else if(other.root==nullptr){

}
else{
if(root->val > other.root->val){
swap(this->root, other.root);
}
root->son[1] = move(Heap(move(root->son[1])).merge(move(other.root)).root);
if(root->son[0] == nullptr ||
(root->son[1] != nullptr && root->son[0]->dist < root->son[1]->dist)){
swap(root->son[0], root->son[1]);
}
if(root->son[1] != nullptr)
root->dist = root->son[1]->dist + 1;
else
root->dist = 0;
}
return *this;
}
int pop(){
this->root = move(Heap(move(root->son[0])).merge(move(root->son[1])).root);
// 当 merge 函数返回时, 它返回的是临时对象的左值引用, 临时对象在生命周期内可以被左值引用.
return top();
}
};

int main(){
Heap h(nullptr);
int n;
cin >> n;
for(int i=1;i<=n;i++){
int tp, x;
cin >> tp;
switch (tp){
case 1:
cin >> x;
h.push(x);
break;
case 2:
cout << h.top() << endl;
break;
case 3:
h.pop();
break;
default:
throw "unsupported operation";
}
}
return 0;
}
0%