楚天

惟楚有材,于斯为盛

前言

这篇文章整理一下我当前博客的部署方式,避免以后每次改完博客都要重新回忆一遍。
这套方案的核心思路很简单:

  1. hexo 分支保存博客源码
  2. main 分支保存生成后的静态页面
  3. 平时写文章、改主题、换头像,都是在源码分支完成
  4. 最后通过 hexo deploy 把静态文件发布到 GitHub Pages

当前博客结构

我现在这个仓库的配置大致如下:

1
2
3
4
5
6
7
# _config.yml
theme: next

deploy:
type: git
repository: https://github.com/ChutianDuan/ChutianDuan.github.io.git
branch: main

也就是说:

  • hexo 分支负责维护源码
  • main 分支负责网站发布
  • GitHub Pages 最终读取的是 main 分支内容

这种做法的好处是源码和生成产物分离,日常维护会清晰很多。


需要的环境

先保证本地安装好 Node.js 和 npm。
然后在博客目录执行依赖安装:

1
npm install

当前项目里比较关键的依赖有:

1
2
3
"hexo": "^7.3.0",
"hexo-deployer-git": "^4.0.0",
"hexo-theme-next": "^8.27.0"

其中 hexo-deployer-git 很重要,如果没有它,执行部署时会报找不到 git deployer


日常写博客的流程

1. 编写或修改文章

文章都放在 source/_posts/ 下面,例如:

1
2
source/_posts/hello-world.md
source/_posts/杂七杂八/内网穿透.md

如果是图片资源,一般放在:

1
source/img/

比如头像目前就是:

1
source/img/me.jpg

2. 本地预览

写完文章后,可以先在本地启动预览服务:

1
npm run server

然后浏览器打开:

1
http://localhost:4000

这样可以先检查下面这些内容有没有问题:

  • 标题和分类是否正常
  • 封面图是否能显示
  • Markdown 排版是否错乱
  • 代码块高亮是否正常
  • 图片路径是否写对

3. 生成静态页面

确认内容没问题后,先执行一次生成:

1
npm run build

它实际对应的是:

1
hexo generate

这一步主要是提前发现问题,比如:

  • 配置文件写错
  • 文章 front matter 格式错误
  • 主题模板引用异常
  • 图片路径或资源处理问题

如果这里都能通过,后面的部署一般就比较稳。


4. 提交源码分支

这一步不要省。
很多人只执行了部署,却忘了把源码推到远程,结果换台电脑后博客改动全没了。

我的源码分支是 hexo,所以常用流程如下:

1
2
3
4
git status
git add .
git commit -m "更新博客文章"
git push origin hexo

这一步推送的是:

  • 文章源码
  • 主题配置
  • 页面配置
  • 图片资源
  • 其他博客工程文件

也就是说,这一步是在“保存你的工作过程”。


5. 部署到 GitHub Pages

源码推送完成后,再执行:

1
npm run deploy

它实际对应的是:

1
hexo deploy

根据当前配置,Hexo 会做下面几件事:

  1. 读取 public/ 里的生成结果
  2. 把这些静态文件提交到部署目录
  3. 推送到远程仓库的 main 分支

这一步完成后,GitHub Pages 就会从 main 分支发布网站。


一套完整命令

如果只是日常更新一篇文章,我现在一般直接按这个顺序执行:

1
2
3
4
5
npm run build
git add .
git commit -m "更新博客"
git push origin hexo
npm run deploy

可以理解成两次推送:

  • 第一次把源码推到 hexo
  • 第二次把生成后的静态站点推到 main

为什么要分两个分支

这个问题很常见。

如果把源码和静态页面都混在一个分支里,会出现几个明显问题:

  • 仓库里会同时出现 Hexo 源码和大量生成后的静态文件
  • 每次提交都会混入很多无关改动
  • 后续维护主题、文章、配置时容易混乱
  • 回滚也不方便

分开后逻辑就很清楚:

  • hexo 是“项目源代码”
  • main 是“构建产物”

这和普通前端项目里“源码目录”和“打包产物目录”分离是一样的道理。


常见问题

1. 执行 hexo deploy 失败

优先检查有没有安装部署插件:

1
npm install hexo-deployer-git --save

然后确认 _config.yml 里的 deploy 配置是否正确。


2. 页面没有立刻更新

这通常不是部署失败,而是 GitHub Pages 还没刷新完成。
一般等几十秒到几分钟即可。

还可以检查:

  • main 分支是否已经收到最新提交
  • GitHub Pages 配置是否指向正确分支
  • 浏览器是否有缓存

3. 本地构建成功,但线上图片不显示

这种问题通常出在路径。

例如站点资源推荐写成:

1
/img/me.jpg

而不是本地绝对路径,也不要写成 Windows 文件系统路径。


4. 只部署了页面,没有保存源码

这是最容易踩的坑。
如果你只执行了 npm run deploy,远程网站可能已经更新,但源码分支并没有保存。

正确顺序应该是:

  1. 先提交并推送 hexo 分支
  2. 再部署到 main

结语

对于个人博客来说,这套流程已经足够稳定:

  • 平时在 hexo 分支维护文章和配置
  • npm run build 检查生成结果
  • git push origin hexo 保存源码
  • npm run deploy 发布站点

这样做的优点是简单、清晰、容易回溯,后续不管是换主题、换头像,还是新增文章,都能沿着同一套流程继续维护。

内网穿透方案_ZeroTier

Liunx 配置

  1. 安装
1
curl -s https://install.zerotier.com | sudo bash
  1. 启动并设为开机自启
1
2
sudo systemctl enable --now zerotier-one
sudo systemctl status zerotier-one --no-pager

) 加入网络(Network ID)

1
sudo zerotier-cli join <NETWORK_ID>
  1. 去 Central 授权

  2. 验证是否拿到 Managed IP

1
2
3
sudo zerotier-cli status
sudo zerotier-cli listnetworks
ip a | grep -n "zt"

Windows(Win10/Win11)

  1. 安装
  1. 去 ZeroTier 官网下载 Windows Installer(.msi / .exe)
  2. 双击安装(默认一路 Next 即可)
  1. 启动方式
  • 安装后通常会自动启动服务,并在右下角托盘出现 ZeroTier 图标
  • 若没看到托盘图标:开始菜单打开 ZeroTier One(会拉起托盘)
  • 或检查服务:Win + Rservices.msc → 找到 ZeroTier One(或类似名字)→ 启动
  1. 加入网络(GUI)
  1. 右下角托盘 ZeroTier 图标右键
  2. Join New Network…
  3. 粘贴 <NETWORK_ID> → Join

最小联通测试

在任意一端用对方 Managed IP:

  • 测试连通:
1
ping <对方Managed_IP>
  • SSH(Linux 目标):
1
ssh user@<对方Managed_IP>
  • RDP(Windows 目标,默认 3389):
    mstsc /v:<对方Managed_IP>

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

栈的注意事项

time:2026_1_22

引用修改

char * 指针能使用任意类型,可以对任意类型的进行字节修改

内存分配

元素在栈上的空间可能不连续,这是由于编译器在编译过程中会产生的优化,可能会出现原本连续的元素,但是其中的元素没有使用,编译优化就不进行字节分配,然后通过其它指针进行访问就会出发未定义行为。
为了防止为定义行为最好还是用start进行封装

malloc 分配的时候默认会安装最大字节进行分配 16字节

new 智能分配字节

Client 实现分析

time:2026_1_21

app 依赖包

ClientRender
可视化

GameConfig
client 基础信息参数 后续可以扩展到server 公用一个确保准确

InputPredicition
预测 x,y 的动作

主体部分

ClientCtx

lab::net::UdpSocket sock; 发收
lab::net::UdpAddr server{}; server 接受

localHist 本地
remoteHist 预测
stateHist 权威

BuildCmdVec (localPid localCmd remoteCmds)
通过pid识别是否是本段,保存一个vectorcmds size = 2的状态,本段保存输入,对端保存预测
return (输入, 预测)// vectorcmds size 2

GetRemoteCmdForTick ctx pid t

return (预测)

ApplyAuthoritativeState ctx st

整理

主要就就两条思路 接受处理,发收处理维护不同的状态

OnUdp
Recv:只负责收包 -> 更新 ack/state -> 更新对手预测/触发回滚

网络开发总结

模块

NetCodec

  1. Input
    ID,count, client(本端)最新tick, clientAckServerTick(server确认最新tick) cmd包

  2. Ack
    权威tick, 最后tick 以及hash验证

  3. Statae
    权威状态包

  4. Start
    游戏开始通知

NetStub

废弃 tick 模拟状态发送和打印监控,状态发送已改为键盘输入

Packets

  1. PacketType 状态,input ack state start
  2. InputPacket 包 冗余发松多个InputCmd 包以防丢包
  3. start 包
    对局状态信息

UdpSocket

udp 连接比tcp简单,持续监听端口

游戏设计者模式

time:2026_1_21

单例模式

通常用于游戏中的全局管理类,保证整个程序(进程)中只有一个实例对象存在。只有第一次调用会进行初始化,后面调用保持原始状态。

1
2
3
4
5
6
7
8
9
10
11
class Game{
public:
static Game *getgame(){
if(game == nullptr){
game = new(Game);
}
return game;
}
private:
static Game *game
}

模板模式

把共同的部分集中到一个基类,把不同的细节部分留给子类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Character{
protected:
virtual void drow();
virtual void move();
public:
void updata(){
move();
move();
move();
drow();
}
};
struct Game{
vector<character *> chars;
void updata(){
for(auto && c : chars){
c->updata();
}
}
}

状态模式

为了解决枚举问题

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
struct Monster;

struct State {
virtual void update(Monster *monster) = 0;
};

struct Idle : State {
void update(Monster *monster) override {
if (monster->seesPlayer()) {
monster->setState(new Chase());
}
}
};

struct Chase : State {
void update(Monster *monster) override {
if (monster->canAttack()) {
monster->setState(new Attack());
} else if (!monster->seesPlayer()) {
monster->setState(new Idle());
}
}
};

struct Attack : State {
void update(Monster *monster) override {
if (!monster->seesPlayer()) {
monster->setState(new Idle());
}
}
};

struct Monster {
State *state = new Idle();

void update() {
state->update(this);
}

void setState(State *newState) {
delete state;
state = newState;
}
};

原型模式

主要解决问题时深拷贝的问题,因为在虚函数会导致的拷贝构造函数会出现拷贝不完全的情况,所以要采用原型模型进行完整数据类型拷贝

1
2
3
// 视线深拷贝
Ball *ball = new RedBall();
Ball *newball = new BedBall(*dynamic_cast<RedBall *>(ball));

原型模式将对象的拷贝方法作为虚函数,返回一个虚接口的指针,避免了直接拷贝类型。但虚函数内部会调用子类真正的构造函数,实现深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Ball{
virtual thread_ptr(Ball) *close() = 0;//加入thread_ptr 实现内存智能管理
};

struct RedBall :Ball{
thread_ptr(Ball) *close() override{
return new RedBall(*this);
}
int x ;// 如果有成员变量,也会一并被拷贝到
}

Ball *ball = new RedBall();
Ball *ball_2 = ball->close(); //视线深拷贝,

CRTP 模式自动实现 clone CRTP能将子类加入模版中已放置重复定义

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
struct Ball {
virtual unique_ptr<Ball> clone() = 0;
};

template <class Derived>
struct BallImpl : Ball { // 自动实现 clone 的辅助工具类
unique_ptr<Ball> clone() override {
Derived *that = static_cast<Derived *>(this);
return make_unique<Derived>(*that);
}
};

struct RedBall : BallImpl<RedBall> {
// unique_ptr<Ball> clone() override { // BallImpl 自动实现的 clone 等价于
// return make_unique<RedBall>(*this); // 调用 RedBall 的拷贝构造函数
// }
};

struct BlueBall : BallImpl<BlueBall> {
// unique_ptr<Ball> clone() override { // BallImpl 自动实现的 clone 等价于
// return make_unique<BlueBall>(*this); // 调用 BlueBall 的拷贝构造函数
// }
};


Ball *ball = new RedBall();
Ball *ball_2 = ball->close(); //视线深拷贝,

组件模式

1
2
3
4
struct connect{
virtual void updata()
}

观察者模式

发布-订阅模式

访问者模式

服务器分析

time:2026_1_20

ClientConn
历史缓存
tick
最后输入tick
最后权威tick
最后输入包
玩家数量

ServerCtx
基础信息
对局设置
帧同步设置
世界设置
sock 网络
clients 存储客户端状态
函数
AssignSlot 分配id 1,2
GetPlayer 从ServerCtx->clients 取出ClientConn
OnlineCount 在线玩家统计
GetCmdForTick 从ClientConn中获取动作信息,正常接受,如果出现延迟但小于延迟设置,着延续上一次包,但是出现过大延迟直接采用默认信息

OnUdp 收包加入缓存
MaybeStartMatch for pid 轮训发收start包,其中通过利用getplayer 吧serverctx中的clientconn取出,同时检测ctx中的状态已start则跳过

OnTick 
    整体事件监听
    推进世界 + 给两边发 State/Ack

工厂设计模式

time:2026_1_20

虚函数

virtual
即使通过基类的指针或引用调用该函数,实际调用的函数是派生类中重写的版本
override
如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译
virtual 定义了父类中的子类需要重载的函数,override 则是确保子类在继承父类的时候必须进行重载,以防后续错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct pet{
virtual void speak() = 0;
};
struct cat::pet{
void speak() override{
print("miao");
}
};
struct dog::pet{
void speak() override{
print("wano");
}
};
void feed(Pet *pet) {
puts("喂食");
pet->speak();
puts("喂食完毕");
}
int main(){
pet *cat = new cat();
pet *dog = new dog();
freed(cat);
freed(dog);
}
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
sturct Reduce(){
virtual int init() = 0;
virtual int add(int a,int b) = 0;
};

struct add_our :: Reduce(){
int init() override{
return 0;
}
int add(int a ,int b) override{
return a+b;
}
};
struct chen_our :: Reduce(){
int init() override{
return 1;
}
int add(int a,int b) override{
return a*b;
}
};

int reduce(std::vector<int> v,Reduce & reducer){
int x =reducer->int();
for(int i = 0;i<v.size();i++ ){
x = reducer->add(x, v[i]);
}
return x
}

工厂模式

享元模式

享元模式:共享多个对象之间相同的部分,节省内存开销
共享同智能指标share_ptr实现多个对象指向同个资源在减少资源开销的同时,也通过rall特性完成智能内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Sprite{
vector<char> texture;
void draw(glm::vec3 posoition){
glDrawPixels(posoition, texture);
}
};
struct Bullet{
glm::vec3 posoition;
glm::vec3 velocity;
shared_ptr<Sprite> sprite;

void draw(){
sprite->draw(position, velocity);
}
}

代理模式