使用.NET Core搭建分散式音頻效果處理服務(三)完成音頻合成效果處理程式

来源:https://www.cnblogs.com/SteveLee/archive/2018/08/14/9475708.html
-Advertisement-
Play Games

上一節我們已經介紹了FFmpeg在Net Core中的簡單應用,這一節我們將根據之前的功能需求和解決方案,進行項目的詳細設計工作。 畫個流程圖 先闡述一下流程,如下圖: 整個流程其實非常簡單,客戶端(無論桌面軟體、還是原生APP、還是HTML網頁)通過一個統一的介面進行調用,我們這裡定義這個介面名稱 ...


上一節我們已經介紹了FFmpeg在Net Core中的簡單應用,這一節我們將根據之前的功能需求和解決方案,進行項目的詳細設計工作。

 

畫個流程圖

先闡述一下流程,如下圖:

整個流程其實非常簡單,客戶端(無論桌面軟體、還是原生APP、還是HTML網頁)通過一個統一的介面進行調用,我們這裡定義這個介面名稱叫AudioSynthesisSync吧,為何名稱後面還要加個同步(也是命名規範),目前來說這個介面就屬於同步的,非同步方式後續再一一解答。

當得到指定的兩個(前景和背景)參數後,伺服器去自動下載(或本地緩存)參數指定的音頻文件,將這兩個音頻帶入到ffmpeg處理程式中進行音頻合成和處理(處理效果包括指定時間剪輯、淡入和淡出、拼接多個音頻、合併兩個音頻、音量縮放,具體參數參見ffmpeg官方文檔),等待音頻處理完成後得到一個新的文件,將這個文件上傳到指定雲(筆者這邊用的是阿裡雲OSS),上傳完成後將阿裡雲的雲文件訪問地址存儲並返回到請求的客戶端,整個流程結束。

反觀上面整個流程,其實就三個步驟的操作,下載文件,處理文件,上傳文件。ok,那麼我們只需要一個Controller即可,這裡我們聲明為MediaApiController吧。

 

創建預設構造函數

大家現在都碼磚都喜歡用依賴註入的方式去構建某個類,筆者也不例外,依賴註入的優勢很多很多,你可以使用netcore自帶,asp.net core,autofac,unity等優秀的框架做依賴註入,筆者這裡不做詳細闡述。

筆者這裡使用autofac註入了大量亂七八糟的類(說是亂七八糟,實際是筆者創建的一個easyHub的框架,日後會一一分享出來),有註釋。

 1 public class MediaApiController: BaseApiController
 2 
 3 {
 4    // 消息匯流排
 5     private readonly IMsgBusService _iMsgBusService;
 6    // 資料庫操作工廠
 7     private readonly IDataOpService _dataOpService;
 8    // 緩存操作工廠
 9     private readonly ICacheAsyncService _iCacheAsyncService;
10    // 宿主環境
11     private readonly IHostingEnvironment _ihostingEnvironment;
12    // 音頻處理單元
13     private readonly AudioHandlerWorkUnit _audioHandlerWorkUnit;
14 
15     public MediaApiController(IDataOpService iDataOpService,
16 
17     ICacheAsyncService iCacheAsyncService,
18 
19     IMsgBusService imsgBusService,
20 
21     IHostingEnvironment iHostingEnvironment,
22 
23     IDurationMath durationMath)
24 
25     {
26 
27         _dataOpService = iDataOpService;
28 
29         _iMsgBusService = imsgBusService;
30 
31         _iCacheAsyncService = iCacheAsyncService;
32 
33         _ihostingEnvironment = iHostingEnvironment;
34 
35         _audioHandlerWorkUnit = new AudioHandlerWorkUnit(iDataOpService: _dataOpService,
36 
37         iCacheAsyncService: _iCacheAsyncService,
38 
39         imsgBusService: _iMsgBusService,
40 
41         iHostingEnvironment: _ihostingEnvironment,
42 
43         iDurationMath: durationMath
44 
45         );
46 
47     }
48 
49 }

 

在MediaApiController構造函數中,筆者將需要使用的中間服務一股腦的全部註入到控制器中,其中iDataOpService介面實現了資料庫的操作,iCacheAsyncService實現了緩存操作,imsgBusService實現了消息隊列操作,iHostingEnvironment是netcore存儲的宿主環境參數和變數,durationMath實現了處理耗時,AudioHandlerWorkUnit是音頻處理的主要方法類。

當然,還有細心的朋友發現了,筆者這邊不是繼承於Controller類型,而是繼承於BaseApiController類型,在net api中可沒有這個類型的啊。的確,這是筆者自定義的一個類型,當然父類肯定繼承於Controller下,為何筆者喜歡這樣中間再多繼承一層,不是多次一舉嗎?不是,筆者這邊簡單的介紹一下:

Controller的功能和特性這裡不做闡述,如果我們要在netcore中實現web請求,那麼必須繼承於該Controller類型。筆者喜歡使用AOP編程模式進行碼磚,這樣能遵循開閉原則,並且便於維護,在BaseApiController中,筆者重寫了OnActionExecuting,OnActionExecuted,OnActionExecutionAsync三個主要方法,便於在請求處理過程中,對出和入進行過濾、處理時間的計算、返回內容的重構等等進行統一規範。(項目源碼會在日後新開的EasyHub框架中詳細介紹)

 

創建音頻信息模型

在我們創建介面之前,需要規範來回傳輸數據結構上面的一些信息,比如音頻文件名、格式、持續時間、編碼率等等一些基礎信息。

因此建立一個模型叫AudioInfo

 1 public class AudioInfo
 2 
 3 {
 4 
 5     public string filename {
 6         get;
 7         set;
 8     }
 9 
10     public int nb_streams {
11         get;
12         set;
13     }
14 
15     public int nb_programs {
16         get;
17         set;
18     }
19 
20     public string format_name {
21         get;
22         set;
23     }
24 
25     public string format_long_name {
26         get;
27         set;
28     }
29 
30     public string start_time {
31         get;
32         set;
33     }
34 
35     public double duration {
36         get;
37         set;
38     }
39 
40     public long size {
41         get;
42         set;
43     }
44 
45     public int bit_rate {
46         get;
47         set;
48     }
49 
50     public int probe_score {
51         get;
52         set;
53     }
54 
55 }

通過ffprobe命令獲取一個音頻文件後,數據實例化樣本如下

[format, {
    "filename": "text.mp3",
    "nb_streams": 1,
    "nb_programs": 0,
    "format_name": "mp3",
    "format_long_name": "MP2/3 (MPEG audio layer 2/3)",
    "start_time": "0.000000",
    "duration": "25.568875",
    "size": "409102",
    "bit_rate": "128000",
    "probe_score": 51
}]

 

各項參數意思不用過多解釋相信大家從單詞上就夠能明白。

 

創建介面

接下來我們創建一個介面,命名為AudioSynthesisSync。

 1      /// <summary>
 2         /// 同步合成兩個音頻文件並上傳到阿裡雲
 3         /// </summary>
 4         /// <param name="frontFileUrl">輸入文件1,一般是前景讀音</param>
 5         /// <param name="backgounedAudioIndex">背景音樂文件</param>
 6         /// <remarks>
 7         /// 背景音樂文件,需要在應用程式根目錄下麵創建StaticResurces文件夾
 8         /// 並將資料庫BackGroundAudioListModels中AudioUrl路徑文件名相對應
 9         /// 合成時間將受到音頻時長、CPU性能嚴重相關而定。介面適合較短(10秒內的音頻合成)
10         /// </remarks>
11         /// <returns>合成後音頻文件的URL地址</returns>
12         [HttpGet]
13         [Route("AudioSynthesisSync")]
14         public JsonResult AudioSynthesisSync(string frontFileUrl, int backgounedAudioIndex)
15         {
16             if (string.IsNullOrEmpty(frontFileUrl))
17                 return new JsonResult("文件不能為空") {StatusCode = ClientStatusCode.ClientParameterError};
18             if (frontFileUrl.Contains("https://"))
19                 return new JsonResult("不支持https加密協議") {StatusCode = ClientStatusCode.ClientParameterError};
20             if (!frontFileUrl.Contains("http://"))
21                 return new JsonResult("文件必須存在於網路") {StatusCode = ClientStatusCode.ClientParameterError};
22 
23             var mixInfo = _audioHandlerWorkUnit.SynthesisAudio(frontFileUrl, backgounedAudioIndex, "");
24 
25             if (mixInfo.GetType() == typeof(JsonResult))
26             {
27                 // 存在錯誤的時候直接返回錯誤
28                 return mixInfo;
29             }
30 
31             return new JsonResult(new Dictionary<string, object>
32             {
33                 {"web_url", mixInfo.WebUrl},
34                 {
35                     "duration", new Dictionary<string, object>()
36                     {
37                         {"download", mixInfo.downloadDuration.GetTotalDuration()},
38                         {"synthesis", mixInfo.synthesisDuration.GetTotalDuration()},
39                         {"upload", mixInfo.uploadDuration.GetTotalDuration()}
40                     }
41                 },
42                 {
43                     "fileInfo", new Dictionary<string, object>()
44                     {
45                         {"front_audio", mixInfo.synthesisAudioinfo.FrontAudioInfo},
46                         {"back_audio", mixInfo.synthesisAudioinfo.BackAudioInfo},
47                         {"synthesis_audio", mixInfo.synthesisAudioinfo.SynthesisInfo}
48                     }
49                 }
50             }) {StatusCode = ClientStatusCode.Ok};
51         }

 

而SynthesisAudio函數的源碼如下(源碼過長,有興趣的朋友可以自行實現自己想要的音頻處理邏輯和效果,全然當此段為參考範本)

  1 public dynamic SynthesisAudio(string frontFileUrl, int backgounedAudioIndex, string taskName)
  2         {
  3             // 將當前任務添加到隊列列表池中,用於限制本機最大隊列數量,防止單機隊列過多而死機
  4             CurrentQueueTask.Add(taskName);
  5 
  6             ProcessState.CurrentAudioProcessingState = AudioProcessingState.StartHandler;
  7             _iCacheAsyncService.SetDatabase(0);
  8             _iCacheAsyncService.SetStringAsync(taskName,
  9                 JsonConvert.SerializeObject(new ProgressPrompt()
 10                 {
 11                     Remarks = ProcessState.GetState(),
 12                     Progress = 10
 13                 }),
 14                 TimeSpan.FromDays(KeyExpire));
 15 
 16             var totalDuration = new DurationMath();
 17             var synthesisDuration = new DurationMath();
 18             var downloadDuration = new DurationMath();
 19             var uploadDuration = new DurationMath();
 20             var backgroundInfo = new BackGroundAudio();
 21 
 22             string aliyunReturnUrl;
 23             MixedInfo synthesisAudioinfo;
 24 
 25             try
 26             {
 27                 #region 獲取前景和背景音頻文件
 28 
 29                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.DownloadAudio;
 30                 downloadDuration.Start();
 31 
 32                 using (var r = GetFrontFileAndBackGroundAudio(frontFileUrl, backgounedAudioIndex))
 33                 {
 34                     if (!r.IsExceptionReturn)
 35                     {
 36                         backgroundInfo = (BackGroundAudio) r.ReturnObjects;
 37 
 38                         if (backgroundInfo == null)
 39                         {
 40                             _iCacheAsyncService.SetStringAsync(taskName,
 41                                 JsonConvert.SerializeObject(new ProgressPrompt()
 42                                 {
 43                                     Remarks =
 44                                         $"InExpcetion_{DateTime.Now}_{taskName}_request_audio_index_not_found_for_{backgounedAudioIndex}",
 45                                     Progress = 10
 46                                 }),
 47                                 TimeSpan.FromDays(KeyExpire));
 48 
 49                             return new JsonResult($"請求的背景音樂索引不存在{backgounedAudioIndex}")
 50                                 {StatusCode = ClientStatusCode.ClientParameterError};
 51                         }
 52 
 53                         backgroundInfo.AudioUrl = AppDomain.CurrentDomain.BaseDirectory
 54                                                   + "StaticResources/"
 55                                                   + backgroundInfo.AudioUrl;
 56 
 57                         if (!System.IO.File.Exists(backgroundInfo.AudioUrl))
 58                         {
 59                             _iCacheAsyncService.SetStringAsync(taskName,
 60                                 JsonConvert.SerializeObject(new ProgressPrompt()
 61                                 {
 62                                     Remarks =
 63                                         $"InExpcetion_{DateTime.Now}_{taskName}_request_audio_path_not_found_for_{backgroundInfo.AudioUrl}",
 64                                     Progress = 100
 65                                 }),
 66                                 TimeSpan.FromDays(KeyExpire));
 67 
 68                             return new JsonResult($"背景音頻文件物理路徑不存在{backgroundInfo.AudioUrl}")
 69                                 {StatusCode = ClientStatusCode.ClientParameterError};
 70                         }
 71                     }
 72                     else
 73                     {
 74                         _iCacheAsyncService.SetStringAsync(taskName,
 75                             JsonConvert.SerializeObject(new ProgressPrompt()
 76                             {
 77                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
 78                                 Progress = 100
 79                             }),
 80                             TimeSpan.FromDays(KeyExpire));
 81 
 82                         return new JsonResult($"文件獲取失敗,詳見錯誤日誌({r.ExceptionCode})輸出")
 83                             {StatusCode = ClientStatusCode.ServerHandlerError};
 84                     }
 85                 }
 86 
 87                 _iCacheAsyncService.SetStringAsync(taskName,
 88                     JsonConvert.SerializeObject(new ProgressPrompt()
 89                     {
 90                         Remarks = ProcessState.GetState(),
 91                         Progress = 30
 92                     }),
 93                     TimeSpan.FromDays(KeyExpire));
 94 
 95                 downloadDuration.Stop();
 96 
 97                 #endregion
 98 
 99                 #region 音頻合成
100 
101                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.SynthesisAudio;
102                 synthesisDuration.Start();
103                 using (var r = GetCustomMixedTwoAudio(frontFileUrl, backgroundInfo.AudioUrl))
104                 {
105                     if (!r.IsExceptionReturn)
106                     {
107                         synthesisAudioinfo = (MixedInfo) r.ReturnObjects;
108                     }
109                     else
110                     {
111                         _iCacheAsyncService.SetStringAsync(taskName,
112                             JsonConvert.SerializeObject(new ProgressPrompt()
113                             {
114                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
115                                 Progress = 100
116                             }),
117                             TimeSpan.FromDays(KeyExpire));
118 
119                         return new JsonResult($"音頻合成失敗,詳見錯誤日誌({r.ExceptionCode})輸出")
120                         {
121                             StatusCode = ClientStatusCode.ServerHandlerError
122                         };
123                     }
124                 }
125 
126                 _iCacheAsyncService.SetStringAsync(taskName,
127                     JsonConvert.SerializeObject(new ProgressPrompt()
128                     {
129                         Remarks = ProcessState.GetState(),
130                         Progress = 60
131                     }),
132                     TimeSpan.FromDays(KeyExpire));
133 
134                 synthesisDuration.Stop();
135 
136                 #endregion
137 
138                 #region 上傳到阿裡雲
139 
140                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.UploadAudio;
141                 uploadDuration.Start();
142 
143                 using (var r = UploadTheAliyun(synthesisAudioinfo))
144                 {
145                     if (!r.IsExceptionReturn)
146                     {
147                         aliyunReturnUrl = (string) r.ReturnObjects;
148                     }
149                     else
150                     {
151                         _iCacheAsyncService.SetStringAsync(taskName,
152                             JsonConvert.SerializeObject(new ProgressPrompt()
153                             {
154                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
155                                 Progress = 100
156                             }),
157                             TimeSpan.FromDays(KeyExpire));
158 
159                         return new JsonResult($"上傳阿裡雲失敗,詳見錯誤日誌({r.ExceptionCode})輸出")
160                         {
161                             StatusCode = ClientStatusCode.ServerHandlerError
162                         };
163                     }
164                 }
165 
166                 _iCacheAsyncService.SetStringAsync(taskName,
167                     JsonConvert.SerializeObject(new ProgressPrompt()
168                     {
169                         Remarks = ProcessState.GetState(),
170                         Progress = 80
171                     }),
172                     TimeSpan.FromDays(KeyExpire));
173                 uploadDuration.Stop();
174 
175                 #endregion
176 
177                 #region 存儲到資料庫
178 
179                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.UpdateDatabase;
180                 var dataOptionException = "";
181                 _dataOpService.DataOperatedCallBackEvent += (sender, args) =>
182                 {
183                     if (!string.IsNullOrEmpty(args.Exceptions))
184                     {
185                         _iCacheAsyncService.SetStringAsync(taskName,
186                             JsonConvert.SerializeObject(new ProgressPrompt()
187                             {
188                                 Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
189                                 Progress = 100
190                             }),
191                             TimeSpan.FromDays(KeyExpire));
192 
193                         dataOptionException = args.Exceptions;
194                     }
195                 };
196 
197                 // 將非同步最終結果放入到資料庫中,便於多次查詢
198                 var addResult = _dataOpService.AddEntity(new AudioSynthesisAsyncResult
199                 {
200                     HandlerResult = JsonConvert.SerializeObject(synthesisAudioinfo),
201                     TaskName = taskName,
202                     web_url = aliyunReturnUrl,
203                     Duration = JsonConvert.SerializeObject(new Dictionary<string, object>()
204                     {
205                         {"download", downloadDuration.GetTotalDuration()},
206                         {"synthesis", synthesisDuration.GetTotalDuration()},
207                         {"upload", uploadDuration.GetTotalDuration()}
208                     })
209                 }, new MediaContext(new DatabaseConfig())).Result;
210 
211                 if (!string.IsNullOrEmpty(dataOptionException))
212                 {
213                     _iCacheAsyncService.SetStringAsync(taskName,
214                         JsonConvert.SerializeObject(new ProgressPrompt()
215                         {
216                             Remarks = $"InExpcetion_{DateTime.Now}_{taskName}",
217                             Progress = 100
218                         }),
219                         TimeSpan.FromDays(KeyExpire));
220 
221                     return new JsonResult("資料庫操作失敗,詳見錯誤日誌(Error)輸出")
222                     {
223                         StatusCode = ClientStatusCode.ServerHandlerError
224                     };
225                 }
226 
227                 #endregion
228 
229                 if (addResult > 0)
230                 {
231                     ProcessState.CurrentAudioProcessingState = AudioProcessingState.InCompleted;
232                     _iCacheAsyncService.SetStringAsync(taskName,
233                         JsonConvert.SerializeObject(new ProgressPrompt()
234                         {
235                             Remarks = ProcessState.GetState(),
236                             Progress = 100
237                         }),
238                         TimeSpan.FromDays(KeyExpire));
239                 }
240 
241                 // 屏蔽返回字元串中詳細的IO地址
242                 synthesisAudioinfo.FrontAudioInfo.filename =
243                     Path.GetFileName(synthesisAudioinfo.FrontAudioInfo.filename);
244                 synthesisAudioinfo.BackAudioInfo.filename =
245                     Path.GetFileName(synthesisAudioinfo.BackAudioInfo.filename);
246                 synthesisAudioinfo.SynthesisInfo.filename =
247                     Path.GetFileName(synthesisAudioinfo.SynthesisInfo.filename);
248                 totalDuration.Stop();
249 
250                 CurrentQueueTask.Remove(taskName);
251                 ProcessState.CurrentAudioProcessingState = AudioProcessingState.EmptyHandler;
252                 return new AudioSynthesisSyncResult
253                 {
254                     WebUrl = aliyunReturnUrl,
255                     downloadDuration = downloadDuration,
256                     synthesisDuration = synthesisDuration,
257                     uploadDuration = uploadDuration,
258                     synthesisAudioinfo = synthesisAudioinfo
259                 };
260             }
261             catch (Exception e)
262             {
263                 Console.WriteLine(e);
264                 CurrentQueueTask.Remove(taskName);
265                 return e;
266             }
267         }
View Code

 

 

本節總結

先畫個圖:

畫的比較簡單:-)

當客戶端請求該介面的時候,邏輯伺服器收到請求,並通過ffmpeg進行處理,當ffmpeg處理完成後,將新的文件上傳到雲OSS,通過邏輯伺服器再將雲URL將返回給請求客戶端,很簡單。

 

註意問題

按照如上的代碼,很快便實現了這個項目的需求,可能最多也就三天時間吧(包括測試)。心裡暗示,嗯,功能我完成了,可以向上彙報了。

可細心的朋友、或有過多年WEB伺服器工作經驗的朋友就會發現一個至關重要的問題:這個功能介面的TPS非常的慢。為何這麼說,單機的性能是固定的,而用戶所錄製的音頻時長卻不是固定的,假如就算限製為3分鐘的錄音(微信最長才60秒),那麼伺服器會面臨1s-180s區間不同的處理耗時,即使用上目前最好的多路CPU,恐怕也不可能在180s的處理需求中達到毫秒級的處理響應吧。

我們這樣來假設一個場景(嗯,有點機器學習的味道o(∩_∩)o),用戶錄音為10秒,背景聲音為15秒(前後加點聽覺緩衝:漸入和漸出),筆者多次測試過,包括用上E-2680 v2版的CPU,忽略下載和上傳時間,FFMPEG處理時間仍然需要3秒(FFMPEG的處理模型是如何工作的我不清楚,加入-threads參數也無濟於事)。因此可得到這樣一個公式:

如果這台伺服器大部分時間都耗在處理一個任務上,那麼出現多個請求都在這個介面上呢,那麼時間將會更加的長,筆者試過一次併發10個請求(對於處理時間以秒為單位的狀況,10個併發對單機是很嚇人的),結果最後一個請求結果得到的時間是48秒,哈哈,客戶端肯定是無法等待這麼長的時間的,而且這個10個請求中,有2個請求出現了運行時錯誤...

這樣的問題很嚴重,就算功能實現了也決不能部署到生產環境中,也是筆者開闢這個系列的主要解決目標。也許你有更好的思路或者建議,歡迎大家一起

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JMM簡介 Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發者提供的多線程環境下的記憶體可見性、是否可以重排序等問題的無關具體平臺的統一的保證。(可能在術語上與Java運行時記憶體分佈有歧義,後者指堆、方法區、線程棧等記憶體區域)。併發編程有多種風格,除了CSP(通信順序 ...
  • 一.標識符 標識符:在java程式中,有些名字是我們自己定義的,那麼這些我們自己定義的名字就叫做自定義的標識符 標識符的命名規則: 1.標識符是由字母(a-z A-Z)、數字、下劃線(_)、美元符號($)組成的 2.標識符不能以數字開頭 3.標識符是嚴格區分大小寫的 4.標識符是沒有長度限制的 5. ...
  • 在C#中消息有兩個指向,一個指向Message,一個指向INotify。這裡主要講INotify。 INotify也有人稱之為[通知],不管叫消息還是通知,都是一個意思,就是傳遞信息。 消息的定義 INotify消息其實是一個介面,介面名叫INotifyPropertyChanged。介面定義如下: ...
  • 持續集成配置之Nuget Intro 本文是基於微軟的 VSTS(Visual Studio Team Service) 做實現公眾類庫的自動打包及發佈。 之前自己的項目有通過 Github 上的 Travis 和 Appveyor,這次主要是用 VSTS 來做的,對比 appveyor 和 vst ...
  • 概要 相信很多朋友在程式生涯中,或多或少都會遇到處理媒體流的需求,而且是採用S端處理,排除代碼上課優化的極限,仍然還是需要很長的時間時,比如: 1:百度網盤在播放視頻的時候,如非VIP會員還需要更長甚至直接斷開流; 2:任何直播視頻在轉碼的時候,不論是否VIP,都會有段緩衝時間,已至於觀看者無法達到 ...
  • 什麼是NoSql NoSQL(Not Only SQL),泛指非關係型的資料庫,是對不同於傳統的關係型資料庫的資料庫管理系統的統稱,強調Key-Value Stores和文檔資料庫的優點。為瞭解決大規模數據集合多重數據種類帶來的挑戰而興起的資料庫。有著模式自由,逆規範化,多分區存儲,彈性可擴展,多副 ...
  • 使用ILMerge工具,將C#項目debug目錄下的exe及其依賴的dll文件打包成一個exe文件,直接雙擊就可運行。 使用工具: ILMerge :http://www.microsoft.com/en-us/download/details.aspx?id=17630 ILMerge-GUI:h ...
  • 眾所周知垂直擴展是提升單機的性能的方式,比如提升雙路、四路的CPU運算能力,加大記憶體,更換速度更快的SSD,或者從代碼根本上進行優化和性能提升。水平擴展是提供多台多種伺服器分離單機性能的方式,比如集群,主從,隊列,負載平衡等等。 白話的垂直擴展 現在伺服器都是雲伺服器,單純從單機的硬體性能提升整體性 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...