MoreRSS

site iconGamea | 飘逝的风修改

游戏后台开发十多年, 目前就职于Tencent。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Gamea | 飘逝的风的 RSS 预览

Mac mini M4 游戏体验报告

2024-11-25 08:00:00

Featured image of post Mac mini M4 游戏体验报告

大家的 Mac mini M4 入手了吗?每年都有不少朋友会尝试到底 Mac 能不能玩游戏。今年性能又听说强这么多,那不得试试?

我们的好奇心

我入手的是最低配的 16+256G 版本,运气不错,赶上了国补,花费 3500+。它的搭配以及使用感受在上一篇Mac mini M4 折腾记中已经聊过了,感兴趣的可以往前翻一翻。作为一个游戏玩家,虽然没指望长期用它打游戏,但是,咱可以不玩,不能没有这能力是吧?

对于 M4 芯片的 Mac mini,我们可能有几个疑问:

  • 它能玩3A大作吗?玩游戏体验怎么样?
  • 如果我想玩,需要怎么折腾?

带着这些好奇心,我花了一些时间趟趟坑,这篇文章聊聊我的体验,希望对你有所帮助。因为对一些软件接触较短,理解不深,若有错误,欢迎交流。

几种玩游戏的方式探索

MacOS原生玩游戏?

我上一篇文章试过一次,在玩 Inside 时,贴图花了。 对于这种难度不高的游戏,似乎也没有保障。所以如果你直接 MacOS 下玩Steam 的游戏,可能会遇上类似的不开心事情。当然,这个问题我劝你不要怪苹果,可能开发商锅大一点。 有一些游戏它本身在 Mac 下适配不错,也能拥有不错的体验,比如《文明 6》、《古墓丽影》系列、《博德之门3》等,有网友实际玩过还不错,帧率在 1080P 下流畅运行,2K也勉强能跑的地步。

CrossOver 玩游戏

跨平台运行 Windows 软件,或许你听过大名鼎鼎的 Wine,而CrossOver 则是基于它的一个可能更易用的商业化版本,我们就用它来玩玩游戏吧(我折腾过一下免费开源的Whisky,对一些游戏的兼容性差一些)。 关于 CrossOver的一句话介绍:

CrossOver 是一款系统兼容软件,它让您可以在 Mac 和 Linux 系统上运行 Windows 应用/游戏,不必购买 Windows 授权,不必重启,不用虚拟机。通过 CrossOver, 从 dock 直接启动 Windows 应用/游戏,与您的 Mac 和 Linux 系统无缝集成。

刚好上面的 Inside 游戏没有跑得很顺畅,那么给 CrossOver 次能力证明的机会。 看起来贴图没问题了,细心的你或许发现,帧数有下降?别担心实际它跑起来是基本稳定 60 帧,而在截图时游戏自动降频了。 然后接上手柄也试了一下,一切正常能控制。 开胃菜过后,咱来点硬菜。我的Steam库上正好有个《哈迪斯》,它有 Mac 版本,那就两种模式PK一下吧? 就体验来说,MacOS和CrossOver跑的感受差不多,很流量,画面也没打折扣,以下是一小段录屏 :
但是当我想玩一下《生化危机 4 重制》时,报错了:

这个问题,我们需要升级 CrossOver 的默认 game porting toolkit(GPTK)到 2.0 版本。很简单,安装完 Crossover 后,到苹果GPTK官网下载最新版本 GPTK。
阅读一下Read Me.rtf就可以了,我们是用的 CrossOver,就看这几行:

1
2
3
4
CrossOver: replace CrossOver’s copies of the D3DMetal.framework and libd3dshared.dylib found at /Applications/CrossOver.app/Contents/SharedSupport/CrossOver/lib64/apple_gptk/external with the libraries from this distribution:
cd /Applications/CrossOver.app/Contents/SharedSupport/CrossOver/lib64/apple_gptk/external
mv D3DMetal.framework D3DMetal.framework-old; mv libd3dshared.dylib libd3dshared.dylib-old
ditto /Volumes/Evaluation\ environment\ for\ Windows\ games\ 2.0/redist/lib/external/ .

简单说就是备份两个文件夹再覆盖过去就可以了。再次打开软件,正常进入《生化危机 4》。 游玩一切正常,可能识别到机器性能,画质不太高,不过还算流畅,我一小心通过了刚开始的丧尸围剿,哈哈。可惜退出游戏时会报崩溃要强行结束,美中不足呐!

有网友isaced评论:

最近在 Mac Mini M2 Pro + CrossOver 24.04 (GPTK) 尝试了《永劫无间》,除了机身发烫比较厉害,基本可以完美游玩。

2024 年激励无数国内游戏玩家的黑神话自然也是可以畅玩的,看到有网友展示,并且这个兼容性官方网站可查: 可惜我本人购买的是 PS版本,故未亲自验证哈。(顺便说一下,如果你想了解如何在 SteamDeck 上玩 PS 上的游戏,可以见我之前SteamDeck在手,国庆远程游玩PS5指南一文,公众号历史中也可以查看到。)

但是,也别太高兴,CrossOver 并不是万能的,有一些游戏也不兼容,比如想玩一下撸啊撸(英雄联盟)的话,你可能被SayNo,不支持的原因有多种,比如游戏厂商反作弊系统限制。我们可以在这里看到兼容的游戏列表。提前查看可节省掉不少瞎折腾时间:)

PS:前文说过 CrossOver 也不止for 游戏,Windows 的软件等也可以跑,如果你购买了,别浪费,不妨试一下。我居然用它安装了十几年前的金山打字通,运行在 XP 上:)这个软件有 15天免费,国内价格倒也不贵,我购买了 2024版本。但我推荐你不着急购买,是否能解决你的实际问题,并且用几天一切稳定再说呢~

虚拟机玩游戏

前面虽然用 CrossOver 解了一下玩 3A游戏的馋,但大众游戏 LOL居然不行。或许用原生OS 确实不行,你肯定会想那我上虚拟机得行不?正好这次购买 Mac mini 时,同时赠送了一个 PD 的两年授权,那就试一下 PD 虚拟机吧?说做就做,安装上 WeGame,又下载了25G+的游戏,运行看看: 无论怎么尝试依旧不行,这个LOL客户端可太有历史了,估计屎山深筑吧:)有哪位小哥可以跑的话,请不吝赐教,感谢!话说PD也并非没有作用,像 WeGame 在 CrossOver 不行,我们在 PD 里玩有些游戏是没问题的。我打开了一些小游戏,重温了一下《去月球》,只记得那旋律,剧情快忘记了,当年泪目的游戏呢~~~ 看到网上也有朋友试过 VMware 虚拟机,那个兼容性似乎更差一些,这次我没有尝试。其它比如 Mac 下的 UTM 等虚拟化方案,相信不太能超过 PD 便也没有再试。

一些实践Tips

开启Metal HUD

有两种方法:

  1. 在程序启动时添加环境变量:MTL_HUD_ENABLED=1。比如在 CrossOver 中,运行游戏时添加相关参数然后打开游戏即可。如果给Steam添加相关选项后,它启动的游戏也自动会开开HUD。
  2. 直接在终端执行/bin/launchctl setenv MTL_HUD_ENABLED 1然后再启动相关程序,要关闭把 1 改为 0 再执行。这种方式,官方提示CrossOver要先关闭再操作后重新打开。

CrossOver中的选项

你可能会发现CrossOver有一系列的高级设置,默认啥也没开,到底要开哪一个好呢?我们简要介绍一下

设置 功能描述 适用场景 建议
D3DMetal 将 Direct3D 调用转换为 Metal 调用,优化 macOS 上的图形性能。 在 M 系列芯片的 Mac 上运行需要 Direct3D 的游戏。 如果支持,建议开启。
DXVK 将 Direct3D 10 和 11 调用转换为 Vulkan 调用,提升性能和兼容性。 在 Linux 或其他支持 Vulkan 的系统上运行游戏。 在 macOS 上通常无需开启。
ESync 通过减少系统调用优化多线程应用程序性能,特别是在处理同步对象时。 多线程游戏,特别是对同步敏感的应用。 建议开启以提升性能,若有问题可尝试关闭或切换至 MSync。。
MSync 对 ESync 的改进,旨在更好地处理多线程同步,减少性能开销。 高度多线程的应用程序。 可作为 ESync 的替代选项,需根据具体游戏表现进行测试。
高分辨率模式 启用后以高分辨率报告给应用程序,提升视觉效果。 支持高分辨率显示的应用程序。 对于配置较低的设备(如 M4 mini),建议关闭以确保性能稳定。

我们是 Mac,苹果出的 GPTK 正是优化Metal 调用等,故D3DMetal推荐开启。其它建议开启ESync,而高分辨率模式我们这“羸弱“的 M4 mini 就先不开了吧?

个人感受

如果你懂一些技术,看到命令行不会犯怵,可以通过GPTK iwiki 看到如何完全手动安装 Steam、战网等,这样结合 Wine可以完全免费搭建起基本的 Windows游玩平台。如果希望更简单易上手,也可以直接安装 CrossOver,然后像文中提到去升级一下 GPTK2.0 以提供更好的兼容性。

总的来说,想要在 Mac 下愉快玩游戏,其实也挺折腾的,如果手上没有更适合的设备,或者有一定的移动需求而你的设备是 PC 等,这小小的 Mac Mini 倒提供了较好的移动性。不然我觉得你就别浪费时间了,相信上面我遇到的现象也是你随时可能碰到的。但你说它不能玩吧,它还行呢,3A 大作也陆续有不少可以流畅游玩。配置高一些的 Pro 或 Max 甚至在一些游戏上百帧也不是问题。但你说它坑吧,玩个 LOL 都有点困难,特别是国服。

于我而言,过年背回老家接个显示器/键盘/鼠标玩一下或许可考虑,平常嘛,我大概是不太会在这上面玩的啦~白买了我的 CrossOver,也算是给各位探探路啦。

本篇文章就写到这里,感谢阅读。如果期待看到更多玩法,欢迎点赞鼓励,您的支持是我写作的动力!

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号

Mac mini M4 折腾记

2024-11-16 08:00:00

Featured image of post Mac mini M4 折腾记

最近MacMini可是太火了,拿到手一周多,折腾了一些玩意,闲言碎语随手记录一下。我们聊聊配套的硬件选择,聊聊如何更好的利用它,聊聊它的AI能力,聊聊游戏和一些工具。或许有些许参考价值,或许仅是自娱自乐,欢迎交流。

为什么购买

作为十几年的Macbook用户,我绝大多数的工作、学习都是基于Apple的Mac平台,而Windows平台仅用于偶尔游戏娱乐了。若不是要玩某些游戏,我大抵是不会启动Windows机器的。想到这里,顺便盘点一下我使用过的Macbook。

  • 似乎在2013年入的Macbook Air 13寸,它真的超轻,第一次接触它,打开电源后“duang”的一声激起的兴奋似乎还能回味。
  • 可惜当年它还不是视网膜屏的,后面15年换15寸Macbook Pro高配版后,又眼前似乎变清晰了。我用它多年,直到19年左右把它给爱回收了,居然还挺保值,这远超我的预期。
  • 然后公司19年配的带Touchbar那款,这玩意虽然用了三年,但是没太好的印象,感觉Touchbar的体验并不好,特别是对程序员,我的那个快捷键呢? 可是在它快退休的时候,苹果有个键盘召回更换活动,拿到天才吧居然免费把键盘和电源一并免费换了。这让我在转手出去时,居然以一种全新状态(除了屏幕)卖出了不错的价格。真得夸夸苹果的售后服务。
  • 接着遇到全新ARM架构的M1芯片,那个续航真是超级心动。于是借着生日礼物的由头,让媳妇送了一台Macbook Air给我。为了节省点预算,选了丐版的8G内存。老实说后面挺后悔,因为这金贵的内存。最开始8G内存好好的,但随着有舆论说它狂用SSD磁盘,后面开始就卡了。时不时提示内存不足,这严重影响了我的使用体验。一度让我养成了浏览器只打开几个标签页的“好习惯”。但它的轻薄、续航、颜值特别棒,是我家里写代码和码字的主力,和跟随我出没在书房、客厅、卧室的各个角落。这就是所谓爱不释手吧。
  • 22年公司给换了MacBook Pro 15寸,32G+1T的M1 Pro机型,在工作中使用至今。对它还挺满意,就是重了点。让我自己买一台在家用的话,我肯定不会选它。要怪就怪Air系列把我养得娇气了哈~

可是,我却从来没有用过Mac Mini。以前的感受是,就这一台小主机,要价还不便宜?干嘛不多花点钱买台带屏幕、键盘、触控板的MacBook呢,就丐版的Macbook Air也是超香啊?人在权衡这些时,看起来是评论它的定位,而一旦Mini最新版本降到只要3500元,信念它居然就有了回旋的余地,哈哈。原来并不是信念,而是钱在作怪。

当然还是要怪Air时不时提醒我内存不足,又恰逢最新M4芯片的Mac Mini在内存上加量不加价,于是就它了。 当然还是要怪Asahi Linux居然让M1芯片上也可以跑Linux了,好想去折腾一下,有就有了更多理由了。

还有呢,国补优惠这么高,不得支持一下这经济复兴政策嘛。

硬件搭配

最有性价比的当然是最丐版的16G + 256G存储的版本,恰逢国补,我在京东3581元拿下。这台小主机有10核心CPU,其中4个性能核心,6个能效核心,GPU是10核心,NPU是16核心。光有它可玩不转,我们还得搭配很多东西。对我来说,第一个有必要的就是硬盘,毕竟256G的存储空间,装个系统和一些软件就占掉一大半,想试着跑个模型啥的,别说GPU它不行,你硬盘可能先炸为敬了。

虽然我是11月8号拿到的机器,但是在3号预定时,就谋划着硬盘扩充方案了。我们知道网上已经有直接替换内置硬盘的方案了。不过我手头本来就有闲置的前两年支持国货买(囤)的2T的致态TiPlus7100,是时候到它表示真正技术的时候了。咱这个硬盘速度好歹也是PCIE4.0的,不上个雷电4就暴殄天物了。于是左右看各种评测,结合我未来可能长期开机作服务器的用法,还是买个散热好点的带风扇的硬盘盒。最后选择了这个,性价比较高: 机器到手后实测了一下,硬盘盒的读写速度如下:(前者为外接的雷电4硬盘盒,后者为机器内置的M.2硬盘)

然后是键盘,考虑到有时还会有Windows需求,主力是MacOS,所以选择了一款对MacOS友好的键盘。并且想要切换方便,需要三模的(有线/蓝牙/2.4G),于是选择了这款: 到手后它的小巧和颜值还是很喜欢的,敲击手感也不错。之前没有用过矮轴,这次体验下来,感觉还不错,打字不用太辛苦,红轴也不吵。 期间也考虑过不少人推荐的罗技mx keys,似乎很匹配MacOS,不过按键布局上,我更喜欢Nuphy的紧凑。

接着是鼠标,我原本就有几个罗技的鼠标,仅用于打游戏,它只支持2.4G,一般是用于Windows上延迟比蓝牙低一些。然后继续看了一下它们家的Master3S等,这个似乎大手才比较好,小手另有其它如anywhere系列。考虑了一下,我或许还是更适合多年习惯的妙控板,于是放弃购买鼠标了,不买立省100%是不是,把常年放在公司很少使用的触控板带回家了。

显示器的话,家里一直是Redmi 4K的那块畅销屏,用着也不错,就不必升级了。不过原来使用笔记本再外加一块扩展屏,习惯了多屏工作的人,只有一个屏幕都不会写代码了:) 所幸还有经常吃灰的iPad以及一块玩主机的便携屏(1080P),它们都能很方便的加入到生产中,凑合着用吧。

至于音箱,都说Mac Mini只能听个响,作为Apple生态的Apple AirPods耳机咱也是换过几代了,手上自然也是有的,这笔费用支出也免了。

最后,扩展坞什么的,我虽然有,也不需要,也不想用(第一次进系统确实需要,后面就再也没用过了)。天知道插满一堆设备一堆线的Mac Mini,乔帮主知道会不会从坟墓里跳出来,哈哈。

LLM跑一下

百闻不如一测,大家说M4芯片有多厉害,我到手第一天就迫不及待的跑了一下LLM。我用的是LM Studio,跑了下4bit量化的Qwen2-7B模型:

输出速度是22token/s,几次差不多都比较稳定的这个成绩。这个模型是MLX优化过专门for Apple Silicon的,所以速度上比原生版本快了一些。但这个速度嘛,其实我并不惊喜。顺便用Ollama跑一下(我现在硬盘大了,下载这种小模型可不用皱眉啦~),速度也差不多。现在各种闭源模型那输出都飞快,这测试的速度就有点考验耐心啦。当然,这个的速度主要得看GPU,而咱们最丐版的也就只有10核心,或许也就这样了。

那么它和我手上M1 Pro芯片比怎么样呢?同样在LM Studio中同样的模型同样的问题,它的输出有33.84token/s,比M4 Mini快了1.5倍。大概是因为M1 Pro有16核心GPU,而M4 Mini只有10核心GPU。看来GPU的提升对LLM的输出速度还是有比较大的影响。未来真正跑大模型得上Max或者Ultra芯片吧?

玩下游戏

既然跑模型不太行,咱能不能更好的玩Windows游戏呢?当然可以,需要一点折腾时间。我说的肯定不是安装虚拟机玩,那个游戏体验不可能好咯。Steam有MacOS的版本,我先跑了一下它,能运行的游戏不多。跑了一下Inside,这个性能要求不高,帧数比较稳定,但是画面贴图似乎有些问题。

如果我们想玩原生的Windows游戏,最近几年苹果似乎也更关注这一块了。原来推出的game porting toolkit,现在也更新到2.0了。

  • 支持更多的游戏技术。
  • 改进的图形和计算兼容性。
  • 光线追踪。
  • AVX2指令集。
  • 性能提升。

还有开源的Whisky以及更可靠稳定支持更多游戏的商业化软件crossover。之前掉到Whisky里没折腾好,后面有空换crossover试试。从国外老哥的测试体验来看,效果还不错的。要是有M4Pro,打一下低画质的黑悟空问题不大。什么?你想要超高画质吗,那得加钱。可能加钱也解决不了,我也蹲一个他人的测评。

功耗

都说M4芯片的Mac Mini功耗低,性能不俗。我实测休眠时功耗在3W左右(外接了一个雷电4),一般性工作时,功耗在15W左右(接两个显示器),拔掉外置的显示器,功耗在10W左右。 而在LM Studio中跑模型时,GPU用满,功耗能到40W-50W左右,性能释放上还是不错的。

基于这么点功耗,日常使用上应该也不需要关机了。但是或许是这款机器太小了,日常使用上,它还是有一点温度的,温温的,不至于烫手。冬天可作暖手宝了:) 而雷电4的硬盘盒,那玩意还是有一点烫的,我不乐意摸它。

软件推荐

作为一个老MacOS用户,要说有什么推荐的软件,那可太多了。这里就随便介绍几个。

我以前介绍过Mac的神器Alfred,这次再推荐一个Raycast,它和Alfred一样,都是快速启动工具,但是Raycast的UI更简洁,启动更快,功能更强大。然而我还是老Alfred用户,因为我付费终生会员啦~进一步使用参考以前的文章MacOS神器之Alfred workflow概览

苹果的硬盘空间卖这么贵,一个好用的磁盘清理软件还得常备。我我主要用免费好用的Tencent Lemon,它清理的比较干净,自定义选项多,而且有苹果官方的认证。

因为这次在JD购买Mac Mini,顺便被赠送了一个Parallels Desktop的两年激活码。久闻大名,但不想用盗版,既然你给我也笑纳了。它支持Windows、Linux、MacOS跨平台运行,安装Windows后,体验和Windows原生安装没啥区别,启动速度也挺快。当然,毕竟是虚拟机,性能上相比原生还是差一些。对于偶尔要开个Windows使用下专属软件的,这个还是挺方便的。

输入法上,我个人因为习惯使用五笔输入,使用的是清歌输入法(https://qingg.im/)。绿色无广告,因为我太讨厌在输入法中看到广告了。

作为一个开发者,我日常还会使用iTerm2, VScode, Neovim, Cursor,docker等工具,可能你并不需求这些。 最近看评测发现一个不错的监控软件,比原生的活动监视器更好一些。本来最开始的是asitop,但是它对最新的芯片支持不好,又有了一个更好的选择macpm。你只需要:

1
pipx install macpm

然后就可以使用macpm命令查看Mac的性能了。在终端下还挺好看: 一边开着LM Studio跑模型,一边开着macpm监控着机器的性能,感觉还不错。

总结

虽然很多文章力推新版的M4 Mac Mini,但是我还是要看个人使用场景。以下是我认为要慎重的几点:

  • 它只是个主机,你需要再额外配备不少东西,如果你不啥都凑和着用,整体上成本也不会太低。
  • 性能上日常使用不错,但想玩大模型,还是得上Max或者Ultra芯片。
  • 想用它玩游戏,花这个价钱,我推荐你出门找主机PS5什么的2000+更香,或者Windows才是你的归宿。
  • 如果你有Macbook Pro,其实可以不用考虑它。

但是,如果你喜欢折腾又爱玩,那么还是不错的选择:

  • 当个小主机?我相信它的出现,让国内各种小主机厂商都要哀伤了。
  • 当个软路由?长期稳定跑,功耗又低,也很香。
  • 组个NAS?这么多雷电口,芯片又强,至少文件服务能力杠杠的。

或者就用用它的内容缓存能力,让你家各种Apple设备看iCloud照片、视频更流畅,下载安装各更新快捷,也能收获良好的情绪价值。

BTW:怎么感觉下一代的Macbook Air还是会让我心动啊!我的病并没有被Mac Mini治好。

附一张全家福,后面这台主机估计会进入机柜。

本篇文章就写到这里,感谢阅读。如果期待看到更多玩法,欢迎点赞鼓励,您的支持是我写作的动力!

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号

云上云下,资源自由:混合 Kubernetes 集群搭建指南

2024-10-16 08:00:00

Featured image of post 云上云下,资源自由:混合 Kubernetes 集群搭建指南

云厂商的CVM好贵啊!除了丐版引流招新人入坑,其它稍高点的配置那价格就像在考验智商。作为小小个人如果想搭建一套比较完善的kubernetes集群的话,那资源就捉襟见肘了。如何更经济实惠的应对资源不够这个问题,本文给一点新思路。

基本原理

我曾经基于云上一些CVM搭建了一套k8s集群,简单玩玩尚可。但是如果想搞点花样,比如想加点监控或分析,Prometheus一上,内存就咔咔告警;日志采集分析一下?ES或loki等一上,存储也就喵喵在叫。这阻碍我们学习知识了呢?这不能忍。

看过我之前文章的朋友可能知道,我家里有不少小型计算单元与存储,NUC等小主机几台、NAS几台。这有算力、内存、存储都充足。那么,咱可以优势互补,搭建一个云上云下混合的k8s集群吗?当然可以。

本文将借助tailscale及其相关工具(有比较详细的全套使用指南)构建一个云与家庭一体的混合集群,并且在之上我们使用更轻量的k3s发行版搭建一个k8s集群。这样,我们就可借用云上主机,获得如公网IP,备案等必要的便利,同时需要计算资源时又依托于自己家庭内的低成本CPU内存及存储等,真正物尽其用。接下来和我一起来操作并且验证一下我们的想法吧!

构建网络

不知道你听过WireGuard没有,我在之前一篇文章有过介绍。作为已经被Linux内核集成的一个VPN,它使用上还稍有不便。但我们借助于一款名叫tailscale的软件可快速构建安全可靠的虚拟局域网(VPN)。以下是GPT提供的介绍:

Tailscale是一款基于WireGuard协议开发的虚拟专用网络(VPN)工具,它允许用户将能够访问外网的设备聚合成一个虚拟的局域网,实现设备之间的安全通信。以下是关于Tailscale的详细介绍: 内网穿透:Tailscale可以将不同地点的设备连接起来,实现内网穿透,使得用户可以像在同一个局域网内一样访问这些设备。 设备连接:支持Windows、macOS、Linux、iOS和Android等多个平台,使得不同平台的设备可以轻松连接。 安全性:使用加密协议保护通信内容,确保数据的安全性。

它有强大的自动内网穿透能力,并且即使不能直连也会自动使用中继。据我观察,经常是使用几次中继收发消息后,自动打洞成功走上直连。你要说这么强大的软件,它有什么缺点不?那当然有:

  1. tailscale不是免费的,免费有接入节点限制,还有些高级功能有限。
  2. 中继都在国外,若需要经过它转发性能会有影响。

但是,别慌,这都能很好的解决。有个开源的控制面headscale,我们可以自建它解决官方各种限制。我们还可以自建中继服务DEPER。下面会一一道来,不过为了保证数据安全,首先我们得准备一个SSL证书。

使用ACME.sh自动申请证书

这里假定你已经有一个自己的域名了,我们可以借助于开源的acme.sh快速帮我们申请一个时长3个月的SSL证书,并且可以配置自动续期。这个证书是为了后面headscale服务安全性而用的,它也内置了自动申请证书功能,但目前(2024-10)只支持HTTP认证,这对国内域名不友好。我发现我在腾讯云上使用时不成功,可能是备案相关要求导致。所以下面我们就自己手动通过DNS认证来申请吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安装acme.sh
curl https://get.acme.sh | sh -s email=[email protected]

# 方案一:http验证:以standalone模式获取证书(如果网站未备案,在国内机器上操作可能报错,国外机器这里或能成功)
acme.sh --issue --standalone -d example-domain.com

# 方案二:以DNS验证。 我域名托管在cloudflare中,那么就需要设置环境变量。
export CF_Token="你在cloudflare的API token"
export CF_Email="你域名的Email"

acme.sh --issue --dns dns_cf -d example-domain.com

如果你想要调试一下,可以先使用测试域名,避免因为一些报错被平台限制申请。这相关信息查看这里。 申请成功后,你将会获得证书相关的文件,把它拷贝出来放到了/home/ubuntu/example-domain.com备用。

搭建tailscale的控制面headscale

安装配置headscale

An open source, self-hosted implementation of the Tailscale control server. 多亏了开源的方案headscale,我们可以免费使用tailscale。查看这里的指引可轻松搭建。上面有容器方案使用起来简单,比如我们基于docker-compose来部署:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
version: "3.7"

services:
 headscale:
 image: headscale/headscale:v0.23
 restart: unless-stopped
 container_name: headscale
 network_mode: host
 volumes:
 - /home/ubuntu/headscale/config:/etc/headscale
 - /home/ubuntu/example-domain.com:/certs/
 command: serve

上面为了简化网络,使用了host模式。重点是这里面的config配置怎么搞?你可以参考官方仓库中的示例稍作修改,主要是一些地址端口等小小变化,限于文章篇幅,以下仅diff出我的修改与原版差异:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@mercedes:~/headscale/config$ diff config.yaml config.yaml.ori
13,14c13
< #server_url: http://127.0.0.1:8080
< server_url: "https://example-domain.com:8812"
---
> server_url: http://127.0.0.1:8080
19,20c18,19
< listen_addr: 0.0.0.0:8812
< #listen_addr: 127.0.0.1:8080
---
> # listen_addr: 0.0.0.0:8080
> listen_addr: 127.0.0.1:8080
80c79
< enabled: true
---
> enabled: false
221,222c220,221
< tls_cert_path: "/certs/example-domain.com.crt"
< tls_key_path: "/certs/example-domain.com.key"
---
> tls_cert_path: ""
> tls_key_path: ""

这样我们将配置及前面的SSL证书挂载到容器内供headscale使用。之后docker-compose up -d即可启动,注意检查一下启动日志是否一切正常。

在节点安装tailscale并注册

以我使用的Ubuntu系统,我们可以通过apt安装,其它系统官方也有很详细的安装指南。

1
2
3
4
5
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

sudo apt-get update
sudo apt-get install tailscale

安装完毕后可以输入tailscale version查看一下版本。注意如果太老旧的版本和上面的headscale可能不太兼容哦,用当前最新一般没啥问题。

然后我们将node注册到headscale中:

1
sudo tailscale up --login-server=https://example-domain.com:8812 --accept-routes=true --accept-dns=false

正常会收到一个链接。

1
2
3
To authenticate, visit:

 https://example-domain.com:8812/register/mkey:316fabaca478e6d35190bb8e331004b33c0f8c52c98f202105af41465014a269

打开页面,根据指示在headscale中注册,在注册前,我们可能要先创建一个namespace(新版本叫user)了。

1
2
3
4
5
# 创建一个namespace(user),名叫union。别以为我写错了headscale,前一个是容器名,后一个是命令名
docker exec -it headscale headscale users create union

# 注册
headscale nodes register --user union --key mkey:316fabaca478e6d35190bb8e331004b33c0f8c52c98f202105af41465014a269

然后查看有如下结果:

1
2
# 查看节点列表
docker exec -it headscale headscale nodes list

可能会输出类似的信息,能看到自己对应主机online了就注册成功了。

开启或搭建中继服务DERP

这一步可选,因为tailscale在全球提供了不少节点。但可惜没有大陆内的,延迟可能会有一定影响。如果你希望更快,我这里给一个我的搭建命令,一行docker命令即可(注意它也同样使用了上面申请的SSL证书)。

1
2
3
4
5
6
7
8
9
docker run --restart always \
--name derper-with-verify-2024-10-15 \
--network host \
-v /home/ubuntu/example-domain.com/:/app/certs \
-v /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock \
-e DERP_CERT_MODE=manual \
-e DERP_ADDR=:12345 \
-e DERP_DOMAIN=example-domain.com \
fredliang/derper:latest

这个搭建好后,如果想让headscale使用我们自己搭建的DERP服务,则需要修改一下config。在DERP服务那一块:

1
2
paths:
 - /etc/headscale/derp.yaml

同时把这个文件挂载到容器内。这个文件的示例官方仓库也有,我的是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# /etc/headscale/derp.yaml
regions:
 900:
 regionid: 900
 regioncode: gz-mercedes
 regionname: Tencent Guangzhou
 nodes:
 - name: 900a
 regionid: 900
 hostname: example-domain.com
 stunport: 3478
 stunonly: false
 derpport: 12345

修改完上面headscale相关配置并重启后,我们可以在节点上执行tailscale netcheck看到定义的中继服务器信息。

加入其它节点并验证网络

除了云上我们购买的节点外,现在我们加入自己家里的一些虚拟机进来,让它未来成为混合集群的一部分。比如我想把家里一台Ubuntu24.04TLS作为节点一部分。这块操作和前面完全一致,先安装tailscale再注册到headscale,然后我们使用node list查看:

1
2
3
4
ubuntu@mercedes:~/headscale$ docker exec -it headscale headscale nodes list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | union-test | union-test | [j1D6O] | [aDlUn] | union | 100.64.0.3, fd7a:115c:a1e0::3 | false | 2024-10-22 06:46:41 | 0001-01-01 00:00:00 | online | no
2 | outside | outside | [TWC9a] | [MVLAj] | union | 100.64.0.4, fd7a:115c:a1e0::4 | false | 2024-10-22 06:46:40 | 0001-01-01 00:00:00 | online | no

如果你按上面部署了自己的DERP,相信速度会快一些。

1
2
3
4
5
6
$ tailscale ping 100.64.0.3
pong from union-test (100.64.0.3) via DERP(gz-mercedes) in 18ms
pong from union-test (100.64.0.3) via DERP(gz-mercedes) in 19ms
pong from union-test (100.64.0.3) via DERP(gz-mercedes) in 19ms
pong from union-test (100.64.0.3) via DERP(gz-mercedes) in 18ms
pong from union-test (100.64.0.3) via DERP(gz-mercedes) in 19ms

也可以使用tailscale status等检查网络状态。

到这里,我们的混合集群的网络部分已经Ready了,现在你可以看到这两台原本互不相干的机器,它们成为了一个局域网,并且是有着当前最安全的加密通信支撑的安全网络。接下来我们继续完成我们的集群部署以及功能验证。

补充题外话:tailscale好处不止于此,作为VPN,借助于它的Router能力,可以让你很方便的从外界安全的访问整个家庭网络,打通一切网络隔阂。鉴于此事于主题无关,就不在此多言啦。

部署集群

经过前文折腾,网络已经就绪。我已经迫不及待想上个集群了,虽然我曾经尝试过部署一个原生的kubernetes集群,但对于咱们小型集群,使用原生的Kubernetes占用资源还是太多了,我更推荐的还是k3s发行版。 我之前文章也提到过如何创建一个k3s集群。下面简单说明一下过程,并且增补一些实用技巧。

master节点

考虑到集群的稳定性,家里或许没有云上持续可靠,所以master节点可以考虑放在云上。但如果这个集群打算跑很久,云上VM你可能不续费的话,那么以家中节点为master也是可行的,要注意备份集群控制面数据即可。

1
2
3
4
5
6
7
8
9
export INSTALL_K3S_CHANNEL=v1.26
export K3S_TOKEN=union-k3s-on-2024-10-23

curl -sfL https://get.k3s.io | sh -s - server \
--disable=traefik \
--node-ip=100.64.0.4 \
--node-external-ip=<机器外网IP> \
--flannel-iface=tailscale0 \
--tls-san=<机器外网 >

我们在上面的outside(香港)机器上执行如上命令,即可部署一个k8s 1.26版本。对几个参数作个解释:

  • 关闭了默认的traefik。这是我的使用经验,因为未来我们使用它还需要再定义一些参数等(如自动申请证书啥的),故自行管理和部署更好。
  • 节点IP使用的是我们tailscale分配的局域网地址。这样集群内部通信才能走咱们上面的混合网络。
  • node-external-ip指定外网IP,用于LB等暴露时绑定的IP。
  • flannel-iface确保每个节点都使用其 Tailscale接口,不然底层flannel网络不知道应该基于这个设备转发。
  • tls-san需要指定机器的外网IP。为了你的k3s集群api未来可以从外部访问(kubeconfig)。

如果你安装时遇到网络问题进展缓慢,那么可以看下面node的部署,选择国内加速的镜像脚本即可。

你说都2024年10月了,还在用1.26版本?虽然我也尝试了较新的1.30版本,遇到了比较神奇的连接kubernetes api失败的报错,coredns一直起不来。

[INFO] plugin/ready: Still waiting on: “kubernetes”
[INFO] plugin/ready: Still waiting on: “kubernetes”
[INFO] plugin/ready: Still waiting on: “kubernetes”

有issue提可能和内核约莫有些关联,但是咱用的是Ubuntu24.04了,内核不应该低呀。也有issue提到tls-san添加上kubernetes svc ip,尝试均无效。想着换个版本呗?回退到1.26这个我常年用的老版本后,一切正常。此事颇为蹊跷,待我搞完这篇文章再来诊断诊断,在此记下来避免你也同样遇到。

node节点

我把master选择了云上机器,那么node我就选择加入自己家内部的虚拟机啦。我们部署一个node节点,在这里有意切换到一个国内安装脚本。

1
2
3
4
5
6
export INSTALL_K3S_CHANNEL=v1.26
export K3S_TOKEN=union-k3s-on-2024-10-23

curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -s - agent --server https://100.64.0.4:6443 \
--flannel-iface=tailscale0 \
--node-ip=100.64.0.3

启动成功后,我们就有了一个可以小小集群了。查看一下:

1
2
3
4
$ sudo k3s kubectl get nodes
NAME STATUS ROLES AGE VERSION
union-test Ready <none> 73s v1.30.5+k3s1
outside Ready control-plane,master 12h v1.30.5+k3s1

实用技巧

如果你遇到过镜像拉取很慢或者就完全拉不下来,可以为k3s server或agent配置一下代理,根据是master还是node情况来修改/etc/systemd/system/k3s.service.env/etc/systemd/system/k3s-agent.service.env文件,在末尾添加上https代理即可。比如

1
2
K3S_TOKEN='union-k3s-on-2024-10-23'
HTTPS_PROXY=http://<ip>:<port>

然后重启k3s/k3s-agent服务即可。

成果验收

如果上面都操作OK了,集群也调试就绪了。这么辛苦折腾一番,那么是时候收点好处了。我们搞个简单的,部署一个网站,网站实际托管在家里VM上,但是流量入口自云主机。你要问为什么这么搞?那是因为家宽都不让开放80/443端口呀,你得有云上主机才可以。

我们直接部署个nginx来演示,注意这里主动把它调度到家庭VM上(union-test节点)。

 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
apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment
spec:
 replicas: 1
 selector:
 matchLabels:
 app: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 nodeName: union-test # 这里为了测试故意将它调度到家庭内部设备上
 containers:
 - name: nginx
 image: nginx:latest
 ports:
 - containerPort: 80

# k3s默认的servicelb提供了基于VM的LB能力,免费!
apiVersion: v1
kind: Service
metadata:
 name: nginx-service
spec:
 type: LoadBalancer
 selector:
 app: nginx
 ports:
 - protocol: TCP
 port: 80
 targetPort: 80

我们借助k3s中内置了servicelb,它通过iptables一系列规则,帮助你在无需使用云厂商的LB而达到对外暴露服务的目的。然后我们即可以直接访问云上主机的IP地址和端口即可实际访问到家里的计算资源。 跨主机访问Nginx

有这个基础,未来可想象的空间就比较大啦。例如将数据库部署在家庭 NAS 上,将机器学习模型训练任务放到家庭闲置 GPU 上等。咱家里大House不一定非得装小姐姐,也可以放点有用的数据嘛:)

后记

其实k3s的较新版本也直接支持集成tailscale了,并且也支持我们自己部署的headscale作为控制面,详情查看上面链接。不过我们一步步设置过来,对全过程更清晰明白啦!

如此折腾之后,便可充分的利用自建资源,再也不担心搞点研究碰上资源不够的窘境了。未来或许你只需要在云上留下一台最普通的机器即可,连啥负载都可以不放,想想可不要太香,哗哗的省钱。

本篇文章就写到这里,感谢阅读。如果期待看到更多玩法,欢迎点赞鼓励,您的支持是我写作的动力!

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号

SteamDeck在手,国庆远程游玩PS5指南

2024-09-30 08:00:00

Featured image of post SteamDeck在手,国庆远程游玩PS5指南

在黑神话悟空发售时,我因为购买的PS5版本,不能随时随地玩到它真是着急。于是利用手上的SteamDeck研究了一下远程串流,在网络稳定情况下,效果还不错。最近因为工作忙,文章一直拖着没发,转眼一个月过去,国庆要到了!这不,已经在老家躺着,想起来把这篇文章补上。主要是网上看不到特别完整和严谨的过程,导致还有一点小坑,聊以记录。如果你像我一样,希望外出时也能愉快玩游戏的话,可能对你有用。

准备工作

你需要一台SteamDeck,无论是否OLED都一样,当然OLED开HDR配合PS5或能获得更爽快的游戏体验。 其次,为了远程游玩,你家最好是有外网IP。如果不是静态的(多数人都不是),那就DDNS,这些网上有一堆教程就不在此细聊。 最好确定你家的上行带宽有10-20Mb/s,不然有可能玩起来会糊?虽然官方文档说5Mb以上,但我实测使用更多。

远程游玩配置

给PS5打开远程游玩

点开PS5的设定,再点击系统,打开远程游玩,点击启动。根据提示操作即可。 remoteplay

安装串流软件Chiaki4deck

有一个跨平台的串流软件叫Chiaki(千秋),然后它有个SteamDeck优化版本叫Chiaki4deck。为了安装它,我们需要按如下步骤:

  • 切换SteamDeck到桌面。按Steam键,电源,切换至桌面。
  • 找到并安装Chiaki4deck。点击左下角第三个图标(软件管理中心),呼出键盘(右手边三个点键+X),搜索Chiaki,找到chiaki4deck并安装。
  • 回到游戏模式桌面。在桌面找到Return to Gaming Mode点击即可。

配置Chiaki4deck进行局域网串流

为了排除干扰,第一次进行串流建议先配置好本地局域网串流,后面再升级为可远程在外串流回家。这里具体操作当时没有记录,主要要点有:

  • 查询PS5的ipv4地址。在网络查看连接状态中可看你的PS5的IP地址。
  • 启动Chiaki4deck。Host填入上面PS5的IP地址。
  • 查看PSN Account-ID。打开 https://psn.flipscreen.games/ 输入PSN用户名查询Encoded ID (for Chiaki)。特别注意这个ID不是昵称,也不是登录账户。我就搞不清输错了多次,汗。
  • 查看PIN码。打开PS5主机,在远程游玩那查询安全码。

大概是这几步,我们本地就能将PS5的画面和操作串流到这台SD上。 PS:为了使体验更加流畅,建议PS5使用有线连接到路由器或交换机上。这个能大大降低延迟。我实测有线除了延迟低,还非常平稳。以下从路由开始走内网ping延迟,而无线WiFi内网还是有近10ms的延迟并且时有抖动。

1
2
3
4
5
6
7
[admin@MikroTik] > ping 192.168.1.189
 SEQ HOST SIZE TTL TIME STATUS
 0 192.168.1.189 56 64 1ms157us
 1 192.168.1.189 56 64 1ms21us
 2 192.168.1.189 56 64 995us
 3 192.168.1.189 56 64 1ms151us
 4 192.168.1.189 56 64 963us

随时随地,外部串流回家爽玩

本地局域网串流只解决了一小部分问题,比如我是将客厅的PS5串流后,可以在卧室床上一边玩SD。因为整体延迟低,游戏体验还是很不错的。但我们还可以想要更多,比如外出露营,比如像国庆回老家等,手痒想玩怎么办?显然,我们要配置端口转发。那么到底转发哪些端口呢?网上资料有些不太准确,我们还是查看官方资料稳妥一些。另外有个知识点,网络的NAT类型,也可以顺带了解一下。在PS5的网络中可以检测到,我家的是NAT2类型。对此GPT4o有如下解释:

1
2
3
4
5
在游戏主机(如PS、Xbox)中,NAT类型通常被描述为三种:
开放(Open)/类型1:直接连接,无限制。
中等(Moderate)/类型2:通过路由器连接,有一定限制。
严格(Strict)/类型3:限制较多,连接问题较多。
这些类型主要关注的是设备在联网游戏中的连接性能和兼容性。

为了使PS5支持远程游玩,查看官方文档:

Remote Play uses UDP Port 8572 to connect to your PS5 console, PS4 console, or mobile device. Check to make sure that your router, internet service provider, and mobile carrier aren’t blocking this port.

在PS4的文档中查看到如下描述:

If your router does not support UPnP, adjust the router’s port-forwarding setting to allow communication to your PlayStation 4 console from the Internet. The port numbers that are used by Remote Play are 9295 (for TCP), and 9296 and 9297 (for UDP). If these ports are unavailable, it might be possible to use ports 9295 through 9304 (for TCP and UDP).

这块官方太零散了,没有更全面的说明,最后从Reddit找到一个详细的探讨,我们要转发的端口列表是:

服务名称 内部端口 外部端口 协议
远程唤醒 987 987 TCP/UDP
Remote Play App 8572 8572 UDP
PS5 认证 9295 9295 TCP/UDP
PS5 传输 9296 9296 UDP
PS5 数据传输 9297 9297 UDP
PS5唤醒 9302 9302 UDP
额外端口 9303 9303 TCP/UDP
9304 9304 TCP/UDP
9305 9305 TCP/UDP
9306 9306 TCP/UDP
9307 9307 TCP/UDP
9308 9308 TCP/UDP

如果你嫌麻烦,可以使用自己路由器的UPnP功能,端口映射或许能简化一些。我上文提到刚将路由切换到了RouterOS,所以我写如下几条命令做转发(我的PS地址是192.168.1.189)。

1
2
3
4
5
6
7
/ip firewall nat
add chain=dstnat protocol=tcp dst-port=9295 action=dst-nat to-addresses=192.168.1.189 to-ports=9295 comment="Port forward TCP 9295 to PS5"
add chain=dstnat protocol=udp dst-port=8572 action=dst-nat to-addresses=192.168.1.189 to-ports=8572 comment="Port forward udp 8572 to PS5"
add chain=dstnat protocol=udp dst-port=9295-9297 action=dst-nat to-addresses=192.168.1.189 to-ports=9295-9297 comment="Port forward UDP 9295-9297 to PS5"
add chain=dstnat protocol=udp dst-port=9302 action=dst-nat to-addresses=192.168.1.189 to-ports=9302 comment="Port forward UDP 9302(wakeup) to PS5"
add chain=dstnat protocol=udp dst-port=9303-9308 action=dst-nat to-addresses=192.168.1.189 to-ports=9303-9308 comment="Port forward UDP other ports to PS5"
add chain=dstnat protocol=tcp dst-port=9303-9308 action=dst-nat to-addresses=192.168.1.189 to-ports=9303-9308 comment="Port forward TCP other ports to PS5"

如果你的防火墙设置得比较严格,可能要像我这样再放通它转发到内部。以下举个例子:

1
2
3
4
5
/ip firewall filter
add chain=input protocol=tcp dst-port=9295 action=accept comment="Allow external access to PS5 TCP 9295"
add chain=input protocol=udp dst-port=9295-9297 action=accept comment="Allow external access to PS5 UDP 9295-9297"
add chain=input protocol=udp dst-port=9302 action=accept comment="Allow external access to PS5 UDP 9302"
...

效果验收

我尝试了使用手机开热点给SD连接,效果不太好,时有模糊,或许是手机的带宽较低。在公司内部试了一下,能比较流畅的游玩,并且能够在很低功耗的情况下,60帧,画面不错。 在SD中游玩PS5 查看路由器这过程的流量情况: 路由器上的流量1路由器上的流量2

我用它打广谋什么,还是流畅轻松的一顿乱按就过了。不知道打虎先锋手感怎么样,会不会气得摔机器:)

其它提示

一些报错&问题

  1. 遇到过报错:Failed to initialize Steam Session: Failed to initialize FFMPEG Decoder. [E] H265 Codec not available 解决方案:换h264即可。

  2. 遇到Chiaki4deck连接不上。可能路由器开启了IPv6,我关闭后恢复正常。但这点还需要未来再确认,当初开ipv6想再加速一下公网的连接速度,没想到最后连接不上了:(

一些操作

在使用Chiaki(千秋)时,因为SD的按键和PS手柄不一样,如何退回到桌面呢?只需要按:Steam+B即可,或者Steam+左键。 要退出Chiaki,则按L1+R1+L3+R3.

后记

这个方案也就临时解解游戏瘾,还是比不上直接在大屏显示器或电视机上,本地直接玩耍的爽快度。如果你喜欢折腾,不妨玩一下。它倒是有几个好处:

  • 随时随地像带着PS5一样,和朋友炫一下你的白金和操作?
  • 超高续航。可看到上面截图中几有6.2W。超低功耗,外出SD可以玩好几个小时PS游戏。如果小游戏不太需要特别实时的操作,还是不错的。
  • 省钱。买一份游戏,在两种设备上玩。并且如果你的PS5是用光盘的,那你真能省到姥姥家。哈哈,我知道你不在乎:P

文中也提过,本来想顺道折腾一下ipv6后对比一下实际效果再分享的,遇到一点小问题。未来有机会继续折腾,暂时趁国庆把这篇文章补上吧,也希望能帮助到观看它的你。感谢阅读!

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

参考资料

扫码关注公众号

放弃Unifi,再建最快最稳的路由

2024-08-25 08:00:00

Featured image of post 放弃Unifi,再建最快最稳的路由

起因

夏天一到,家里的弱电箱就化作了全国第五大火炉,我在几年前有几篇博文提到我换到了Unifi全家桶,用了近三年还算稳定,除了夏天偶尔掉线,偶尔连个WiFi要等一会。

我曾经天真的以为将几个WiFi SSID划分不同VLAN隔离,绑定不同的网关等对接旁路由而其它网络不受影响,哪想到现在天天是主路由直接嘎了。这套方案中发热大头是USG,想不到小小的这玩意竟成了烫手一坨的铁块。

家人几次反应网络又连不上了,更多次说网速好卡打个页面都转好久。这必须不能忍受啊!折腾起来。

是直接对USG改散热,加风扇加铜片导热?这方案可能不错,有朋友推荐过这么做,但毕竟要拆机,或许未来二手就更不值钱了,何况我手上也没那些东西。

我想起来闲置了几年的一个路由器Mikrotik RB760iGS,当年从公司一老哥二手收的,价格忘记了,想着折腾一下,被它的系统RouterOS的高度可定制性以及操作方式搞得怵了。

但是,这一次我重拾挑战它的信心,我来了,必将搞定,并且踏平它!网页秒开,8K随便上,测速得全满,不能便宜了宽带运营商,这样黑悟空下载也贼快!

网络背景之我想要什么

我家的网络背景,以前是在电信提供的光猫后,通过USG桥接来拨号,然后后面跟一个US8-60W-PoE作为交换机,它支持PoE供电,连接有2个AP(UAP Pro和UAP IW),客厅和卧室网速得到保障,其它端口用来接网线到内部其它房间。我的AP对外提供了四个SSID,其中三个是IoT、Guest、Main,它们是属于不同的VLAN,分别是10,30,50。另一个是Mgr,用于管理网络的。同时做了一些防火墙策略,比如IoT的网络不能访问其它网络,我可不想让摄像头等有权限访问你家NAS共享的文档吧,不给这机会。而对于网络设备路由/AP等维护,只能在专属的Mgr网络,避免其它网段可以管理网络设备,这也是我所不希望的。

前情提要在此结束,现在我们的任务是,拿着手上尘封已久的Mikrotik hEXS(型号RB760iGS)来把USG下岗了去,其它功能不打折扣。本文将带着一路趟过去,遇难则战,遇怪全清。但谦虚地说,我也只是个折腾Mikrotik的新手,分享一下经历是主要目的,希望对同好有些微帮助。

替换之路之Mikrotik扶正指南

初识

我顺便翻了下小黄鱼,这RB760iGS还能值个几百,看来闲置它这么久是我暴殄天物了。 它的具体参数我们可以通过system/resource print查看,以下是我升级完固件之后的版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 uptime: 12h36m57s
 version: 7.15.3 (stable)
 build-time: 2024-07-24 10:39:01
 factory-software: 6.46.3
 free-memory: 195.2MiB
 total-memory: 256.0MiB
 cpu: MIPS 1004Kc V2.15
 cpu-count: 4
 cpu-frequency: 880MHz
 cpu-load: 1%
 architecture-name: mmips
 board-name: hEX S
 platform: MikroTik

对于一个路由,这个配置不算低了。满载1000M时,发现CPU仅一半左右。

重置之后,全新出发

自从几年前通电折腾了一下,早就忘记密码了。我就全新重置它。虽然我已经配置好正常运行一段时间了。为了写此文章的严谨性。我又又又一次重置了,一切清零,重头开始,希望确保我的所有操作你也可重现。看在这么科学严谨的份上,欢迎点关注、点赞、在看等,对我持续输出高质量有价值文章都是非常好的鼓励。

在不通电时,按着机身侧边Reset键(别说你找不到,我说了在侧边,别习惯性找小针孔,它就是个很大的按扭,吼吼),再接通电源。根源SFP的灯的状态,在几次闪烁后处于常亮时,请松开你紧按RESET的小手指,然后设备会滴几声,过一小会机器则全部重置所有设定。

马上你就迎来第一个坎,找不到设备了。说明书上说的好好的默认192.168.88.1连接不上了。别着急,这次我们清得很干净,所有网口啥配置也没有,啥IP也没绑定。这时我们要借助一个工具WinBox,这就让我的Macbook难办了,幸亏有一台Windows PC在家躺着,去官方下载WinBox,大小仅有几Mb,下载飞快。然后连接上你的路由到电脑,试着用WinBox发现它。(2024-8-29更新: MacOS用户有福了,官方已经放出Winbox的Mac测试版本,亲测可用。)

Tips: 重置后找不到设备,在windows下使用winbox通过二层网络通信来设置。它根本不需要三层的IP等设施。

现在到我们准备开始配置的时候了,先想一想吧。当前总共5个网口,第一个PoE IN,最后一个PoE OUT。我计划使第一个口作为WAN口使用,这比较好记(但为啥它是个PoE In哇,有知道使用场景的告诉我,反正感觉当主路由时,这PoE口也不好供电?)其它4个组为本地LAN口,别管我插哪个,都得一样,因为我记性不好。

脱离Winbox,我想要网页和终端

因为只有Windows版本的winbox,故最开始我们只能在上面做配置。其实我不喜欢页面上点来点去,找半天UI和输入框,但这一部分我们先体验一下网页上的操作。我们单纯使用网页,做到可以普通的拨号上网的程度。当然我要说这一切也都是可以直接用命令行搞定的。

WAN口分配及拨号

点击Interfaces,可以看到默认已经有5个etherX口了,我们把第一个ether1改名为WAN,方便识别。然后Add New中选择PPPoE Client,选择Interface为WAN口,并修改Dial Out的User/Password即可。

LAN口上网

我们需要把其它几个口作为同等的LAN口,这时需要借助网桥,如果你的系统是重置后的,有个默认网桥,我嫌它难听,把这个网桥重命名为LAN了。然后其它4个口都放在这个网桥下(按最左的-主动移除ether1,因为它给我们当WAN口用了)。然后我们给这个网桥一个IP吧?点IP->Addresses,Add New,如下填写即可。

接下来需要设置NAT,如果你拨号成功接上网线上不了网,很可能是这一步不对。操作很简单:IP->Firewall->NAT, 只修改两个地方, Chain: srcnat, Action: masquerade,接口等不用填写,确定即可。到现在,你用网线连接随便一个LAN口,配置一个静态IP在和网桥一个网段,就可以快乐的上网了。

但这显然不够,我们要自动分配IP,这不是标准操作吗?别急,多加一步就好。

借助DHCP服务器帮你自动分配IP

上面LAN口上网要主动设置IP,为了能让LAN口连接的设备自动获取IP,需要启用一个DHCPserver。它的位置在IP->DHCP Server。修改Interface为网桥名LAN,并设置合适的Address Pool。这里推荐使用DHCP Setup来通过引导设置。其中DHCP Addres Space: 192.168.1.0/24, DNS: 223.5.5.5

另外还要注意,DHCP Server的网关需要单独创建,不然DHCP Server会显示红色。可以在IP->Address来创建,注意选择Interface为LAN,另外特别注意地址要写192.168.1.1/24(和你前面同网段),注意后面的24,没填系统不会提示,但不这么写可能ping不通网关,或者不会自动分配IP,我被这坑大,我得去补网络常识,知道为什么的不妨留言告诉我。

是时候切换回Macbook继续操作了

当我们把上面配置好后,三层的IP通信就具备了,现在可以关闭Windows,继续掏出Macbook来配置。当然,你完全可以继续在winbox上操作,它也很好用,不是吗?:)

我们连接上任何一个口,获得一个IP。然后访问http://192.168.1.1就可以打开管理端页面,默认用户名admin,密码为空直接进。可以看到和winbox类似的界面,后面的配置我们都在这里进行。

复杂网络进阶配置,我有多样选择

VLAN划分,网页初上手

前言提到,除了拨号占用了一个口,我们把另外4个口搭建了一个bridge(网桥),现在我们要让它支持未来连接交换机上的三种VLAN的流量。 继续试一下网页,这里我们在bridge下创建三个vlan,点击Interface, Add New, VLAN。这些相关设置比较繁琐,我更推荐使用命令行操作,如果你就喜欢UI操作,那么也完全可行的,大概如下。 有了这些vlan后,还需要相应创建DHCP服务器,给他们分配相应网段的IP。同样建议使用向导帮你创建,当然为了名字干净,自己手工创建Ip Pool再绑定dhcp server也是不错的,这个操作我后面会用命令再来一次。

命令行助力,飞速设置

如果要在UI上一个个点,相信你手已经累了,我们学一下更高效的办法吧?你不用懂太多,照猫画虎会吧?

现在让我们点击Web界面右边的Terminal或者你通过ssh连接到这台设备,你就开启了进阶的大门。我们不妨一边敲命令,一边看UI上有哪些变化。再来一次上面几个繁琐的VLAN创建和设定的过程。

  1. 创建VLAN接口
    在桥接接口上创建VLAN接口。
1
2
3
4
/interface vlan
add interface=LAN name=IoT vlan-id=10
add interface=LAN name=Guest vlan-id=30
add interface=LAN name=Main vlan-id=50
  1. 分配IP地址
    为每个VLAN接口分配IP地址。
1
2
3
4
/ip address
add address=192.168.10.1/24 interface=IoT
add address=192.168.30.1/24 interface=Guest
add address=192.168.50.1/24 interface=Main
  1. 配置DHCP服务器 为每个VLAN配置DHCP服务器。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/ip pool
add name=pool10 ranges=192.168.10.2-192.168.10.254
add name=pool30 ranges=192.168.30.2-192.168.30.254
add name=pool50 ranges=192.168.50.100-192.168.50.200

/ip dhcp-server
add address-pool=pool10 interface=IoT name=dhcp10
add address-pool=pool30 interface=Guest name=dhcp30
add address-pool=pool50 interface=Main name=dhcp50

/ip dhcp-server network
add address=192.168.10.0/24 gateway=192.168.10.1 dns-server=223.5.5.5
add address=192.168.30.0/24 gateway=192.168.30.1 dns-server=223.5.5.5
add address=192.168.50.0/24 gateway=192.168.50.34 dns-server=223.5.5.5,119.29.29.29

可以看到我们原本要点击很多次的行为,都简化了,更重要的是,可以直接复制直接抄作业。另外上面我有意将Main网络的网关设置为旁路由IP了,懂的人,只要会心一笑即可:)

Unifi控制器上适当修改

我们以往的DHCP分配以及一些VLAN设定都是在控制器上的,既然现在USG已经不用了,它的分配DHCP功能也没人给它执行了哇。所以我们可以修改Networks中的设置,将其Router那块勾选VLan-only Network。如下图: 细心的小朋友会发现,为啥你有个没切换过来,唉,不瞒您说,我也试过多种方式,它会报错,这台设备是公司的,已经离线,但在离线设备中找不到。我尝试过直接找控制器的DB存储(mongodb),也没find到这个玩意。我发现目前不修改好像也没啥特别影响,等哪天突发奇想或把公司电脑带回来再试着拯救一下。如果你有知道原因的,不妨告诉我一下。

整体上这个没啥问题,我估计不修改直接下线USG也一点影响都没有,可咱有点强迫症的就在这里把能改的就改了吧。

大功告成,测个速吧

看起来VLAN也搞定了,能够按我们需要将玩家对接到不同VLAN。我们先来测个速?以下是无线网络(网关不走旁路由): 好像速度还行,毕竟Macbook的握手速度和AP会有影响。那再试一下有线网络,台式机等还是走的有线呢?不测不知道,一测吓一跳: 我可是付了1000M的宽带钱,怎么可以只有200M的速度,我不能答应!这绝对会影响黑悟空下载更新对吧(其实我已经下载好了,这网络没折腾清楚根本没时间玩)。我又一次端了个小板凳,蹲坐在门口的弱电箱前,进行专项突破。

一定是有优化手段的,我不相信这路由的配置居然会跑不满千兆。一边测速一边看负载,CPU都不到50%,很快又降回了更低负载。通过一些搜索可能的优化手段,找到了Fasttrack技术,官方在这里有介绍:

Layer 3 Hardware Offloading (L3HW, otherwise known as IP switching or HW routing) allows to offload some router features onto the switch chip. This allows reaching wire speeds when routing packets, which would simply not be possible with the CPU.

简单说就是一种硬件加速技术,特别适用于处理已经建立的连接(如TCP或UDP连接),这些连接的状态通常是“已建立”(established)或“相关”(related)。当一个连接被FastTrack处理时,路由器会跳过一些通常由软件处理的复杂检查,直接在硬件层面转发数据包。官方说因为内存限制,只开TCP比较好,于是有如下几行优化设置。

1
2
3
4
/ip/firewall/filter
add action=fasttrack-connection chain=forward connection-state=established,related hw-offload=yes protocol=tcp
add action=fasttrack-connection chain=forward connection-state=established,related hw-offload=no
add action=accept chain=forward connection-state=established,related

我们再来测试一次: 同时看pppoe接口的流量监控,直接打满1GB了,感觉我们的工作快可以收工了!!然后再拿运营商平台也测了一下,这结果可以满意了。

没有安全性,做啥主路由?

然而还没完呢,作为一个主路由,会面临互联网上各种扫描(攻击),适当的安全设置会让我们更省心。作为一个可高度配置的路由器,我们来简单设置一些安全选项吧。

1. 允许已建立和相关的连接

1
/ip firewall filter add chain=input connection-state=established,related action=accept comment="Allow established and related connections"

2. 丢弃无效的连接

1
/ip firewall filter add chain=input connection-state=invalid action=drop comment="Drop invalid connections"

3. 丢弃所有来自WAN的数据包

1
/ip firewall filter add chain=input in-interface=WAN action=drop comment="Drop all incoming traffic from WAN"

4. 限制SSH和Winbox访问 假设你允许过来管理IP是192.168.1.0/24,其它网段不能连接这个路由。

1
2
3
4
/ip firewall filter add chain=input protocol=tcp dst-port=22 src-address=192.168.1.0/24 action=accept comment="Allow SSH from local network"
/ip firewall filter add chain=input protocol=tcp dst-port=8291 src-address=192.168.1.0/24 action=accept comment="Allow Winbox from local network"
/ip firewall filter add chain=input protocol=tcp dst-port=22 action=drop comment="Drop all other SSH access"
/ip firewall filter add chain=input protocol=tcp dst-port=8291 action=drop comment="Drop all other Winbox access"

5. 阻止端口扫描

1
2
/ip firewall filter add chain=input protocol=tcp connection-state=new tcp-flags=fin,psh,urg action=add-src-to-address-list address-list=port-scan address-list-timeout=1d comment="Add port scan sources to address list"
/ip firewall filter add chain=input src-address-list=port-scan action=drop comment="Drop traffic from port scan sources"

6. 允许ICMP(Ping

1
/ip firewall filter add chain=input protocol=icmp action=accept comment="Allow ICMP (Ping)"

7. 记录和监控日志 可选,但会导致较多的日志,我最后没用它。

1
/ip firewall filter add chain=input action=log log-prefix="Firewall Input: " comment="Log all input traffic"

8. 允许DNS请求

1
/ip firewall filter add chain=input protocol=udp dst-port=53 action=accept comment="Allow DNS requests"

9. 允许HTTP和HTTPS访问 不添加它,我们后面丢弃全部其它流量后,咱的网页控制端都不能访问喽。

1
2
/ip firewall filter add chain=input protocol=tcp dst-port=80 action=accept comment="Allow HTTP"
/ip firewall filter add chain=input protocol=tcp dst-port=443 action=accept comment="Allow HTTPS"

10. 最后,丢弃所有其他输入流量 黑名单的思路,只放通我们想放通的流量,其它全拦截掉。

1
/ip firewall filter add chain=input action=drop comment="Drop all other input traffic"

11. 差点忘记了,IoT网络和其它网络隔离性
我们只要几句话就拦截啦。

1
2
3
4
/ip firewall filter
add chain=forward action=drop src-address=192.168.10.0/24 dst-address=192.168.30.0/24 comment="Block IoT to Guest"
add chain=forward action=drop src-address=192.168.10.0/24 dst-address=192.168.50.0/24 comment="Block IoT to Main"
add chain=forward action=drop src-address=192.168.30.0/24 dst-address=192.168.50.0/24 comment="Block Guest to Main"

同时我们也限制一下访问路由的权限。

1
2
3
4
5
6
# 允许 HTTP 流量 (端口 80)
add chain=input action=accept protocol=tcp dst-port=80 src-address=192.168.1.0/24 comment="Allow HTTP from 192.168.1.0/24" place-before=0
add chain=input action=accept protocol=tcp dst-port=80 src-address=192.168.50.0/24 comment="Allow HTTP from Main VLAN" place-before=0

# 确保其他网段的流量被拒绝
add chain=input action=drop dst-address=192.168.1.1 comment="Drop other subnets from accessing router" place-before=0

相信有了这些配置,你的路由会安全很多。

私有场景应用

DDNS

我家内部有NAS,有网盘等,有一些服务偶尔要从外部访问。这就需要将动态的IP绑定到某个域名上,即DDNS服务。但是routeros不像之前用的Unifi家的设备,在pppoe之后可以有后置脚本触发。但可以曲线救国呢?如果你记得我以前分享过通过webhook来进行一些逻辑分离的话,这就是一个好场景。这里借助于它的Script,我们点击System->Script,然后AddNew,添加一个脚本取名叫update-ddns,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
local mydomain "your.domainsite.com"
:local currentIP [/ip address get [find interface="pppoe-out1"] address]

# Remove the subnet part from the currentIP
:local currentIP [:pick $currentIP 0 [:find $currentIP "/"]]

# Specify the DNS server for resolving
:local dnsServer "119.29.29.29"
:local resolvedIP [:resolve "$mydomain" server=$dnsServer]
:log info "resolvedIP: $resolvedIP"

:if ($currentIP != $resolvedIP) do={
 /tool fetch url="https://webhook.{换成你的webhook地址}/hooks/public-new-ip" mode=https http-header-field="Authorization: Bearer {换成你的Key}" http-data="ip=$currentIP"
 :log info "DDNS updated with IP $currentIP,old:$resolvedIP"
} else={
 :log info "DDNS already up-to-date with IP $currentIP"
}

然后在System->Schaduler设置定时任务,设置一个合适的间隔比如5分钟,在OnEvent中输入

1
/system script run update-ddns

如果一切正常触发,你可以在Log中查看到一些日志信息。但你也可能遇到脚本会报错,可能是DNS解析有问题,你可以直接执行 /ip dns set servers=223.5.5.5,8.8.4.4 allow-remote-requests=yes来给路由设置一个默认DNS,之后或许脚本就能正常执行了。

DDNS之外的官方解法

我们可以借助IP->Cloud来自动上报到一个官方的DDNS域名中。 这里的配置很简单,这样当你的WAN口IP变化后,会在一定间隔同步更新到官方的DNS Name这里,你可以自己解析试试看。接下来我们只需要把我们的目标域名进行CNAME即可,这个比较简单就不细说啦。

感觉上是官方重新实现了我们上面脚本的做法,这里不够完美,因为居然有Update Interval,它应该是触发式而不是定时的,不是吗?我未实测过,未来有机会再验收一下:)

端口转发

我们有一些服务需要对外暴露,这时候就需要进行端口转发了,在这里很简单,你可以同样选择UI操作或者命令行。

1
2
3
4
5
# 添加NAT规则
/ip firewall nat add chain=dstnat protocol=tcp dst-port=9443 action=dst-nat to-addresses=192.168.50.96 to-ports=8080 comment="Port forward TCP 9443 to 192.168.50.96:8080"

# 允许输入链的9443端口访问
/ip firewall filter add chain=input protocol=tcp dst-port=9443 action=accept comment="Allow external access to TCP 9443"

这样当我从外部访问回家时,这条路就畅通了,比如: 而我们家其它端口都是全部拒绝的。

其它小问题

时间不准

你可能在重置后发现时间不对,可以设置NTP对时,System->SNTP_Client(旧版本是这名),新版本叫 System->NTP Client。如果没先设置dns,你需要填写IP: ,比如这是一个NTP服务器地址: 120.25.108.11

诡异问题

在RouterOS中对权限管理非常的严格,一旦没权限很多事都做不了。我遇到过admin居然没有full的权限,没有ssh权限,我可难倒我了。我试这多种方法,包括把固件更新到最新(话说固件升级真的很简单),可以看这里。我直接将2018年的固件升级到2024年7月份了,非常顺畅,配置也无任何兼容性问题。

不可,我的权限问题仍然没解决,最后的最后,唯一手段就是完全Reset路由!然后权限恢复正常啦~ 我想出现这种情况难不成是我在折腾时手贱点了一下?RouterOS如果没有开启Safe Mode,每一次点击和设置都是立即生效的?若果真如此,官方在这时应该弹个框告诉我啊!

PoE问题

前文说过,有办法不再外接电源吗?那么eth0就不能作WAN口,当作电源输入来使用,奇葩操作是让交换机给它供电,同时提供数据通路给交换机。似乎可行,总感觉怪怪的呢?

总结

到这里,本次关于这个全新路由Mikrotik的探索就结束了。以上也仅是本人这两三天的折腾收获和一些经验。网上找相关资料不太多,自己实验探索居多。分享出来希望对有想法折腾的同学若有参考,能帮到你少熬几天,那就太棒了。我已经有多个晚上凌晨2-3点坐在弱电箱里扯头发了。谁叫我遇上问题都得上,将就一下?好难受,不可以!

总结使用下来,对于RouterOS这个系统我现在可太喜欢它了。全透明可控,每一个流量视图,甚至它的防火墙的设置修改等,比起iptables好用不知道哪里去。我的每一次comment都变得有意义了,定制化路由器厂商真有其过人之处。

至于有人说用RouterOS分流进行某些高阶探索,多拨(这简单),新版本还加入了WireGuard等,这些且待未来有需求再研究吧,但我已经近不及待想推荐你也试用一下啦!

现在再次思考,当初选择USG作主路由,我有没有三思?在这个炎热的夏天,你有你的选择,我选择Mikrotik!这个真正专业的路由。

PS:在我折腾这玩意的前两天(每天下班后深夜),感觉有点悬,怕自己不能快速修复网络。同步下单的ER-X路由在我修复网络的同天到了。我露出了苦涩的笑,这个坑刚完,新坑又来了哟。 我们或许都听说过,ER-X也是个相当棒的路由,号称弱电箱神器,适合当主路由,还是Ubiquiti家族产品之一。现在幸福的烦恼来了,两个当打之明星,谁能真正转正呢?又再经过了几个晚上的折腾,成功让ER-X同样完美升级为主路由,具体如何行动,里面有多少辛酸和故事,且听下回分解。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号

自制实时AI语音对话

2024-07-29 08:00:00

Featured image of post 自制实时AI语音对话

背景

在GPT刚出来时,恰逢家里小娃经常要嚷嚷着听故事,讲一个什么什么的故事,可是苦于想象力的匮乏,要胡编一个带有主题思想的故事还挺难。作为程序员老爸,那时我就打算给它造一个AI爸爸出来。所以,这个文章算是这过程中的副产品吧。

我在写着写着,拖着拖着,GPT-4o演示出来了,那语音对话等能力加上及时响应性,一度我都打算中止相关开发和验证了。可是没想到,OpenAI他们也拖着拖着,一直没对外放出这块能力,居然又熬到了我快赶上了:D

我们基础的框架是实时对话能力,然后为了让这个AI爸爸更像样,就需要基于自己的声音训练一个模型。第一部分是一个比较基础的各种能力的调用,关注点在实时性上。第二点略难,但所幸已经有比较多种的开源实现了,只要整合即可。这篇文章先介绍第一部分。

实时对话的几步

我希望通过语音或文字输入和AI交流,背后的AI可自行选择。显然传统的拿到回答再通过语音合成,再播放的话,这延迟就太大了,我们需要在全程利用实时流式处理,以此提升响应速度,最后基本可以在1-2s内语音回答。整体的效果如下:

UI

以下按执行顺序,分各步骤聊一下。

AI交互过程:语音输入

一般来说,我们通过语音输入的内容不会太长。尽管我们称之为实时,但这一步并非真正实时的。当前的AI也不能将prompt分多次上传呀?所以借助于录音之后,我们进行一次语音识别。当前(24年7月)大模型的多模态还不支持语音输入,故语音识别自然是免不了的。 我们先说录制,我是MacOS机器,所以直接调起了sox程序实现内容的录制。我们只需要几句话就封装了一个录音机:

 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

type Recorder struct {
 buf bytes.Buffer
 cmd *exec.Cmd
}

func NewRecorder() *Recorder {
 return &Recorder{}
}

func (r *Recorder) Start() {
 r.buf.Reset()
 r.cmd = exec.Command("sox", "-d", "-t", "wav", "-")
 r.cmd.Stdout = &r.buf

 err := r.cmd.Start()
 if err != nil {
 log.Fatal(err)
 }
}

// Stop recording
func (r *Recorder) Stop() {
 err := r.cmd.Process.Signal(os.Interrupt)
 if err != nil {
 log.Fatal(err)
 }

 // Wait for the recording process to finish
 err = r.cmd.Wait()
 if err != nil {
 log.Fatal(err)
 }

 log.Debugf("Recording stopped. recorded %d bytes", r.buf.Len())
}

func (r *Recorder) Buffer() *bytes.Buffer {
 return &r.buf
}

接下来是将它转换为文字。这一步选择有很多,可以用OpenAI的whisper,我正好发生腾讯云有免费送很多的token,就用它的ASR功能了。已经有现成的SDK可调,几行代码即可搞定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 将音频内容转为文本返回,出错返回err
func (a *ASRClient) ToVoice(fileType string, fileContents []byte) (string, error) {
 request := asr.NewSentenceRecognitionRequest()

 // 设置上传本地音频文件
 request.SourceType = common.Uint64Ptr(1)
 request.VoiceFormat = common.StringPtr(fileType)
 request.EngSerViceType = common.StringPtr("16k_zh")

 // 将buf的内容base64编码后设置给request.Data
 d64 := base64.StdEncoding.EncodeToString(fileContents)
 request.Data = common.StringPtr(d64)
 request.DataLen = common.Int64Ptr(int64(len(d64)))

 response, err := a.client.SentenceRecognition(request)
 if err != nil {
 return "", fmt.Errorf("fileType:%v, len:%v, err:%w", fileType, len(fileContents), err)
 }

 return *response.Response.Result, nil
}

AI交互过程:流式输出

我们知道AI的输出有流式和非流式,而我们后面还要将文字内容合成为语音呢,为了更快速的有响应,我们必然需要使用流式输出。然后可以一边将输出传递给另一个线程去做语音合成。

流式输出没啥可讲的,但AI交互这一块,还是要再提一下之前文章介绍过的一站式多模型管理:One API实用指南。最开始我实现了多种模型的支持,后面接触到它后,果断将所有代码都移除了,那是人家做的事情,我这重复劳动就没意义了。

实时语音转换

有了比较实时的结果后,我们一边读出来,一边交给某个地方去合成(当然未来会是在本地模型或自己搭建的服务上,毕竟我是想用自己声音来讲故事的嘛)。我继续使用腾讯云的语音合成能力,非实时的方式也试过,调用后中间等待要花几秒时间,虽然说如果内容比较多时,刚开始的几秒还好,但是,咱发现有实时合成,那必须得上啊。

虽然有实时接口,但是上传并不支持持续数据流,只是下行实时,合成一部分语音就提前下发,于是我们得将要转换的内容拆分一下,我以一句话的句号来分段,这样一段段上传,然后将合成的内容提前播放,正好可以用播放掩盖掉后面合成的用时。 核心代码也很简单:

 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

// StreamTTS 语音合成
// 读取textChan中的数据,将它以。分割,然后合成语音
func StreamTTS(voiceType int64, emotionCategory string, textChan chan string, audioChan chan []byte) {
 var buffer strings.Builder

 var wg sync.WaitGroup
 wg.Add(1)

 s := tts.NewRealTimeSpeechSynthesizer(int64(appId), secretId, secretKey, voiceType, emotionCategory, speed)

 sentenceChan := make(chan string)
 // 启动一个 goroutine 来处理语音转换, 这样才能按顺序
 go func() {
 defer wg.Done()
 index := 1
 for sentence := range sentenceChan {
 log.Debug("----------------------------------")
 log.Debugf("正在转换第[%d]段语音中,文字内容为:%s ", index, sentence)
 s.Run(sentence, audioChan)
 index++
 log.Debug("----------------------------------")
 }
 log.Info("**语音转换全部结束!!**")
 }()

 index := 1
 for {
 select {
 case resp, ok := <-textChan:
 if !ok {
 log.Debugf("TextChan closed, buf len:%d", buffer.Len())
 // Channel 已关闭
 if buffer.Len() > 0 {
 // 发送句子到通道
 sentenceChan <- strings.TrimSpace(buffer.String())
 }
 goto END
 }

 // log.Debugf("Speech recv [%q]", resp)
 buffer.WriteString(resp)

 // 按句号分割句子
 content := buffer.String()
 sentences := strings.Split(content, "。")

 // 重置 buffer
 buffer.Reset()

 for i, sentence := range sentences {
 sentence = strings.TrimSpace(sentence)
 if sentence == "" {
 continue
 }

 if i == len(sentences)-1 && !strings.HasSuffix(content, "。") {
 // 最后一个句子可能是不完整的,保存到 buffer 中
 buffer.WriteString(sentence)
 } else {
 sentenceChan <- sentence + "。"
 // log.Debugf("发送第[%d]句子到sentenceChan:%s", index, sentence)
 index++
 }
 }

 }
 }
END:
 close(sentenceChan)
 log.Debug("sentenceChan closed")

 wg.Wait()
}

起了两个goroutine,一个做分段,一个做合成。 发现腾讯云的语音合成音色挺多的,有小女孩的童真声音,也有粤语、四川话等,试用了还蛮有意思。同时还有情感的描述,学习到语音合成也有它的语法,可以通过在文字中做一些标注,使用不同的声音和不同的情感等。想着如果一段话,借助AI帮标注出来,再让它合成或许情感会更丰富一些。

流式语音播放

声音多数都是流媒体传播的,所以不少播放器都是支持流式播放的。我让声音合成返回了mp3,然后借助于ebitengine/oto的库完成播放工作,这个库有点小问题,播放有时会卡住不播,估计是某种并发下的异常情况没处理好,稍微walk around了一下。

 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

func (p *MyPlayer) Play() {
 log.Debug("正在调用播放器来播放语音")
 go p.readFromStream()

 for {
 if p.readFinished || p.buffer.Len() >= minDataSize {
 log.Debugf("已经收取足够语音数据,正在初始化解码器, readFinished:%v, buf len:%d", p.readFinished, p.buffer.Len())
 // 确保播放器在播放前初始化
 if p.player != nil {
 log.Debug("播放器已经存在,关闭现有播放器")
 p.player.Close()
 }

 p.initializePlayer()

 if p.player != nil && !p.player.IsPlaying() {
 log.Debug("未在播放中,调用播放器来播放语音, Play!")
 time.Sleep(500 * time.Millisecond)
 p.player.Play()
 }

 if p.player != nil && p.player.IsPlaying() {
 log.Debug("播放器已经进入Playing")
 break
 }

 log.Debug("未在播放中,将会重置播放器!")
 }

 time.Sleep(100 * time.Millisecond)
 }
}

func (p *MyPlayer) readFromStream() {
 for {
 select {
 case data, ok := <-p.audioStream:
 if !ok {
 // Channel is closed, stop reading
 log.Warnf("audioStream closed? 将退出播放器")
 p.readFinished = true
 return
 }
 p.buffer.Write(data)
 log.Debugf("收取语音数据:%d, 剩余长度:%d", len(data), p.buffer.Len())
 }
 }
}

其中开始播放时,需要收集到一些数据才能开始。初始化后立即播放有时会没有声音,未来可能会找找有没更好用的库:) 暂时通过上面一些补丁算是稳定能用。

TUI装修美化一下

基本功能有了,最开始我是命令行的方式,想着毕竟要写个文章介绍一下,就用个最简单的TUI包装一下吧,临时学习了一下bubbletea库的使用。有小朋友和我说这个库有点重杂,刚开始看是这样的,当把遇到的几个问题和BUG修改完,似乎这个库也就是轻量、没那么复杂了:) 改BUG果然是学习东西的好门路!

基本UI

想着有些东西可能会调整着玩的:如模型、音色、情感等,就将它们放在左边的设置里了。当然这个列表还可以不断扩展,我这里仅是做个演示就没塞太多选项。 然后是聊天历史,我们虽然是语音交流,文字也要同步展示出来,所以就给了个地方显示一下,也可以顺便看看上下文都是些什么。 最后,输入中我们想着有时不方便语音说话,所以文字输入和语音输入同时支持。要再画个UI单独是个录音等,我又不想这么搞。我居然创造性的让这个文本框在点击它时进入录音状态,再次点击取消录音。同时这个文本框聚焦后能输入文字,就这么一石二鸟了,哈哈!想不到吧:)

以下是最后的UI,也是蛮粗糙的,但基本够用就行吧。 TUI

UI和逻辑分离

最开始随便在UI中调起一些逻辑,那真是个灾难,各种让UI无响应等,于是乎,学习到了bubbletea的消息处理机制。我们只需要简单的定义一些消息,将触发的一些动作作为一种行为转发出去即可。有点像我们过往Windows编程中的事件响应。

 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
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 //... 省略
 case "enter":
 switch m.currentFocus {
 case 0:
 selectedModel := m.modelList.SelectedItem().(item)
 m.notificationCh <- fmt.Sprintf("选择了模型: %s", selectedModel.Title())
 m.eventChan <- Event{Type: "model", Payload: selectedModel.Title()}
 case 1:
 selectedTone := m.toneList.SelectedItem().(item)
 m.notificationCh <- fmt.Sprintf("选择了音色: %s", selectedTone.Title())
 m.eventChan <- Event{Type: "tone", Payload: selectedTone.Title()}
 case 2:
 selectedEmotion := m.emotionList.SelectedItem().(item)
 m.notificationCh <- fmt.Sprintf("选择了情感: %s", selectedEmotion.Title())
 m.eventChan <- Event{Type: "emotion", Payload: selectedEmotion.Title()}
 case 3:
 log.Debug("选择了历史记录框")
 m.notificationCh <- "选择了历史记录"
 case 4:
 question := m.questionInput.Value()
 log.Debug("问题输入完毕", question)
 m.questionInput.SetValue("")
 m.notificationCh <- fmt.Sprintf("输入了问题: %s", question)
 m.eventChan <- Event{Type: "question", Payload: question}
 }
 // 省略

然后在其它线程中响应它即可。这期间为了处理鼠标的点击事件(对的,要自己封装去判断点到了哪个控件),发现边框等会占用1个字符,发现屏幕的宽度和高度是以字符数量来计算的。在我的Macbook Air屏幕上,它只有178x48大小。所以建议像我一样,手绘一个UI布局好好计算一下。整个UI为了让窗口自适应,需要比较好的响应tea.WindowSizeMsg事件,并且让我们的设置都是一个相对值。

中文字宽问题

在处理历史记录的展示过程中,为了让排版稍好看点,我们需要根据能显示的长度对结果的文字做重排。突然发现这也是个技术活,最开始是有些库只支持英文,对中文或其它文字长度计算有误。后面我基于go-runewidth库封装了下计算逻辑,这样中文+英文等计算宽度稍好一点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// WrapWords 使用mattn/go-runewidth库来精确计算字符宽度
func WrapWords(s string, maxWidth int) string {
 var builder strings.Builder
 currentWidth := 0
 for _, r := range s {
 charWidth := runewidth.RuneWidth(r)
 if r == '\n' {
 currentWidth = 0
 }
 if currentWidth+charWidth > maxWidth {
 builder.WriteString("\n")
 currentWidth = 0
 }
 builder.WriteRune(r)
 currentWidth += charWidth
 }
 return builder.String()
}

后记

上述的实现已经在GitHub中开源了,需要请查看talk-with-ai,如果对你有用,欢迎Star。接下来的文章,可能会聊一下如何复刻你的声音,然后在这里用上。听听自己声音的回答到底是惊喜还是恐怖,你说呢?

EOF

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:

扫码关注公众号