随着 .NET5.0 Preview 8 的发布,许多新功能正在被社区成员一一探索;这其中就包含了“单文件发布”这个炫酷的功能,实际上,这也是社区一直以来的呼声,从 WinForm 的 msi 开始,咱们就但愿有这样一个功能,虽然在 docker 时代,单文件发布的功能显得“不那么重要”,但正是从这一点能够看出,.NET 的团队成员一直在致力于实用功能的完善。html
在 Java 的世界里,单文件发布一直伴随着他们的成长,War 文件能够直接上传到 Tomcat 上运行,话说咱们仍是有那么一丢丢的羡慕的,不过凡事有利就有弊,单文件发布对于细分模块的热更新来讲,还有有一点点的不方便。linux
不过瑕不掩瑜,在微服务概念愈来愈火热的今天,相信单文件发布的功能带给你们更多的是兴奋。git
首先,咱们要清楚的了解,什么是单文件发布。github
从上面的目标能够看出,和以往版本最大的不一样在于:将全部依赖打包到一个可执行文件中,可直接运行,不影响调试操做。docker
注意上面的这句话“将全部依赖打包到一个可执行文件中”,而在以往,咱们使用 dotnet publish 将应用程序进行发布以后,咱们会看到,在 publish 下有许多项目依赖的 dll 文件,在 .NET5.0 到来以后,这些依赖文件可收纳到一个文件中,瞬间让人感觉到了清凉。json
平台 | 命令 | 说明 |
---|---|---|
Linux | dotnet publish -r linux-x64 /p:PublishSingleFile=true | - |
Windows | dotnet publish -r win-x64 --self-contained=false /p:PublishSingleFile=true | - |
Mac OS | - | - |
属性 | 描述 |
---|---|
IncludeNativeLibrariesInSingleFile | 在发布时,将依赖的本机二进制文件打包到单文件应用程序中。 |
IncludeSymbolsInSingleFile | 将 .pdb 文件打包到单个文件中。提供该选项是为了和 .NET 3 单文件模式兼容。建议替代的方法是生成带有嵌入式的 PDB (
|
IncludeAllContentInSingleFile | 将全部发布的文件(符号文件除外)打包到单文件中。该选项提供是为了向后兼容 .NETCore 3.x 版本 |
除了可使用命令行参数的形式,还能够经过配置文件的形式设置发布参数,编辑项目文件,添加配置节点到文件中并保存便可。windows
<PropertyGroup> <TargetFramework>net5.0</TargetFramework> <RuntimeIdentifier>linux-x64</RuntimeIdentifier> <PublishSingleFile>true</PublishSingleFile> <IncludeContentInSingleFile>true</IncludeContentInSingleFile> </PropertyGroup>
关于 RID 说明见:https://docs.microsoft.com/en-us/dotnet/core/rid-catalog架构
这是截止本文发布前的 RID 版本,不排除 .NET5.0 有新的发布app
除了上面的三个可选参数,我在查询文档的过程当中还发现,官方还提到了其它参数的使用,目前不肯定是否有效编辑器
<PropertyGroup> <SelfContained>true</SelfContained> <!--启用使用assemby修剪-仅支持自包含应用程序--> <PublishTrimmed> true </PublishTrimmed> <!--启用AOT编译 目前暂不支持预编译--> <!--<PublishReadyToRun>true</PublishReadyToRun>--> </PropertyGroup> <ItemGroup> <Content Update="*-exclute.dll"> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <ExcludeFromSingleFile>true</ExcludeFromSingleFile> </Content> </ItemGroup>
还能够经过设置 ExcludeFromSingleFile 元素,该设置将指定某些文件不嵌入单个文件之中。
为了更直观的看出正常发布和单文件发布的区别,咱们特别准备了一个 Web 应用程序,并对两个程序集进行依赖引用。
准备好项目,编译成功,尝试发布,打开 PowerShel 控制台,分别输入如下命令
dotnet publish -r linux-x64 /p:PublishSingleFile=true dotnet publish -r win-x64 --self-contained=false /p:PublishSingleFile=true
linux-x64 和 win-x64 两个目录下,分别有 publish 目录,因为平台的不一样,所引用的依赖也不同,这是咱们早就了解过的,咱们看看打包先后的区别
以上执行的两条命令语句,会为咱们生成 Linux 和 Windows 两个平台的程序包,从上图中能够看出,在打包以前,项目的各类引用依赖都被复制到了发布目录下,这也是咱们以前的程序发布方式,在通过打包后,全部依赖文件都被装入了一个可执行文件中,在 Linux 平台下表现为:PreviewWebApplication ,Windows 平台下则为:PreviewWebApplication.exe。从打包效果来看,迁移将变得更加方便了。
打包后的程序和未打包的发布程序在运行方式上没有太多的差别性,在 Windows 平台上,只须要双击 PreviewWebApplication.exe 就能够运行该打包程序了,本示例建立的是一个 WebApi 的程序,直接访问程序侦听的地址后获得接口返回的结果,若是您建立的是带有 Razor 视图或者携带其它资源文件的,可能没法访问指定的 url。
在程序成功运行起来后,咱们发现,打包程序并无解压缩文件到磁盘,而是直接从包中加载文件到内存中运行;这是巨大的进步,也是和 War 文件根本的区别。
须要注意的是,该 .exe 文件并不能单独复制到别的地方运行,你必须把 .exe 当前目录完整的复制才能运行,这涉及到主机探测的问题,下面咱们将会一一提到。
经过上面的示例咱们了解到,打包程序老是为不一样的平台生成独立的包程序,这是为何呢?这里就涉及到一个概念,也就是 Tool Interface Standard (TIS)
Common Object File Format(COFF)于1983年引入,最初使用在 AT&T 的 UNIX 系统上。因为 COFF 的各类局限性,好比:节的最大数量受到限制,节名称,所包含的源文件的长度受到限制,而且符号调试信息没法支持实际的语言。最后,在 System V Release 4 (SVR4) 发布后,AT&T 使用 ELF 替代了 COFF。
工具接口标准委员会
援引委员会规范文件的说明:可执行文件和连接格式最初由 UNIX 系统开发和发布实验室(USL)做为应用程序二进制接口(API)的一部分。工具接口标准委员会 (TIS) 选择将不断发展的 ELF 标准做为便携式对象文件。该标准适用于各类操做系统的 32 位英特尔架构环境的格式。ELF 标准旨在经过向开发人员提供具备一组跨多个操做环境的二进制接口定义。这将减小不一样接口实现的数量,从而减小须要从新编写和编译的代码。
ELF 文件结构又分为三种类型,分别是:
名称 | 说明 | 描述 |
---|---|---|
可重定位文件 | Relocatable File | 包含适合与其余对象文件连接的代码和数据,以建立可执行文件或共享对象文件。 |
可执行文件 | Executable File | 包含适合执行的程序 |
共享目标文件 | Shared Object File | 包含适合在两种上下文中连接的代码和数据。首先,连接编辑器能够处理它与其余可从新删除和共享的对象文件,以建立另外一个对象文件。其次,动态连接器将其与可执行文件和其余共享对象相结合,以建立进程映像。 |
在 Windows 阵营,微软在此 COFF 标准的基础上,又进行了创新和发展出了 PE 文件标准
PE Format
该规范描述了Windows操做系统家族下的可执行文件(图像)和目标文件的结构。这些文件分别称为可移植可执行(PE)和公用对象文件格式(COFF)文件。
从上面的两种规范中能够看出,LinuX 和 Windows 都有各自的文件格式规范,而这种规范在必定程度上是不兼容的,不管是从文件结构仍是解析方式;因此 .NET5.0 中的打包程序必须为不一样的平台实现独立的打包器。打包器的实如今 runtime 中的 Microsoft.NET.HostModel 库中。
认识了 ELF 和 PE 文件结构以后,咱们就能够对打包器代码进行阅读理解。
你能够从 github 上下载 .NET 5.0 的源代码,
转到目录:
runtime/src/installer/managed/Microsoft.NET.HostModel
源码不太多,可直接进行阅读,主要理解层次关系便可。
打包器主要包含了三大部分的内容,分别是 AppHost、Bundler、ComHost
模块 | 说明 |
---|---|
AppHost | 用于单文件主机启动时的文件探测,还复制将程序资源从 App.dll 复制到 AppHost备用,目前已经过 HostFxr 和 HostPolicy 进行静态连接,其探测逻辑已转移到 HostPolicy(由C++编写) |
Bundler | 打包器的具体实现,主要是将应用程序及其依赖项嵌入 AppHost 中,随后发布单个可执行文件到指定目录 |
ComHost | 建立一个包含嵌入式 CLSIDMap 文件的 ComHost,以将 CLSID 映射到 .NET 类。 |
在文件 Bundle/Manifest.cs 的头部,咱们看到了“单文件程序”的文件结构定义
BundleManifest is a description of the contents of a bundle file. This class handles creation and consumption of bundle-manifests. Here is the description of the Bundle Layout: _______________________________________________ AppHost ------------Embedded Files --------------------- The embedded files including the app, its configuration files, dependencies, and possibly the runtime. ------------ Bundle Header ------------- MajorVersion MinorVersion NumEmbeddedFiles ExtractionID DepsJson Location [Version 2+] Offset Size RuntimeConfigJson Location [Version 2+] Offset Size Flags [Version 2+] - - - - - - Manifest Entries - - - - - - - - - - - Series of FileEntries (for each embedded file) [File Type, Name, Offset, Size information] _________________________________________________
从上面的文件结构中,咱们能够很是清晰的看到,单文件程序的结构一共分为三大部分,分别是:
定义 | 说明 | 描述 |
---|---|---|
嵌入的文件 | Embedded file | 主要是配置文件和描述文件,好比 .deps.json,runtimeconfig.json 等文件 |
打包文件头信息 | Bundle Header | 描述了整个文件的结构信息,类型,存储位置,段、表等信息 |
实体清单 | Manifest Entries | 实际打包的文件列表,每一个文件分段写入,可执行文件使用 16byte - prev file end position 进行分隔,普通文件直接按 prev file end position 进行写入 |
咱们能够经过一些工具去查看已经打包好的文件,在 Linux 下,可使用 readelf/objdump 等程序来获取 PreviewWebApplication 文件的信息。在 Windows 下,可使用 PE Tools 等工具
Linux 下 readelf 读取文件头信息
从图中咱们能够看到 Type:DYN (Shared object file)
这是一个标准的共享对象文件,关于 ELF 头部信息的内容再也不展开,有兴趣的同窗能够自行学习相关内容。
Windows下 PE Tools 读取文件头信息
已经打包好的程序内部包含了 319(Linux)、Windows(359) 个文件,Windows 版本在未打包前是 84.3MB,打包后是 69.8MB,最重要的是在运行时无需解压缩,直接从 Bundle 中运行文件。
文件中的第三部分,也就是 “实体清单(Manifest Entries)的写入代码在 Bundle\Bundler.cs\AddToBundle
long AddToBundle(Stream bundle, Stream file, FileType type) { if (type == FileType.Assembly) { long misalignment = (bundle.Position % AssemblyAlignment); if (misalignment != 0) { long padding = AssemblyAlignment - misalignment; bundle.Position += padding; } } file.Position = 0; long startOffset = bundle.Position; file.CopyTo(bundle); return startOffset; }
在成员方法 GenerateBundle(IReadOnlyList
// 代码片断 public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs) { ... foreach (var fileSpec in fileSpecs) { string relativePath = fileSpec.BundleRelativePath; ... using (FileStream file = File.OpenRead(fileSpec.SourcePath)) { FileType targetType = Target.TargetSpecificFileType(type); long startOffset = AddToBundle(bundle, file, targetType); FileEntry entry = BundleManifest.AddEntry(targetType, relativePath, startOffset, file.Length); Tracer.Log($"Embed: {entry}"); } } // Write the bundle manifest headerOffset = BundleManifest.Write(writer); ... }
由于解压器的实现已经转移到了 HostFxr 和 HostPolicy 中,以静态连接库的方式连接到打包器中,且该部分代码由 C++ 进行编写,鉴于 C++ 水平有限,在这里不做介绍。
编写这篇文章耗费了我大量的时间,期间大量阅读海量的参考资料、文献、标准文档、制做文章配图等等,写干货文章真的须要投入巨大的精力和时间,但愿大家喜欢。
文章进行到这里,我知道确定还有不少同窗没看过瘾,可是咱们能够经过回顾打包器的开发进度表来体验一下 .NET 团队的开发热情。
.NET团队计划经理 Richard Lander 的博客:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-8/
Bundler 进度表:https://github.com/dotnet/runtime/issues/36590
single-file:https://github.com/dotnet/designs/tree/master/accepted/2020/single-file
ELF文档:https://refspecs.linuxbase.org/elf/elf.pdf
ELF维基百科:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
Readelf:https://sourceware.org/binutils/docs/binutils/readelf.html
PE文档:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
PE Tools:https://github.com/petoolse/petools