隨着 .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