飞鸟真人头像
关注

Luvit入门实践(一) Hello, Http Server

目录

1. Lua版本的Node.js

📊 用的人多么?—— 社区规模与影响力

🛠️ 目前能干啥?—— 核心用途与场景

2.💻 在 Windows 11 上的安装方法

🚀 快速入门:从“Hello World”开始

📚 学习资源推荐

3. 三个基本工具

🔧 三个工具的区别:从引擎到工具集

3.1 🌐 配置 HTTPS 服务器

3.2 📊 编写 Windows CPU/内存采集脚本

3.3 使用外部库简化代码

📉 代码量对比

🛠️ 使用 coro-spawn 的修正版脚本

4. 崩溃即退出的问题

为什么 Luvit 默认会崩溃?

在 Luvit 里实现“不崩溃”的两种方法

方法一:用 coroutine.wrap + xpcall 包裹你的业务逻辑

方法二:用 luv 的 makeCallback 思路(如果你能改底层)

实际建议

5. http客户端与爬虫

📦 内置模块情况

5.1 JSON:有内置模块

5.2 🕷️ 做简单爬虫的可行性

5.3 📊 总结

5.4 下载B站的音乐与视频

🎯 核心思路与前置准备

📝 Luvit 爬虫脚本

6. 扩展livit库

6.1 纯 Lua 扩展:像写普通脚本一样简单

6.2 涉及 C 扩展(FFI 或 Native Module):需要编译,但不用重编 Luvit

6.2.1 针对具体需求(XML/Protobuf)

6.2.2 官方对 FFI 的定位

6.2.3 如何用 FFI 调用 DLL

7. 嵌入式使用luvi或者luv

方案一:通过 luvi 嵌入(最接近 Luvit 的方式)

🧩 它和 Luvit 的关系

🔗 交互能力:正是我们需要的

📦 版本选择

方案二:仅使用 luv 绑定(更轻量)

🔩 luv:底层的“零件”

📦 luvi:打包好的“成品”

⚖️ 核心区别一览


1. Lua版本的Node.js

Luvit是一个在github.com上开源的(近 4000 Star)Lua工具包;它的主要用途是让你能用 Lua 语言,以类似 Node.js 的方式构建高性能的网络服务或命令行工具。

换句话说,luaJit没有异步能力,添加了Libuv以后就有了事件驱动的异步能力,再扩展http,json,zip,等能力,就可以方便的编写简单的小程序了。那么作者为啥要重新造轮子呢?答案是:

  • 启动速度:Node.js 启动约 35ms,而 LuaJIT 只要 0.64ms。如果你的脚本是“跑完就退”的短任务,Luvit 的启动速度是数量级的优势。

  • 内存占用:Node.js 基础内存约 44MB,LuaJIT 约 2.6MB。如果你要在资源极其受限的环境(比如嵌入式设备、或需要开几十个进程的场景)运行,Luvit 的优势很明显。

代码仓库:GitHub - luvit/luvit: Lua + libUV + jIT = pure awesomesauce · GitHub

预编译版本:GitHub - truemedian/luvit-bin: Luvit build scripts and weekly binary releases · GitHub

📊 用的人多么?—— 社区规模与影响力

它属于“小而精”的项目,在 GitHub 上获得了不错的口碑,但活跃度和普及度还不能和 Node.js 相提并论。

  • 社区认可度高:在 GitHub 上拥有约 3,950+ 个 Star 和 376 个 Fork,在代码仓库中排名前 2%。这说明它在对异步 I/O 和 Lua 感兴趣的开发者群体中,是一个受关注和有价值的项目。

  • 并非大众工具:它的核心用户群是 Lua 开发者或需要嵌入式脚本能力的人。与 Node.js 动辄百万的周下载量相比,Luvit 的用户基数非常小。

  • 项目成熟但更新缓慢:项目本身已有超过 10 年的历史,技术栈非常稳定。最近的代码提交在几个月前,但正式的版本发布停留在 2021 年(v2.18.1),这表明项目已进入维护状态,而非高速迭代期。

🛠️ 目前能干啥?—— 核心用途与场景

Luvit 的核心价值在于,它把 Node.js 那种“事件驱动、非阻塞 I/O”的编程模型带到了 Lua 生态中。你可以把它看作一个可以独立运行 Lua 脚本的“服务器引擎”。

  • 构建网络服务:你可以用 Luvit 直接编写 HTTP 服务器,无需 Nginx 或 Apache。代码风格非常像 Node.js,很适合用来快速开发 API 服务、微服务或网络代理。

  • 异步编程与脚本工具:它既是一个库,也是一个可执行文件。你可以用它编写需要处理大量并发连接(如 WebSocket 服务、TCP 客户端)的工具,或者任何需要高效文件操作、系统调用的自动化脚本。它同时支持 回调(Callbacks) 和 协程(Coroutines) 两种异步风格,后者能让代码看起来像同步写法,更易于阅读。

  • 灵活的运行时定制:Luvit 的生态是模块化的,你可以按需组合。比如,你可以只使用它底层的 luv(Lua 的 libuv 绑定)库,或者只使用它的包管理工具 lit,而不必引入整个框架。

总的来说,Luvit 是一个为 Lua 开发者设计的、成熟且专注的异步运行时。如果熟悉 Lua,并且想用它来做后端网络编程,它会是一个很趁手的工具。

2.💻 在 Windows 11 上的安装方法

目前主要有两种方式,推荐第一种:

1. 使用官方 PowerShell 脚本(官方推荐) 这是 Luvit 官方文档目前给出的 Windows 安装指引,它会自动下载所需的 lit 和 luvit 工具。

  • 打开 PowerShell(建议以管理员身份)。

  • 执行以下命令(注意,原文档中有一处笔误,这里的命令已修正):

     PowerShell -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = 'Tls12'; iex ((new-object net.webclient).DownloadString('https://github.com/luvit/lit/raw/master/get-lit.ps1'))"
  • 脚本运行完成后,建议将生成的可执行文件所在目录(通常在当前用户目录下)添加到系统的 PATH 环境变量中,这样就能在任何地方使用 luvit 命令了。

2. 使用社区维护的预编译包(备用方案) 如果上面的脚本因网络问题失败,可以考虑使用 truemedian/luvit-bin 项目,它每周都会自动构建适用于 Windows x64 的二进制文件。你可以在其 GitHub Releases 页面手动下载 luvit.exe 和 lit.exe,然后放入一个目录并同样添加到 PATH 中。

🚀 快速入门:从“Hello World”开始

安装成功后,就可以体验“像 Node.js 一样写 Lua”了。

  1. 验证安装:在终端输入 luvit,如果进入一个带 > 提示符的交互式界面(REPL),就说明安装成功了。

  2. 创建第一个脚本:新建一个文件 server.lua,写入以下代码,这是一个简单的 HTTP 服务器:

     local http = require('http')
     ​
     http.createServer(function (req, res)
       local body = "Hello world\n"
       res:setHeader("Content-Type", "text/plain")
       res:setHeader("Content-Length", #body)
       res:finish(body)
     end):listen(1337, '127.0.0.1')
     ​
     print('Server running at http://127.0.0.1:1337/')
  3. 运行脚本:在终端中执行 luvit server.lua,然后打开浏览器访问 http://127.0.0.1:1337/,就能看到 “Hello world” 了。

📚 学习资源推荐

  • 官方文档:访问 luvit.io 查看完整的 API 文档,其设计思路与 Node.js 高度相似,如果你有 Node.js 基础会非常容易上手。

  • 交互式教程:luvit-quest 是一个有趣的交互式工作坊,通过“闯关”的形式引导你学习网络编程。

考虑到你的任务是开发状态监控,熟悉了基本的 HTTP 服务器后,下一步可以重点研究一下 Luvit 的 childprocess 模块,它对于执行系统命令来采集监控数据会很有帮助。

3. 三个基本工具

下载的 truemedian/luvit-bin 提供的三个文件,正好对应了 Luvit 技术栈的三层结构。

🔧 三个工具的区别:从引擎到工具集

你看到的三个可执行文件,构成了 Luvit 生态的完整链条,其分工非常明确:

  • luvi.exe:核心引擎。它是用 C 语言编写的底层运行时,打包了 LuaJIT、libuv(异步 I/O)、OpenSSL 等原生库。你可以把它理解为一个“专用的 Lua 解释器”,它本身不含 HTTP 等高级 API,只提供最基础的 I/O 和网络套接字能力。它还能将你的 Lua 代码和依赖打包成一个独立的可执行文件。

  • luvit.exe:Node.js 风格的平台。它本身是用 Lua 写的,构建在 luvi 之上。它实现了一套与 Node.js 高度相似的 API(如 http, fs, tls 等),让你能用 Lua 写出类似 Node.js 的代码。你平时写代码、跑脚本,主要用的就是它。

  • lit.exe:包管理与构建工具。它是 Luvit 世界的 “npm + 构建器”。你用它来安装第三方库、管理项目依赖,以及将你的脚本打包成可独立分发的二进制文件。

简单来说:luvi 是发动机,luvit 是装好发动机和内饰的汽车,而 lit 是车库和改装工具。

3.1 🌐 配置 HTTPS 服务器

在 Luvit 中配置 HTTPS 服务器,核心是使用 https 模块(或 tls 模块)并传入包含证书和私钥的 options 对象。

首先,你需要用 OpenSSL 生成自签名证书(仅用于测试):

 openssl genrsa -out server.key 2048
 openssl req -new -x509 -key server.key -out server.crt -days 365

然后,编写你的 HTTPS 服务器脚本 https_server.lua:

OpenSsl下载地址:Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions

 local https = require('https')
 local fs = require('fs')
 ​
 -- 1. 读取证书和私钥文件
 local options = {
   key = fs.readFileSync('server.key'),
   cert = fs.readFileSync('server.crt')
 }
 ​
 -- 2. 创建 HTTPS 服务器
 local server = https.createServer(options, function (req, res)
   local body = "Hello from HTTPS!\n"
   res:setHeader("Content-Type", "text/plain")
   res:setHeader("Content-Length", #body)
   res:finish(body)
 end)
 ​
 -- 3. 监听 8443 端口
 server:listen(8443, '0.0.0.0', function()
   print('HTTPS Server running at https://127.0.0.1:8443/')
 end)

运行 luvit https_server.lua,然后在浏览器访问 https://localhost:8443 即可(自签名证书会触发安全警告,这是正常的)。

3.2 📊 编写 Windows CPU/内存采集脚本

对于最简单的 Windows 主机状态采集,最直接的方法是调用系统自带的 wmic 命令,解析其输出。

创建一个名为 win_monitor.lua 的脚本:

 local http = require('http')
 local json = require('json')
 local childprocess = require('childprocess')
 ​
 -- 解析 CPU 输出
 local function parse_cpu(output)
   for line in output:gmatch("[^\r\n]+") do
     if not line:match("^Node") and not line:match("^$") then
       local val = line:match(",(%d+)$")
       if val then return tonumber(val) end
     end
   end
   return nil
 end
 ​
 -- 解析内存输出
 local function parse_mem(output)
   for line in output:gmatch("[^\r\n]+") do
     if not line:match("^Node") and not line:match("^$") then
       local free, total = line:match(",(%d+),(%d+)$")
       if total then
         return {
           mem_total_kb = tonumber(total),
           mem_free_kb = tonumber(free),
           mem_usage_percent = math.floor((tonumber(total) - tonumber(free)) / tonumber(total) * 100)
         }
       end
     end
   end
   return nil
 end
 ​
 -- 异步执行命令,完成后调用 callback(output)
 local function run_command_async(cmd, args, callback)
   local child = childprocess.spawn(cmd, args)
   local output = ""
 ​
   child.stdout:on('data', function(chunk)
     if chunk then output = output .. chunk end
   end)
 ​
   child:on('exit', function(code)
     callback(output, code)
   end)
 end
 ​
 -- 主逻辑:异步获取系统信息,然后返回响应
 local function handle_metrics(res)
   run_command_async('wmic', {
     'path', 'Win32_PerfFormattedData_PerfOS_Processor',
     'where', "Name='_Total'", 'get', 'PercentProcessorTime', '/format:csv'
   }, function(cpu_out)
     local cpu_usage = parse_cpu(cpu_out)
 ​
     run_command_async('wmic', {
       'OS', 'get', 'FreePhysicalMemory,TotalVisibleMemorySize', '/format:csv'
     }, function(mem_out)
       local mem_info = parse_mem(mem_out) or {}
 ​
       local info = {
         cpu_usage = cpu_usage,
         mem_total_kb = mem_info.mem_total_kb,
         mem_free_kb = mem_info.mem_free_kb,
         mem_usage_percent = mem_info.mem_usage_percent
       }
 ​
       local body = json.stringify(info)
       res:setHeader("Content-Type", "application/json")
       res:setHeader("Content-Length", #body)
       res:finish(body)
     end)
   end)
 end
 ​
 -- HTTP 服务器
 local server = http.createServer(function (req, res)
   if req.url == '/metrics' then
     handle_metrics(res)
   else
     res:setHeader("Content-Type", "text/plain")
     res:finish("Try /metrics\n")
   end
 end)
 ​
 server:listen(8080, '0.0.0.0', function()
   print('Monitoring agent running at http://localhost:8080/metrics')
 end)
 ​

运行 luvit win_monitor.lua 后,访问 http://localhost:8080/metrics,你将看到类似

 {"mem_usage_percent":65,"cpu_usage":0,"mem_total_kb":16495096,"mem_free_kb":5657780}

的 JSON 输出。

3.3 使用外部库简化代码

使用 coro-spawn 库确实能显著减少代码量,并且代码结构会清晰很多。它用协程(coroutine)把异步的子进程调用包装成了类似同步的写法,正好解决了你之前遇到的“协程边界”报错问题。

 ./lit.exe install luvit/coro-spawn

会在deps目录下添加相关的lua文件。

📉 代码量对比

方面原生 childprocess(修正版)使用 coro-spawn
核心逻辑需要手动管理 stdout:on('data') 和 exit 事件,通过回调嵌套处理一行 for 循环即可读取输出,waitExit() 直接等待结束
代码结构深层回调嵌套,异步流程不直观线性同步风格,易于阅读和维护
依赖无需要 lit install luvit/coro-spawn

🛠️ 使用 coro-spawn 的修正版脚本

安装库后,代码可以大幅简化:

 local http = require('http')
 local json = require('json')
 local spawn = require('coro-spawn')  -- 需要先 lit install luvit/coro-spawn
 ​
 -- 解析函数保持不变
 local function parse_cpu(output)
   for line in output:gmatch("[^\r\n]+") do
     if not line:match("^Node") and not line:match("^$") then
       local val = line:match(",(%d+)$")
       if val then return tonumber(val) end
     end
   end
   return nil
 end
 ​
 local function parse_mem(output)
   for line in output:gmatch("[^\r\n]+") do
     if not line:match("^Node") and not line:match("^$") then
       local free, total = line:match(",(%d+),(%d+)$")
       if total then
         return {
           mem_total_kb = tonumber(total),
           mem_free_kb = tonumber(free),
           mem_usage_percent = math.floor((tonumber(total) - tonumber(free)) / tonumber(total) * 100)
         }
       end
     end
   end
   return nil
 end
 ​
 -- 用 coro-spawn 执行命令,返回完整输出
 local function run_command(cmd, args)
   local child, err = spawn(cmd, {
     args = args,
     stdio = {nil, true, true}  -- 关键:忽略 stdin,开启 stdout/stderr 管道
   })
   if not child then
     return "", err or "spawn failed"
   end
 ​
   local output = ""
   -- 同步风格遍历读取 stdout
   for chunk in child.stdout.read do
     if chunk then output = output .. chunk end
   end
   child.waitExit()  -- 等待进程结束
   return output
 end
 ​
 -- 现在 get_system_info 变成了同步写法,非常干净
 local function get_system_info()
   local info = {}
 ​
   local cpu_out = run_command('wmic', {
     'path', 'Win32_PerfFormattedData_PerfOS_Processor',
     'where', "Name='_Total'", 'get', 'PercentProcessorTime', '/format:csv'
   })
   info.cpu_usage = parse_cpu(cpu_out)
 ​
   local mem_out = run_command('wmic', {
     'OS', 'get', 'FreePhysicalMemory,TotalVisibleMemorySize', '/format:csv'
   })
   local mem = parse_mem(mem_out) or {}
   info.mem_total_kb = mem.mem_total_kb
   info.mem_free_kb = mem.mem_free_kb
   info.mem_usage_percent = mem.mem_usage_percent
 ​
   return info
 end
 ​
 local server = http.createServer(function (req, res)
   if req.url == '/metrics' then
     -- 关键:用 coroutine.wrap 创建新协程来执行
     coroutine.wrap(function()
       local info = get_system_info()
       local body = json.stringify(info)
       res:setHeader("Content-Type", "application/json")
       res:setHeader("Content-Length", #body)
       res:finish(body)
     end)()
   else
     res:setHeader("Content-Type", "text/plain")
     res:finish("Try /metrics\n")
   end
 end)
 ​
 server:listen(8080, '0.0.0.0', function()
   print('Monitoring agent running at http://localhost:8080/metrics')
 end)
 ​

4. 崩溃即退出的问题

Luvit 的设计哲学就是“未捕获错误直接崩溃”,没有内置类似 Node.js 的全局 uncaughtException 机制。 但可以在自己的代码层面实现类似的效果。

为什么 Luvit 默认会崩溃?

Luvit 的底层是 luv(libuv 的 Lua 绑定),它从 C 层调用 Lua 回调时,没有用 pcall 保护。这意味着一旦回调里出错,错误会直接冒泡到 C 层,最终导致进程终止。

这与 Node.js 不同:Node.js 在事件循环的每个回调外层都做了保护,所以才能提供 process.on('uncaughtException')。Luvit 社区曾讨论过加类似的机制,但最终决定维持“硬错误”行为,强制开发者自己处理错误。

在 Luvit 里实现“不崩溃”的两种方法

方法一:用 coroutine.wrap + xpcall 包裹你的业务逻辑

既然问题出在 HTTP handler 是 C 回调,那就在 handler 内部把业务逻辑“隔离”到受保护的协程里:

 local server = http.createServer(function (req, res)
   if req.url == '/metrics' then
     coroutine.wrap(function()
       local ok, err = xpcall(function()
         local info = get_system_info()
         local body = json.stringify(info)
         res:setHeader("Content-Type", "application/json")
         res:setHeader("Content-Length", #body)
         res:finish(body)
       end, function(e)
         -- 错误处理器:返回堆栈信息
         return debug.traceback(e)
       end)
 ​
       if not ok then
         -- 出错时给客户端返回 500,而不是让进程崩溃
         res:setHeader("Content-Type", "text/plain")
         res:finish("Internal Server Error:\n" .. tostring(err))
       end
     end)()
   else
     res:setHeader("Content-Type", "text/plain")
     res:finish("Try /metrics\n")
   end
 end)

这样做的效果:get_system_info() 里的错误会被 xpcall 捕获,你可以在错误处理器里记录日志或返回错误响应,进程不会崩溃。

方法二:用 luv 的 makeCallback 思路(如果你能改底层)

Luvit 的 init.lua 里其实有一个全局的 xpcall 包装主流程,但它不覆盖由 C 层(libuv)触发的回调。理论上你可以通过修改 luv 的 C 代码,给所有回调加上保护,但这需要重新编译 luvi,代价较大。

实际建议

对于的监控脚本项目,方法一足够了。把每个 HTTP handler 里的核心逻辑用 xpcall 包起来,错误时返回 500 并打印日志,就能达到类似 Node.js 的效果。唯一的代价是每个入口处都要记得加这层包装,不像 Node.js 那样有一个全局兜底。

5. http客户端与爬虫

Luvit 有内置的 JSON 模块,但没有内置的 XML 解析器。对于你“做简单爬虫”的需求,Luvit 的 HTTP 客户端能力是够用的,但 XML 解析需要额外安装第三方库。

📦 内置模块情况

5.1 JSON:有内置模块

Luvit 现在已经有了原生的 json 模块,基于 yajl 实现,这个是内置的不需要安装。你可以直接使用:

 local json = require('json')
 ​
 -- 解析 JSON 字符串
 local obj = json.parse('{"name": "test", "value": 123}')
 p(obj.name)  -- 输出: test
 ​
 -- 生成 JSON 字符串
 local str = json.stringify({name = "test", value = 123})
 p(str)  -- 输出: {"name":"test","value":123}

需要注意,Lua 的 table 和 JSON 之间有一些语义差异(比如空 table 默认会被编码为 {} 还是 []),编码时可以给 table 设置 __jsontype 元字段来明确指定。

XML:没有内置

搜索结果中没有发现 Luvit 有内置的 XML 解析器。如果你需要解析 XML,只能通过 lit install 安装第三方包,或者自己用字符串模式匹配做简单的解析。

5.2 🕷️ 做简单爬虫的可行性

HTTP 客户端能力:够用

Luvit 的内置 http 模块提供了类似 Node.js 的 HTTP 客户端和服务端 API。你可以用它发起 HTTP 请求。不过,http.request 的回调风格比较繁琐,社区更推荐使用 coro-http 这个辅助库,它用协程包装了请求流程,代码会简洁很多:

 .\lit.exe install luvit/coro-http
 local coro = require('coro-http')
 ​
 coroutine.wrap(function()
   local result, body = coro.request('GET', 'https://example.com/api/data')
   -- body 就是响应内容
   local json = require('json')
   local data = json.parse(body)
   p(data)
 end)()

做爬虫的实际建议

对于“简单爬虫”来说,Luvit 是可行的:

  1. 发起请求:用 coro-http 获取页面内容。

  2. 解析内容:

    • 如果目标 API 返回 JSON,直接用内置 json 模块解析,非常顺畅。

    • 如果目标页面是 HTML/XML,Luvit 没有内置解析器。你需要:

      • 用 lit install 找社区库(比如搜索 XML/HTML 解析相关的包);

      • 或者用 Lua 的 string.match/gmatch 做简单的正则提取(只适合结构非常固定的页面)。

  3. 处理数据:用 json.stringify 生成结构化输出。

一个需要留意的坑:你之前遇到的 attempt to yield across C-call boundary 错误,在爬虫场景下同样会出现。如果你在 HTTP 服务器的 handler 里发起爬虫请求,记得用 coroutine.wrap 包裹爬虫逻辑。

5.3 📊 总结

能力Luvit 支持情况
JSON 解析/生成✅ 内置 json 模块
XML 解析❌ 无内置,需第三方库或手动解析
HTTP 客户端✅ 内置 http,推荐 coro-http
简单爬虫⚠️ 可行,但 HTML/XML 解析需自行解决

所以,如果你的爬虫目标主要是 JSON API,Luvit 用起来会很顺手。如果目标网页是 HTML 且需要提取结构化数据,就需要额外安装 XML/HTML 解析库,或者自己写简单的字符串提取逻辑。

5.4 下载B站的音乐与视频

坦白说,用 Luvit 写一个能直接跑的 B 站下载器,会遇到不少棘手的工程问题。不过,我为你准备了一套可运行的脚本框架,它能完成“获取视频信息 → 下载音视频流 → 调用 FFmpeg 合并”的核心流程,你可以拿它作为起点来研究。

🎯 核心思路与前置准备

B 站视频采用 DASH 协议,视频和音频是分离的 .m4s 文件,必须下载后合并才能正常播放。整个流程分为三步:

  1. 获取 CID 和播放地址:通过视频 BV 号调用 view 接口拿到 CID,再调用 playurl 接口获取音视频流地址。

  2. 下载音视频流:分别请求视频和音频的 URL,保存为本地文件。关键点:请求 CDN 资源时必须带上 Referer: https://www.bilibili.com,否则会返回 403。

  3. FFmpeg 合并:用 ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4 进行无损封装,不重新编码,速度极快。

前置准备:

  • 安装 coro-http:./lit.exe install luvit/coro-http

  • 确保 ffmpeg 已加入系统 PATH。

  • 准备好你的 B 站 Cookie(SESSDATA 字段),否则大概率只能拿到 480P,且可能因风控返回 -412。

📝 Luvit bilibili 爬虫脚本

将以下代码保存为 bili_downloader.lua:

 ./lit.exe install luvit/secure-socket
 winget install --id GnuWin32.Gzip -e
 chcp 65001

目前有个问题,就是这个版本的livit没有编译gzip模块,导致了不能直接解码,需要使用外部的gzip.exe来执行解码,

 local http = require('coro-http')
 local json = require('json')
 local fs = require('fs')
 local spawn = require('coro-spawn')
 ​
 -- ============ 配置 ============
 local VIDEO_URL = "https://www.bilibili.com/video/BV1TKha67Eob?vd_source=46553807f77d6bc390bf1eb40d81874d"
 local OUTPUT_DIR = "d:\\mp3"
 local pathJoin = require('luvi').path
 local SESSDATA = ""
 -- ==============================
 ​
 local UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 ​
 -- 从 res 中提取指定 header 的值(res[1..n] 是 {name, value} 形式的 table)
 local function get_header(res, name)
   name = name:lower()
   for i = 1, #res do
     local h = res[i]
     if type(h) == "table" and h[1] and h[2] then
       if h[1]:lower() == name then
         return h[2]
       end
     end
   end
   return nil
 end
 ​
 -- 收集响应中的 Set-Cookie
 local function collect_cookies(res, cookies)
   for i = 1, #res do
     local h = res[i]
     if type(h) == "table" and h[1] and h[1]:lower() == "set-cookie" then
       local name, value = h[2]:match("^([^=]+)=([^;]+)")
       if name then cookies[name] = value end
     end
   end
 end
 ​
 -- 带重定向跟随的 GET
 local function get_with_redirect(url, extra_headers, max_redirects)
   max_redirects = max_redirects or 5
   local current_url = url
   local cookies = {}
 ​
   if SESSDATA ~= "" then
     cookies["SESSDATA"] = SESSDATA
   end
 ​
   for i = 1, max_redirects do
     local headers = {
       {"User-Agent", UA},
       {"Accept", "*/*"},
       {"Accept-Language", "en-US,en;q=0.5"},
       {"Accept-Encoding", "identity"},
     }
     if next(cookies) then
       local cookie_str = {}
       for k, v in pairs(cookies) do
         table.insert(cookie_str, k .. "=" .. v)
       end
       table.insert(headers, {"Cookie", table.concat(cookie_str, "; ")})
     end
     if extra_headers then
       for _, h in ipairs(extra_headers) do table.insert(headers, h) end
     end
 ​
     local res, body = http.request("GET", current_url, headers)
     collect_cookies(res, cookies)
 ​
     -- 处理 gzip 压缩:写临时文件 -> 调用 gzip 解压 -> 读回
     local ce = get_header(res, "content-encoding")
     if ce and ce:lower() == "gzip" then
       local tmp_gz = "temp_body.gz"
       local tmp_out = "temp_body.out"
       fs.writeFileSync(tmp_gz, body)
 ​
       -- gzip -d -c temp_body.gz > temp_body.out
       local proc = spawn("C:\\Program Files\\Git\\usr\\bin\\gzip.exe", {
         args = {"-d", "-c", tmp_gz},
         stdio = {nil, true, true}
       })
 ​
       -- 读取 stdout 到文件
       local out_chunks = {}
       for chunk in proc.stdout.read do
         if chunk then table.insert(out_chunks, chunk) end
       end
       proc.waitExit()
 ​
       local decompressed = table.concat(out_chunks)
       body = decompressed
       print("  已解压 gzip: " .. #body .. " bytes")
 ​
       -- 清理临时文件
       pcall(fs.unlinkSync, tmp_gz)
       pcall(fs.unlinkSync, tmp_out)
     end
 ​
     if res.code == 301 or res.code == 302 or res.code == 303 or res.code == 307 or res.code == 308 then
       local location = get_header(res, "location")
       if not location then error("重定向但无 Location 头") end
       if location:sub(1, 1) == "/" then
         local base = current_url:match("^(https?://[^/]+)")
         location = base .. location
       end
       print("  重定向 -> " .. location)
       current_url = location
     elseif res.code == 200 then
       return body, current_url, cookies
     else
       error("HTTP " .. res.code .. " at " .. current_url)
     end
   end
   error("重定向次数过多")
 end
 ​
 ​
 ​
 -- 对应 Python 的 re.search + json.loads
 local function my_match(text, prefix, suffix)
   local function esc(s)
     return s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
   end
   local pattern = esc(prefix) .. "(.-)" .. esc(suffix)
   local match = text:match(pattern)
   if match then
     local ok, result = pcall(json.parse, match)
     if ok then return result end
   end
   return nil
 end
 ​
 -- 对应 Python 的 clean_title
 local function clean_title(text)
   text = text:gsub("|", " ")
   text = text:gsub("%s+", " ")
   for _, char in ipairs({"<", ">", ":", '"', "/", "\\", "?", "*"}) do
     text = text:gsub("%" .. char, "")
   end
   return text
 end
 ​
 -- 对应 Python 的 merge_video_and_audio
 local function merge_video_and_audio(video_path, audio_path, output_path)
   local proc = spawn("ffmpeg", {
     args = {
       "-i", video_path,
       "-i", audio_path,
       "-c", "copy",
       "-map", "0:v:0",
       "-map", "1:a:0",
       "-shortest",
       "-y",
       output_path
     },
     stdio = {nil, true, true}
   })
   local code = proc.waitExit()
   if code == 0 then
     print("已合并: " .. output_path)
     return true
   else
     print("ffmpeg 执行失败, 退出码: " .. tostring(code))
     return false
   end
 end
 ​
 -- 主流程
 coroutine.wrap(function()
   print("提取到的URL: " .. VIDEO_URL)
 ​
   -- 1. 获取页面 HTML(自动跟随重定向)
   print("请求页面...")
   local html, final_url = get_with_redirect(VIDEO_URL)
   print("最终页面: " .. final_url .. " (" .. #html .. " bytes)")
 ​
   -- 2. 提取内嵌 JSON
   local playinfo = my_match(html, "__playinfo__=", "</script><script>")
   local initial_state = my_match(html, "__INITIAL_STATE__=", ";(function()")
 ​
   if not playinfo then
     print("__playinfo__ 未匹配,尝试备用后缀...")
     playinfo = my_match(html, "__playinfo__=", "</script>")
   end
   if not initial_state then
     print("__INITIAL_STATE__ 未匹配,尝试备用后缀...")
     initial_state = my_match(html, "__INITIAL_STATE__=", "</script>")
   end
 ​
   if not playinfo or not initial_state then
     fs.writeFileSync("debug_page.html", html)
     print("解析失败,已保存 debug_page.html")
     error("未能解析页面数据")
   end
 ​
   -- 3. 提取音视频地址和标题
   local video_url = playinfo.data.dash.video[1].baseUrl
   local audio_url = playinfo.data.dash.audio[1].baseUrl
   local video_name = initial_state.videoData.title
 ​
   print("视频名字: " .. video_name)
   print("视频地址: " .. video_url:sub(1, 100) .. "...")
   print("音频地址: " .. audio_url:sub(1, 100) .. "...")
 ​
   -- 4. 创建输出目录
   pcall(fs.mkdirSync, OUTPUT_DIR)
 ​
   video_name = clean_title(video_name)
   local referer = VIDEO_URL
 ​
   -- 5. 下载视频
   local video_path = OUTPUT_DIR .. "\\" .. video_name .. "_video.mp4"
   print("开始下载视频...")
   local vres, vbody = http.request("GET", video_url, {
     {"User-Agent", UA},
     {"Referer", referer},
   })
   if vres.code ~= 200 then error("视频下载失败: HTTP " .. vres.code) end
   fs.writeFileSync(video_path, vbody)
   print("视频下载完成: " .. #vbody .. " bytes")
 ​
   -- 6. 下载音频
   local audio_path = OUTPUT_DIR .. "\\" .. video_name .. "_audio.mp4"
   print("开始下载音频...")
   local ares, abody = http.request("GET", audio_url, {
     {"User-Agent", UA},
     {"Referer", referer},
   })
   if ares.code ~= 200 then error("音频下载失败: HTTP " .. ares.code) end
   fs.writeFileSync(audio_path, abody)
   print("音频下载完成: " .. #abody .. " bytes")
 ​
   -- 7. 合并
   local output_path = OUTPUT_DIR .. "\\" .. video_name .. "_merged.mp4"
   print("合并中...")
   if merge_video_and_audio(video_path, audio_path, output_path) then
     print("完成: " .. video_name)
   else
     print("合并失败,保留临时文件")
   end
 end)()
 ​

执行的结果类似这样的:

 PS E:\2026work> ./luvit.exe .\bili_downloader.lua
 提取到的URL: https://www.bilibili.com/video/BV1TKha67Eob?vd_source=46553807f77d6bc390bf1eb40d81874d
 请求页面...
   重定向 -> https://www.bilibili.com/video/BV1TKha67Eob/?vd_source=46553807f77d6bc390bf1eb40d81874d
   已解压 gzip: 508704 bytes
 最终页面: https://www.bilibili.com/video/BV1TKha67Eob/?vd_source=46553807f77d6bc390bf1eb40d81874d (508704 bytes)
 视频名字: 周深湾区首唱粤语新歌《月亮桥》
 视频地址: https://xy58x49x253x21xy.mcdn.bilivideo.cn:8082/v1/resource/upgcxcode/01/99/42061529901/42061529901-...
 音频地址: https://xy60x190x133x152xy.mcdn.bilivideo.cn:8082/v1/resource/upgcxcode/01/99/42061529901/4206152990...
 开始下载视频...
 视频下载完成: 15151947 bytes
 开始下载音频...
 音频下载完成: 2062730 bytes
 合并中...
 已合并: d:\mp3\周深湾区首唱粤语新歌《月亮桥》_merged.mp4
 完成: 周深湾区首唱粤语新歌《月亮桥》

这里没有真的写入cookie,如果需要高质量版本的视频,需要F12复制到自己的COOKIE,并添加清晰度选择的代码逻辑。

6. 扩展livit库

到目前为止,我们知道怎么使用这个东西了,那么我希望自己使用这个库,并扩展自己的函数,比如做加法,或者XML解析与编码,protobuf编解码。

6.1 纯 Lua 扩展:像写普通脚本一样简单

如果你要做的功能能用纯 Lua 实现(比如简单的加法、基础的字符串处理),你不需要编译任何东西。直接创建一个 .lua 文件,用 require 加载即可。

Luvit 本身就是用 Lua 编写的,它天然支持从文件系统或 deps 目录加载模块。你可以把自定义函数放在项目目录下的一个文件里,直接 local mylib = require('./mylib') 就能用。这就是扩展你自己的业务逻辑(比如之前写的监控采集函数)最直接的方式。

6.2 涉及 C 扩展(FFI 或 Native Module):需要编译,但不用重编 Luvit

如果你要做的功能涉及高性能计算或调用现有的 C 库(比如 XML 解析库、Protobuf 的 C 实现),就需要编写 C 扩展。Luvit 对此有明确的支持路径:

  1. Luvit 支持 FFI 和基于 Lua 的二进制模块。官方文档提到有一个 Wiki 专门解释如何管理和包含二进制模块。这意味着你可以用 LuaJIT 的 FFI 功能直接调用已有的 C 动态库(比如 .dll 或 .so),而不必为 Luvit 专门写绑定代码。

  2. 如果必须写 C 扩展,流程和 Node.js 的 native addon 类似:编写 C 代码,编译成动态库(在 Windows 上是 .dll),然后通过 require 加载。你不需要重新编译整个 Luvit/luvi 运行时,只需要编译你这个模块本身。社区邮件列表里也有关于创建 native modules 的讨论,指向了官方的“Publishing-Compiled-Code”指南。

6.2.1 针对具体需求(XML/Protobuf)
  • XML 解析:纯 Lua 的 XML 解析库在 lit 生态里可能不多,但你可以:

    • 寻找已有的 Lua XML 解析库(很多是纯 Lua 的),直接放进 deps 目录使用。

    • 或者用 FFI 调用系统里现成的 C 库(比如 libxml2),这需要你了解一些 LuaJIT FFI 的用法。

  • Protobuf 编解码:这是典型的适合用 C 扩展或 FFI 的场景。

    • 纯 Lua 实现的 Protobuf 库性能通常较差。

    • 最佳路径:用 FFI 加载官方的 libprotobuf-c 或类似的 C 库。你只需要写几十行 Lua 代码来定义 FFI 接口,就能获得接近原生的性能,完全不用重新编译 Luvit。

是的,Luvit 官方明确支持使用 FFI(外部函数接口) 来调用 DLL 扩展功能,这是其二进制模块支持的核心方式。

6.2.2 官方对 FFI 的定位

Luvit 的 README 中直接写道:“Luvit supports FFI and Lua based binary modules.”。在 Luvit 的语境下,FFI 的核心价值在于:允许 Lua 直接加载并运行动态库(如 DLL)中的代码,而无需编写繁琐的 C 绑定代码。

官方的邮件列表讨论也印证了这一点:当你需要集成一个 C 库时,推荐的做法是把 C 库编译成 .so/.dll,然后用 FFI 从 Lua 调用其 API。相比传统的 C Lua API 绑定,FFI 方式通常更快,因为它是 JIT 引擎的核心部分,允许 JIT 优化器做更多假设。

6.2.3 如何用 FFI 调用 DLL

基本流程分三步,以调用一个名为 myffi.dll 的库为例:

 E:\2026work\
 ├── bili_downloader.lua        # 你的主脚本
 ├── deps/
 │   ├── myffi.lua              # FFI 定义文件(ffi.cdef 声明)
 │   └── myffi.dll              # 你的 C 扩展动态库

  1. 声明函数签名:用 ffi.cdef 描述 DLL 中函数的 C 语言签名。

     local ffi = require("ffi")
     ffi.cdef[[
     int add(int x, int y); /* 声明函数 */
     ]]
  2. 加载 DLL:用 ffi.load 加载动态库。在 Windows 上,如果名字里不带点号,会自动补上 .dll 后缀。

     local myffi = ffi.load('myffi')  -- 会查找 myffi.dll
  3. 调用函数:通过返回的命名空间直接调用。

     local ffi = require('ffi')
     ​
     -- 加载 deps 下的 DLL(Luvit 的 require 会搜索 deps 目录)
     local myffi = ffi.load('deps/myffi')  -- 或 'myffi',取决于搜索路径
     ​
     -- 从 Lua 文件里读取 C 函数声明
     ffi.cdef[[
     int add(int x, int y);
     ]]
     ​
     local res = myffi.add(1, 2)
     print(res) -- 输出 3

关于官方接口”的注意事项

Luvit 本身不提供一个额外的“DLL 管理 API”,它依赖的是 LuaJIT 自带的 FFI 库。因为 Luvit 的运行时核心就是 LuaJIT,所以 LuaJIT 的 FFI 就是 Luvit 的“官方接口”。你之前用的 luvi 运行时也已经内置了 FFI 支持,所以 require("ffi") 可以直接使用。

TODO: 这里没有讨论如何将DLL打包到luv中。

7. 嵌入式使用luvi或者luv

在 C/C++ 代码中调用 Luvit 引擎并与之交互,核心不是直接调用 Luvit,而是使用它底层的 luvi 运行时或 luv 绑定。

Luvit 本质上是一个为 luvi 运行时编写的 Lua 库集合。

在 C 代码中嵌入完整 Luvit 较为复杂,更常见的做法是嵌入 luvi(它内置了 LuaJIT 和 libuv),并通过 LuaJIT FFI 来实现 C 与 Lua 的双向交互。

方案一:通过 luvi 嵌入(最接近 Luvit 的方式)

luvi 是一个“介于 luv 和 luvit 之间的项目”。

 https://github.com/luvit/luvi
🧩 它和 Luvit 的关系

luvi 的官方定位是 “A project in-between luv and luvit”(位于 luv 和 luvit 之间的项目)。它把 LuaJIT、libuv、OpenSSL、miniz 等依赖打包成一个独立的运行时,目标是 让构建 Luvit 及其衍生应用变得更容易。

它的 README 里明确写道:

Luvi has a somewhat unique, but very easy workflow for creating self-contained binaries on systems that don't have a compiler.

这意味着,不需要一个完整的 C 编译器就能打包出可执行文件——这就是它作为“嵌入式运行时”的核心价值。

🔗 交互能力:正是我们需要的

你关心的“在自己的代码中调用引擎并与它交互”,luvi 提供了明确的机制:

  • C 的 main 函数集成:C 侧的 argc 和 argv 会以 0 起始索引的字符串表形式暴露给 Lua 的全局 args 表。

  • 退出码:如果 main.lua 返回一个整数,它就是程序的退出码。

  • 环境变量:env 模块提供 env.get、env.put、env.set 等接口来读写环境变量。

  • Bundle I/O:luvi 的 bundle 属性统一了磁盘目录和 zip 包的读取方式,提供 bundle.stat、bundle.readdir、bundle.readfile 等接口。

📦 版本选择

luvi 提供两个预置构建版本:

  • tiny:只包含 Lua、Libuv、miniz 和最小化模块。

  • regular:完整的 Luvit 体验,包含 OpenSSL、LPeg、lrexlib。

它把你的 main.lua 运行在一个“基本标准的 LuaJIT 环境”中,并内置了 uv(libuv 绑定)和 bundle(文件 I/O)等模块。

交互方式:

  • C 调用 Lua:在 C 侧初始化 luvi 运行时并加载你的 main.lua。这类似于标准的 LuaJIT 嵌入流程(luaL_newstate, luaL_openlibs, luaL_dofile)。

  • Lua 调用 C:在 Lua 代码中通过 ffi.load 加载你编译好的 DLL,然后用 ffi.cdef 声明并调用 C 函数。这是官方推荐的方式,Luvit 明确支持 FFI 和二进制模块。

一个 C 代码的交互示例(概念性):

 // 你的 C 程序
 #include <lua.h>
 #include <lauxlib.h>
 #include <lualib.h>
 ​
 int main() {
     lua_State *L = luaL_newstate();
     luaL_openlibs(L);
 ​
     // 注册一个 C 函数给 Lua 调用,或者直接加载 Lua 脚本
     luaL_dofile(L, "main.lua"); // 你的 Lua 逻辑入口
 ​
     // 启动 luvi 的事件循环(如果使用 uv 模块)
     // ... 从 Lua 侧调用 uv.run() 后,控制权会交回 C
     lua_close(L);
     return 0;
 }

方案二:仅使用 luv 绑定(更轻量)

GitHub - luvit/luv: Bare libuv bindings for lua · GitHub

如果只需要异步 I/O 能力,而不需要 Luvit 的 HTTP 等高级 API,可以直接使用 luv——即 libuv 的 Lua 绑定。有社区回复明确指出:“你很可能想要的不是 luvit 本身,而是 luv 绑定,它们可以独立使用并发布到 luarocks”。

这意味着你可以将 luv 编译进你现有的 C/C++ 应用程序中(你的应用可能已经链接了 libuv),然后在 Lua 脚本中 require('luv') 来使用异步功能。

这两个项目,luv 和 luvi 是“零件”与“成品”的关系。它们同属 luvit 生态,但处于完全不同的抽象层级。

🔩 luv:底层的“零件”

luv 是一个纯粹的 libuv 绑定库。它的官方描述是 “Bare libuv bindings for lua”,核心任务只有一件事:把 libuv 的 C API 原封不动地暴露给 Lua。

你可以把它理解为一把“瑞士军刀”的刀刃。它只提供最基础的异步能力:

  • 事件循环:uv.run()

  • 定时器:uv.new_timer()

  • TCP/UDP 网络:uv.new_tcp()、uv.new_udp()

  • 文件系统操作:uv.fs_open() 等

它的设计目标是可嵌入性:可以编译成静态库、共享库,或者作为 Lua 模块加载。它支持 LuaJIT 和 PUC Lua 5.1 到 5.5,甚至可以在同一个进程的不同线程、不同 lua_State 中使用(每个 state 拥有独立的 uv_loop_t)。

📦 luvi:打包好的“成品”

luvi 则是一个完整的、可独立运行的运行时。它的目标很明确:“make building luvit and derivatives much easier”——让构建 Luvit 及其衍生应用变得更容易。

它把多个组件打包成一个可执行文件:

  • LuaJIT(或 PUC Lua)

  • libuv(通过 luv 绑定,暴露为 uv 或 luv 模块)

  • OpenSSL(regular 版本)

  • miniz(用于 zip 包读取)

  • 其他实用模块:env、bundle、args 等

换句话说,luvi 内部包含了 luv。当你用 luvi 运行一个应用时,你的 main.lua 可以直接 require('uv'),因为 luvi 已经把 luv 编译进去了。

⚖️ 核心区别一览

维度luvluvi
本质libuv 的 Lua 绑定库独立的、自包含的 Lua 运行时
包含关系被 luvi 包含包含 luv、LuaJIT、OpenSSL 等
交付形态.so / .dll / 静态库 / Lua 模块单个可执行文件(如 luvi.exe)
使用方式嵌入到已有 Lua 环境或 C 程序中直接运行 Lua 脚本或打包应用
API 层级底层、接近 libuv 原始 API在 luv 之上增加了 env、bundle、args 等
适用场景已有 C 程序需要异步 I/O,或想自定义运行时快速开发、打包、分发独立的 Lua 应用

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/robinfoxnan/article/details/166894675

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--