有好多朋友留言问.Net在开发板上的部署情况,我在此稍做整理,这一篇主要讲一些跨平台的基础内容,以WEB应用为例介绍开发、发布、部署流程。可能相似的文章网上到处都是,就不声明原创了。
先说结论:.Net跨平台已经非常成熟,完全可以用于生产环境。少部分场景由于第三方组件的兼容性支持不够,做需求会非常痛苦,建议寻求其它方案,比如瑞芯微的RKNPU,对.net支持为0;在另一些场景里,虽然组件对.net直接支持不够,但是PInvoke包一切,仍然可以完美解决问题(Linux环境安装GigE-V Framework并用C#进行线阵相机连接和拍照,C#在Linux下使用MVS SDK获取照片以及一个抓拍问题修复)。
1. 局限:
由于跨平台并不是所有事都由.net自己来做,很多情况都依赖系统自己的实现,所以会有一些兼容性问题,在项目前期调研阶段一定要了解清楚。
另外,完美跨平台的只有控制台类的应用。GUI程序必须使用限定的框架才可以,比如MAUI,Avalonia。此外,如果程序计划运行在Win arm设备,可以用老框架直接发布成win-arm,移植成本非常低,但应用场景极其有限,并且部分库存在兼容性问题(点名sqlite)。
这是一个.net Core WPF项目,发布的时候直接把目标平台改成Windows Arm就可以直接在手机上运行(操作系统Windows 11 ARM)。

1.a. 某些功能不具备所有平台的通用解,需要根据不同环境做针对处理,这个比较好理解。比较典型的操作有:注册表操作、PerformanceCounter、System.Common.Drawing下的GDI操作、System.Management调用WIN32 API等等,这些操作只在Windows下可用。另外一种常见情形是文件读取,比如Linux获取当前cpu和内存使用率时使用的top命令、读取CPU基础信息用的/proc/cpuinfo文件,这些方案在Windows下就完全无效,必须用其它方案,比如System.Management命名空间下的ManagementObjectSearcher(其实也是WIN32_API)。

1.b. 另外,相对Windows而言,有一些包没有对应的Linux版本,或者仅发布了Linux x64版,没有arm架构的版本,甚至只有某个特定linux发行版。

2. 开发板刷机:
第一步是下载镜像和烧写工具,镜像和烧写工具到开发板官网找就行。一般开发板的刷机方式不外乎三种:
2.a. 先把镜像写到TF卡,然后通过TF卡启动后将卡内的系统转写到设备内部存储(通常称为卡刷);
2.b. 将镜像写到TF卡,并直接在TF卡上运行系统;
2.c. 通过USB数据线挂载开发板到电脑上,用软件直接对设备进行烧写(一般称为线刷)。
需要注意的是这三种方式对应不同的镜像。比如,下图是友善之臂的neo4开发板的官方镜像目录。

3. SDK安装
.net现在已经官方支持跨平台部署,只要安装dotnet即可,部分功能因为依赖操作系统,需要针对性安装缺失包。以下分别是SDK和Runtime,我在安装的时候会直接选择SDK,其中包含了所有组件,否则就需要分别安装ASP.NET Core Runtime和.NET Runtime了。当然也可以不选择框架依赖,直接用独立部署模式,这样就可以不需要SDK或者Runtime。


3.a. 下载SDK:
根据操作系统,比如我用的开发板是Debian 10 arm x64,那么对应的SDK就是Linux Arm64。如果你的cpu是x86架构(Intel/AMD的普通PC、服务器),则应该按操作系统位数选择x86或者x64。
3.b. 安装:解压 ,添加环境变量:
我的解压目录是 /usr/local/dotnet,解压完成后,在/etc/profile中添加DOTNET_ROOT,并将DOTNET_ROOT添加到$PATH。
linaro@linaro-alip:~$ ls /usr/local/dotnet/
dnx host packs sdk-manifests templates
dotnet LICENSE.txt sdk shared ThirdPartyNotices.txt
# 添加到 /etc/profile 文件
export DOTNET_ROOT=/usr/local/dotnet
export PATH=$PATH:$DOTNET_ROOT
全部完成后运行source /etc/profile,或者干脆退出重登来应用环境变量。最后执行 dotnet –version查看安装情况。
linaro@linaro-alip:~$ dotnet --version
10.0.1024. 创建和发布程序
最后发一个.net core MVC项目测试下。项目模板按需选择就行,需要注意的是,希望开发跨平台应用时,应该选不带.NET Framework字样的模板。

4.a. 控制器
public IActionResult Index()
{
if (OperatingSystem.IsWindows())
{
ViewBag.CPU =
Microsoft.Win32.Registry.GetValue(
@"HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0",
"ProcessorNameString",
"Unknown")
?.ToString();
}
else
{
ProcessStartInfo info = new("lscpu")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(info);
proc?.WaitForExit();
ViewBag.CPU = Process.Start(info)?.StandardOutput?.ReadToEnd() ?? "-";
}
return View();
}4.b. View
@using System.Runtime.InteropServices
@using System.Text.Json
@{
ViewData["Title"] = "Home Page";
}
<table class="table table-sm small">
<thead><tr><th>Item</th><th>Value</th></tr></thead>
<tbody class="small">
<tr><td>Current DIR</td><td>@Directory.GetCurrentDirectory()</td></tr>
<tr><td>Framework</td><td>@RuntimeInformation.FrameworkDescription</td></tr>
<tr><td>OS</td><td>@RuntimeInformation.OSDescription @RuntimeInformation.OSArchitecture</td></tr>
<tr><td>Release</td><td>@RuntimeInformation.RuntimeIdentifier</td></tr>
<tr><td>@Environment.ProcessorCount vCPU</td><td>@Html.Raw(ViewBag.CPU.Replace("\n","<br />"))</td></tr>
</tbody>
</table>4.c. 发布
添加一个发布配置,发布位置选文件夹。目标运行时如果选择“可移植”,打包的时候会把所有架构的包按列表里的架构挨个发布一遍,最终会看到一个runtimes目录下一堆不同环境的包。

我给项目添加了一个System.IO.Ports包用于演示,生成时会把每个目标环境里的库都打包一份,坏处是目录结构复杂,程序笨重,好处是一次编译到处运行(这话听着很耳熟啊)。
因此,如果条件允许,为了发布简洁,可以直接指定目标环境。此时由于环境的唯一性,对应版本的库文件会直接发布到根目录,不再需要runtimes目录。


5. 部署
部署也很简单,只要把发布生成的完整目录拷到服务器就行。运行软件既可以用dotnet xxx.dll,也可以直接运行可执行文件(Windows下是xxx.exe,Linux下就是xxx不带后缀)。
5.a. Windows部署:



5.b. Linux部署(debian):用 ./xxx 的方式直接运行可执行文件的时候需要chmod添加可执行权限;如果用dotnet xxx.dll的方式来运行,则什么都不需要改。


5.c. 关于端口号:
.net web项目发Release的时候默认会在localhost:5000侦听,可以通过向`appsettings.json`文件添加参数Urls来修改,也可以在dotnet命令后边添加命令行参数–urls来修改,还可以在Program.cs创建HostBuilder时在代码中添加Urls参数来设置侦听。


5.d. 关于地址:
既可以用localhost,也可以显式指定ip。前者需要结合反向代理使用才能发布出来供外部访问,比如nginx;当然如果只是提供本机服务,访客都来自本机,那就不需要反代。后者由于侦听在物理网口的ip上,所以可以直接访问。

5.c. 关于权限:
权限问题非常重要,运行dotnet程序的用户,必须对自己操作的资源具有相应读/写权限。在控制台直接运行程序时,会使用当前登录用户,比如当前运行程序的用户就是linaro。如果我要读取一个linaro不具备权限的文件,就会收到访问被拒绝的异常。作为对比,使用root权限运行时就可以正确读取文件内容。

