整合一个可用于生产环境的静态服务

开发过文件存储那块业务的小伙伴或多或少都应该了解过诸如: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

 朝看花开满树红,暮看花落树还空😊

 

posted @ 2021-01-25 18:26  江北、  阅读(791)  评论(6编辑  收藏  举报