整合一個可用於生產環境的靜態服務
開發過文件存儲那塊業務的小夥伴或多或少都應該了解過諸如:FastDFS、Minio、MongDb GridFS,通過這些第三方組件可以應用於我們的文件存儲系統。
之前有用過Minio,性能很高而且部署起來非常簡單,有興趣的同學可以嘗試一下。😉
同樣,在.Net Core中我們一樣可以處理靜態文件的讀取與寫入,我們一般將靜態文件都放置於wwwroot文件夾下,但是我們可以自行擴展配置,將對外的URL映射到自定義的目錄達到外部對其文件訪問的目的。
於是我自己用.Net Core整合了一個文件服務,對外可提供文件的讀寫操作。特出此文,希望能幫助到大家,僅供參考!😄
我們以微軟的官方文檔為主,官方文檔是最準確,最可靠的。✌️ https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0
項目的整體結構如下(因為沒有涉及過多業務層面的內容,所以分的層也比較簡單,但是也易於後期擴展):
如果要使用靜態文件的話,我們需要在Configure方法中註冊UseStaticFiles中間件,這裡話我寫了一個擴展方法,如下:
/// <summary> /// 使用靜態文件 /// </summary> public static class StaticFilesMildd { public static void UseStaticFilesMildd(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); var staticfile = new StaticFileOptions(); staticfile.ServeUnknownFileTypes = true; var provider = new FileExtensionContentTypeProvider(); #region 手動設置對應的 MIME TYPE =>下載 provider.Mappings[".log"] = "application/x-msdownload"; provider.Mappings[".xls"] = "application/x-msdownload"; provider.Mappings[".doc"] = "application/x-msdownload"; provider.Mappings[".pdf"] = "application/x-msdownload"; provider.Mappings[".docx"] = "application/x-msdownload"; provider.Mappings[".xlsx"] = "application/x-msdownload"; provider.Mappings[".ppt"] = "application/x-msdownload"; provider.Mappings[".pptx"] = "application/x-msdownload"; provider.Mappings[".zip"] = "application/x-msdownload"; provider.Mappings[".rar"] = "application/x-msdownload"; provider.Mappings[".dwg"] = "application/x-msdownload"; #endregion staticfile.ContentTypeProvider = provider; //app.UseStaticFiles(staticfile); // 使用默認文件夾 wwwroot 僅僅使wwwroot對外可見 app.UseStaticFiles(new StaticFileOptions() {
//這裡的路徑寫部署的主機的某個文件夾的路徑 FileProvider = new PhysicalFileProvider(@"/data/Files"), }); } }
這裏面說一下FileExtensionContentTypeProvider類,這個類裏面包含了381種文件的類型。我們可以通過這個類對其文件類型進行刪除或替換,比如上面我就把這些文件的MIME 內容類型
給替換掉了,當客戶端訪問的時候就都為下載。假設我們上傳的是一張jpg圖片,當我們用其URL進行訪問時,則可以在瀏覽器上直接顯示,因為其Content-Type是image/jpeg。如果設置
文件的MIME內容類型是application/x-msdownload這種,那麼就會下載。歸根結底是Response-Header裏面的Content-Type指示瀏覽器這是什麼類型,而不是通過網址後綴去判斷的
我們還需要對外提供的上傳文件的接口,主要代碼如下,Controller層的代碼我就不貼出來了,都很簡單😊
抽象接口層主要代碼:
public interface IUploadBusiness { Task<string> uploadFile(FileDataBase64 file); Task<List<string>> uploadFile(FormData data); Task<string> uploadFile(FileDataStream file); Task<List<string>> uploadFile(FormDataStream data); Task<string> uploadFile(FileDataByte file); Task<List<string>> uploadFile(FormDataByte data); }
業務實現層主要代碼:
public class UploadBusiness : IUploadBusiness { private readonly FileHelper _helper; private readonly UtilConvert _utilConvert; public UploadBusiness(FileHelper helper, UtilConvert utilConvert) { _helper = helper; _utilConvert = utilConvert; } /// <summary> /// 上傳單個文件 /// </summary> /// Base64 /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataBase64 file) { return await _helper.uploadFileHelper(file, fileType.base64); } /// <summary> /// 上傳多個文件 /// </summary> /// Base64 /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormData data) { List<string> picStrArray = new(); foreach (var item in data.files) { picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, FileByBase64 = item.FileByBase64 }, fileType.base64)); } return picStrArray; } /// <summary> /// 上傳單個文件 /// </summary> /// Stream /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataStream file) { return await _helper.uploadFileHelper(new { suffix = file.suffix, fileStream = file.FileByStream.OpenReadStream() }, fileType.stream); } /// <summary> /// 上傳多個文件 /// </summary> /// Stream /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormDataStream data) { List<string> picStrArray = new(); foreach (var item in data.files) { picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, fileStream = item.FileByStream.OpenReadStream() }, fileType.stream)); } return picStrArray; } /// <summary> /// 上傳單個文件 /// </summary> /// Byte /// <param name="files"></param> /// <returns></returns> public async Task<string> uploadFile(FileDataByte file) { FileDataStream fileData = new(); var fileByte = await _utilConvert.StringByByte(file.FileByByte); return await _helper.uploadFileHelper(new { suffix = file.suffix, fileStream = await _utilConvert.BytesToStream(fileByte) }, fileType.stream); } /// <summary> /// 上傳多個文件 /// </summary> /// Byte /// <param name="files"></param> /// <returns></returns> public async Task<List<string>> uploadFile(FormDataByte data) { List<string> picStrArray = new(); foreach (var item in data.files) { var fileByte = await _utilConvert.StringByByte(item.FileByByte); picStrArray.Add(await _helper.uploadFileHelper(new { suffix = item.suffix, fileStream = await _utilConvert.BytesToStream(fileByte) }, fileType.stream)); } return picStrArray; } }
統一的上傳公共方法層主要代碼:
public class FileHelper { private readonly IHostingEnvironment _hostingEnvironment; private readonly IConfiguration _configuration; public FileHelper(IHostingEnvironment hostingEnvironment, IConfiguration configuration) { _hostingEnvironment = hostingEnvironment; _configuration = configuration; } public async Task<string> uploadFileHelper(dynamic data, fileType fileType) { string path = string.Empty; var picUrl = string.Empty; string Mainpath = string.Empty; if (CommonClass.suffixList.Contains(data.suffix.ToLower())) Mainpath = "/Pictures/" + DateTime.Now.ToString("yyyy-MM-dd") + "/"; else Mainpath = "/OtherFiles/" + DateTime.Now.ToString("yyyy-MM-dd") + "/"; string FileName = DateTime.Now.ToString("yyyyMMddHHmmssfff"); string FilePath = $@"/data/Files" + Mainpath; string type = data.suffix; DirectoryInfo di = new DirectoryInfo(FilePath); path = Mainpath + FileName + type; picUrl = $"{_configuration["machine-ip"]}{path}"; if (!di.Exists) { di.Create(); } if (fileType == fileType.base64) { byte[] arr = Convert.FromBase64String(data.FileByBase64); using (Stream stream = new MemoryStream(arr)) { using (FileStream fs = System.IO.File.Create(FilePath + FileName + type)) { // 複製文件 stream.CopyTo(fs); // 清空緩衝區數據 fs.Flush(); } } } else { using (FileStream fs = System.IO.File.Create(FilePath + FileName + type)) { // 複製文件 data.fileStream.CopyTo(fs); // 清空緩衝區數據 fs.Flush(); } } return picUrl; } }
這裡說一下,主機ip我寫在了配置文件中,每上傳一個文件都對其進行路徑拼接。可以上傳Base64,Stream或Byte都行。😊
只有讀取、寫入只是完成了一部分,我們要管控起我們的服務,鑒權、日誌、限流、健康檢查、備份、集群這些都要需要加入進來。
授權與鑒權
鑒權是指對我們文件上傳的接口進行鑒權,校驗其令牌的合法性。授權的話就很簡單,只需在控制器或方法上加一個特性[Authorize]即可
這裡的話我使用JWT,它可以做到跨服務器驗證,只要密鑰和算法相同,不同服務器程序生成的Token可以互相驗證。如果是微服務架構的話需要介入基於 OpenID Connect 和 OAuth 2.0 認證框架IdentityServer4.
主要代碼如下:
//添加jwt驗證: .AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ClockSkew = TimeSpan.FromSeconds(15), ValidateAudience = true, ValidAudience = "Static-Service", ValidIssuer = "localhost", ValidateIssuerSigningKey = true,//是否驗證SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:key"]))//拿到SecurityKey }; options.Events = new JwtBearerEvents { //此處為權限驗證失敗後觸發的事件 OnChallenge = context => { context.HandleResponse(); //自定義自己想要返回的數據結果 var payload = JsonConvert.SerializeObject(new { code = 401, success = false, msg = "很抱歉,您無權訪問該接口。" }); //自定義返回的數據類型 context.Response.ContentType = "application/json"; context.Response.StatusCode = StatusCodes.Status200OK; //輸出Json數據結果 context.Response.WriteAsync(payload); return Task.FromResult(0); } }; });
SecurityKey不能太短了,要在16位以上。這裡當鑒權不成功時我就自定義返迴響應的數據,以供前端好拿到數據,不然就會返回401Unauthorized,這樣不太友好。
如果項目有網關的加入,那麼網關就作為了鑒權的入口,如下:
日誌
日誌的記錄我這裡使用的是Serilog,同時利用Filter將每次請求與異常都寫入了DataBase,藉助Elasticsearch和Kibana可以快速查看我們的日誌信息。
具體關於Serilog的引入我不做太多介紹,請看此篇文章://www.cnblogs.com/zhangnever/p/12459399.html
DataBase中的日誌表,如下:
CREATE TABLE `system_log` ( `id` VARCHAR(36) NOT NULL COMMENT '主鍵', `ip_address` VARCHAR(100) NOT NULL COMMENT '請求ip', `oper_type` VARCHAR(50) NOT NULL COMMENT '操作類型:查看、新增、修改、刪除、導入、導出', `oper_describe` VARCHAR(500) NOT NULL COMMENT '操作詳情', `level` VARCHAR(50) NOT NULL COMMENT '事件', `exception` VARCHAR(500) NULL DEFAULT NULL COMMENT 'ErrorMessage', `create_op_id` VARCHAR(36) NOT NULL COMMENT '創建人id', `create_op_name` VARCHAR(50) NOT NULL COMMENT '創建人', `create_op_date` DATETIME NOT NULL COMMENT '創建時間', `edit_op_id` VARCHAR(36) NULL DEFAULT NULL COMMENT '修改人id', `edit_op_name` VARCHAR(50) NULL DEFAULT NULL COMMENT '修改人', `edit_op_date` DATETIME NULL DEFAULT NULL COMMENT '修改時間', PRIMARY KEY (`id`) ) COMMENT='系統日誌' COLLATE='utf8_general_ci' ENGINE=InnoDB ;
如果需要將日誌寫入ElasticSearch需要安裝包Serilog.Sinks.Elasticsearch
主要代碼:
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("//localhost:9200/")) { AutoRegisterTemplate = true, ModifyConnectionSettings = connectionSettings => {
//訪問的用戶名與密碼 connectionSettings.BasicAuthentication("username", "password"); return connectionSettings; } })
在Kibana中進行查看:
另外對於異常信息,我這裡還加入了郵件發送的功能:
//... ...
var apiUrl = _accessor.HttpContext.Request.Path.ToString(); _context.db.StringIncrement(apiUrl); var result = _context.db.StringGet(apiUrl); //Content mailMessage.Body = $"【Static-Service】當前請求{apiUrl}有異常,異常信息為{exception}。該API已累計異常了{result}次!";
//... ...
限流
對於請求的限流我們可以使用AspNetCoreRateLimit,它是根據IP進行限流,在管道中進行攔截,
源碼在此:https://github.com/stefanprodan/AspNetCoreRateLimit
這裡的話需要引入兩個包:AspNetCoreRateLimit與Microsoft.Extensions.Caching.Redis.
對於不同的Api可配置不同的限流規則,如下:
//... ...
//api規則 "GeneralRules": [ { "Endpoint": "*:/api/*", "Period": "1m", "Limit": 500 }, { "Endpoint": "*/api/*", "Period": "1s", "Limit": 3 }, { "Endpoint": "*/api/*", "Period": "1m", "Limit": 30 }, { "Endpoint": "*/api/*", "Period": "12h", "Limit": 500 } ]
其它代碼我就不羅列出來了,感興趣的同學可以把我的代碼down下來看看。如果是微服務項目的話,限流就在網關(Ocelot)處理了,還有就是對外暴露一個接口用作健康檢查。
文件備份
對於文件的備份我採用rsync+inotify的方式,inotity用來監控文件或目錄的變化,rsync用來遠程數據的同步。
我這裡有兩台服務器,分別是客戶端訪問的服務器(192.168.2.121)和要同步的遠程服務器(192.168.2.122)
一、首先在要同步的遠程服務器上做如下操作:
1.安裝工具(rsync):yum -y install xinetd rsync
安裝完成之後可查看版本看其是否安裝成功:rsync --version
2.設置與客戶端服務器同步的目錄,配置文件在etc/rsyncd.conf中.
[backup] path =/data/Files #同步的目錄 comment = Rsync share test auth users = root #用戶名 read only = no #只讀設為no hosts allow = 192.168.2.121 #客戶端服務器的ip hosts deny = *
3.設置訪問的用戶名與密碼,在/etc/rsync_pass中配置,如下:
root:******
4.重啟服務 service xinetd restart
5.檢測873端口是否已啟動 netstat -nultp
二、客戶端訪問的服務器上做如下配置:
1.安裝inotify-toolwget
鏈接:http://downloads.sourceforge.net/project/inotify-tools/inotify-tools/3.13/inotify-tools-3.13.tar.gz
解壓:tar -zxvf inotify-tools-3.13.tar.gz
進入inotify-tools-3.13目錄,依次執行 ./configure,make&make install
2.安裝工具(rsync):yum -y install xinetd rsync
安裝完成之後可查看版本看其是否安裝成功:rsync --version
3.設置要同步過去的遠程服務器的密碼
在/etc/rsync_pass文本中寫入即可
4.設置腳本,如下: #!/bin/bash inotify_rsync_fun () { dir=`echo $1 | awk -F"," '{print $1}'` ip=`echo $1 | awk -F"," '{print $2}'` des=`echo $1 | awk -F"," '{print $3}'` user=`echo $1 | awk -F"," '{print $4}'` inotifywait -mr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' -e modify,delete,create,attrib ${dir} | while read DATE TIME DIR FILE do FILECHAGE=${DIR}${FILE} /usr/bin/rsync -av --progress --delete --password-file=/etc/rsync_pass ${dir} ${user}@${ip}::${des} && echo "At ${TIME} on ${DATE}, <br> file $FILECHAGE was backed up via rsync" >> /var/log/rsyncd.log done } count=1 # localdir,host,rsync_module,user of rsync_module, sync1="/data/Files/,192.168.2.122,backup,root" ############################################################# #main i=0 while [ ${i} -lt ${count} ] do i=`expr ${i} + 1` tmp="sync"$i eval "sync=\$$tmp" inotify_rsync_fun "$sync" & done
部署
部署的話我先執行dotnet *.dll,然後用nginx做了一個代理轉發.
1.先查看是否安裝了dotnet環境:dotnet --info
2.將發佈好的項目Copy到服務器上,切入到項目根目錄執行:nohup dotnet Static-Application.dll --urls="//*:85" --ip="127.0.0.1" --port=85 >output 2>&1 &
這裡需要說一下在命令中加入nohup可以在你退出帳戶/關閉終端之後繼續運行相應的進程.
3.配置nginx,在/etc/nginx/conf.d文件中新增static-nginx.conf文件。配置的語句如下: server { listen 86; server_name localhost; proxy_set_header X-Real-IP $remote_addr; location /index.html { proxy_pass http://localhost:85/swagger/index.html; } location /api/{ proxy_pass http://localhost:85; } location /{ proxy_pass http://localhost:85/swagger/; } location /swagger/{ proxy_pass http://localhost:85; } }
4.檢查語句是否正確:nginx -t 重啟nginx:nginx -s reload
打開谷歌瀏覽器查看一下
GoAccess
這裡使用了nginx,可以用實時網絡日誌分析器GoAccess來統計和展示日誌信息。
1.安裝GoAccess,可看官網://goaccess.io/download
2.配置nginx,可在瀏覽器端訪問. server { listen 9000; server_name localhost; location /report.html { alias /home/zopen/nginx/html/report.html; } }
3.運行GoAccess關聯到nginx的日誌 goaccess /var/log/nginx/access.log -o /home/zopen/nginx/html/report.html --real-time-html --time-format='%H:%M:%S' --date-format='%d/%b/%Y' --log-format=COMBINED
在谷歌瀏覽器中打開查看
文中可能有疏漏,如有不當,望諒解!😊
但凡可以幫到各位一點點,那就沒白忙活.
代碼我已上傳至我的GitHub://github.com/Davenever/static-service.git
朝看花開滿樹紅,暮看花落樹還空😊