整合一個可用於生產環境的靜態服務

開發過文件存儲那塊業務的小夥伴或多或少都應該瞭解過諸如: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的引入我不做太多介紹,請看此篇文章:https://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("http://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="http://*: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,可看官網:https://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:https://github.com/Davenever/static-service.git

 朝看花開滿樹紅,暮看花落樹還空😊

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章