zoukankan      html  css  js  c++  java
  • 一个简单的文件服务器实现方案

    引子

    最近公司的系统约来越多,基本上每个系统都需要用到“资源”,以前只是简单的把“资源”放到Web服务器中,但是这样的话有一个头痛的问题----如何去管理“资源”?

    想法

    现在不是很流行API嘛,大家好像都在整什么面向服务、面向资源、RESTful什么的,据说在与复杂性的斗争中,人们讨论表象化状态转移(REST)已成为了一种时尚!我对这些概念也就是知道个大概,但是也不能解释的很清楚,但是用意和优点还是很明确的!说白了就是各式各样的“API”,可能我的理解有偏差,还望大家海涵,哈哈!

    HTTP中有几个常见谓词,分别是GET/POST/PUT/DELETE,这也正是对应了我们经常说到的CRUD,含义就是对一个资源的增删改查!

    那咱能不能来一个文件API呢?实现对一个一个文件的CRUD?

    说时迟,那时快

    既然有了想法,咱就得开始干了!那么接下来的问题又来了,怎么干?

    文件的增删改查很简单,基本功呗!

    数据格式?字节数组吧,不用转来转去,

    数据传输呢?就跟一般的API一样走HTTP协议,HTTP请求报文中分为两个部分:请求头和请求体,既然这样,正好符合我们的需求,请求体承载文件流的字节数组,请求头中附加一些额外的信息!

    说到这,基本上大概的“形状”就有了!那咱就开始干!!!

    添加一个Web应用程序作为服务端,WebForms或者Mvc的都可以。我这里演示的是Mvc的!

    不废话,先上代码(只有上传操作),待会大概解释一下。

    // ***********************************************************************
    // Project            : Beimu.Bfs
    // Assembly         : Beimu.Bfs.Web
    // Author           : iceStone
    // Created          : 2014年01月03日 10:23
    //
    // Last Modified By : iceStone
    // Last Modified On : 2014年01月03日 10:23
    // ***********************************************************************
    // <copyright file="DefaultController.cs" company="Wedn.Net">
    //     Copyright (c) Wedn.Net. All rights reserved.
    // </copyright>
    // <summary></summary>
    // ***********************************************************************
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Web;
    using System.Web.Mvc;
    using Beimu.Bfs.Web.Utilities;
    
    namespace Beimu.Bfs.Web.Controllers
    {
        /// <summary>
        /// 默认控制器.
        /// </summary>
        /// <remarks>
        ///  2014年01月03日 10:23 Created By iceStone
        /// </remarks>
        public class DefaultController : Controller
        {
            /// <summary>
            /// 身份验证
            /// </summary>
            /// <param name="filterContext"></param>
            protected override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                var uid = Request.Headers["bfs-uid"];// Request["uid"];
                var pwd = Request.Headers["bfs-pwd"];//Request["pwd"];
                if (!(string.IsNullOrEmpty(uid) || string.IsNullOrEmpty(pwd)))
                {
                    var user = new Users();
                    if (!user.Exist(uid) || user[uid] != pwd)
                    {
                        filterContext.Result = Json(new { Status = 400, Message = "用户名或密码不正确" });
                        return;
                    }
                }
                base.OnActionExecuting(filterContext);
            }
    
            /// <summary>
            /// 上传操作.
            /// </summary>
            /// <remarks>
            ///  2014年01月03日 10:23 Created By iceStone
            /// </remarks>
            /// <returns>ActionResult.</returns>
            public ActionResult Upload()
            {
                #region 批量
                //var files = Request.Files;
                //if (files == null || files.Count == 0) return Json(new { Status = 101, Message = "" });
                //var dict = new Dictionary<int, string>();
                //int index = -1;
                //foreach (HttpPostedFile file in files)
                //{
                //    index++;
                //    if (file == null || file.ContentLength == 0) continue;
                //    var ext = Path.GetExtension(file.FileName);
                //    if (!Config.UploadAllowType.Contains(ext)) continue;
                //    if (file.ContentLength >= (Config.UploadMaxSize * 1024 * 1024)) continue;
    
                //    string root = AppDomain.CurrentDomain.BaseDirectory;
                //    string path = string.Format("{0}/{1}/{2}/{3}", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
                //    string filename = GetStreamMd5(file.InputStream) + ext;//Path.GetFileName(file.FileName);
                //    file.SaveAs(Path.Combine(root, path, filename));
    
                //    dict.Add(index, Config.SiteUrl + path + filename);
                //}
                //return Json(dict); 
                #endregion
    
                string ext = Request.Headers.AllKeys.Contains("bfs-ext") ? Request.Headers["bfs-ext"] : ".jpg";
                var dir = Request.Headers.AllKeys.Contains("bfs-dir") ? Request.Headers["bfs-dir"] : Request.Headers["bfs-uid"];
                //dir = string.IsNullOrEmpty(dir) ? "common" : dir;
    
                using (var stream = Request.InputStream)
                {
                    //var files = Request.Files;
                    if (stream.Length == 0) return SetHeaders(104, "上传文件为空");
    
                    if (stream.Length >= (Config.UploadMaxSize * 1024 * 1024)) return SetHeaders(101, "上传文件过大");
    
                    //string root = AppDomain.CurrentDomain.BaseDirectory;
                    string path = string.Format("/{0}/{1}/{2}/{3}/", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
                    string filename = GetStreamMd5(stream) + ext;//Path.GetFileName(file.FileName);
                    string fullPath = Server.MapPath(path);
                    if (!Directory.Exists(fullPath))
                        Directory.CreateDirectory(fullPath);
                    //var buffer = new byte[stream.Length];
                    //stream.Read(buffer, 0, buffer.Length);  //将流的内容读到缓冲区
                    //using (var fs = new FileStream(fullPath + filename, FileMode.CreateNew, FileAccess.Write))
                    //{
                    //    fs.Write(buffer, 0, buffer.Length);
                    //    fs.Flush();
                    //    //fs.Close();
                    //} 
                    //using (var reader=new StreamReader(stream))
                    //{
                    //    using (var writer = new StreamWriter(fullPath + filename, false))
                    //    {
                    //        writer.Write(reader.ReadToEnd());
                    //        writer.Flush();
                    //    }
                    //}
                    using (var fs = new FileStream(fullPath + filename, FileMode.Create))
                    {
                        byte[] bytes = new byte[stream.Length];
                        int numBytesRead = 0;
                        int numBytesToRead = (int)stream.Length;
                        stream.Position = 0;
                        while (numBytesToRead > 0)
                        {
                            int n = stream.Read(bytes, numBytesRead, Math.Min(numBytesToRead, int.MaxValue));
                            if (n <= 0)
                                break;
                            fs.Write(bytes, numBytesRead, n);
                            numBytesRead += n;
                            numBytesToRead -= n;
                        }
                        fs.Close();
                    }
                    return SetHeaders(100, Config.SiteUrl + path + filename);
                }
    
                //if (file == null || file.ContentLength == 0) return SetHeaders(103, "上传文件为空");
    
                //var ext = Path.GetExtension(file.FileName);
                //if (!Config.UploadAllowType.Contains(ext)) return SetHeaders(102, "上传非法文件");
                //if (file.ContentLength >= (Config.UploadMaxSize * 1024 * 1024)) return SetHeaders(101, "上传文件过大");
    
                //string root = AppDomain.CurrentDomain.BaseDirectory;
                //string path = string.Format("{0}/{1}/{2}/{3}", Config.UploadRoot, dir, DateTime.Now.ToString("yyyy"), DateTime.Now.ToString("MM"));
                //string filename = GetStreamMd5(file.InputStream) + ext;//Path.GetFileName(file.FileName);
                //string fullPath = Path.Combine(root, path);
                //if (!Directory.Exists(fullPath))
                //    Directory.CreateDirectory(fullPath);
                //file.SaveAs(Path.Combine(root, path, filename));
                //return SetHeaders(100, Config.SiteUrl + path + filename);
            }
    
            [NonAction]
            public ContentResult SetHeaders(int status, string resault)
            {
                Response.Headers.Add("bfs-status", status.ToString());
                Response.Headers.Add("bfs-result", resault);
                return Content(string.Empty);
            }
    
            /// <summary>
            /// 获取文件的MD5值
            /// </summary>
            /// <remarks>
            ///  2013年11月28日 19:24 Created By 汪磊
            /// </remarks>
            /// <param name="stream">文件流</param>
            /// <returns>该文件的MD5值</returns>
            [NonAction]
            public String GetStreamMd5(Stream stream)
            {
                var oMd5Hasher = new MD5CryptoServiceProvider();
                byte[] arrbytHashValue = oMd5Hasher.ComputeHash(stream);
                //由以连字符分隔的十六进制对构成的String,其中每一对表示value 中对应的元素;例如“F-2C-4A”
                string strHashData = BitConverter.ToString(arrbytHashValue);
                //替换-
                strHashData = strHashData.Replace("-", "");
                string strResult = strHashData;
                return strResult;
            }
    
        }
    }

    看完代码的同学估计已经发现,其实实现的过程也有不顺利,其实刚开始就是没有想好数据放在请求报文的什么位置,最初尝试了附件的形式,的确可以实现,但是出于某种原因(下面会说到)还是折腾成现在的样子了!

    大概理一下流程:

    1. 首先一个请求过来,校验请求头中身份信息是否存在并且合法,很简单!我就是读了配置文件中定义的用户列表。可以做扩展;
    2. 获取文件类型(请求头中的信息)和文件存放的目录(如果不存在目录参数,默认以当前请求的用户名作为基本目录);
    3. 判断请求体中有没有内容,没有内容返回错误码和错误消息;
    4. 判断上传文件格式是否为允许格式(有个问题“客户端欺骗服务端”,因为文件格式是客户端在请求头中注明的,但是回头想想好像没什么大问题,如果客户端伪装拓展名,最终按照他伪装的拓展名保存,当然这里大家可以办法提出更好更安全的想法),不允许返回错误码和错误消息;
    5. 判断文件大小是否允许,不允许返回错误码和错误消息;
    6. 获取文件流的MD5值作为文件名(防止重复资源占用空间,网盘就是这么干的,所以说所谓的多少T,所谓的秒传,都是扯淡),
    7. 最后以/{指定目录}/{年份}/{月份}/{文件名}{拓展名}保存到服务器!(如果有需要做成集群的话,那就需要我们再做一些拓展了,以后可以详细介绍,实际上就是弄个数据库弄个表保存一下每个服务器的资源情况,以及资源的保存位置)
    8. 最后返回给客户端文件的地址

    这样我们最简单的文件API就完成了,当然了它是很简陋的,但是我更在乎的是思想上的问题,方向对不对!其实咱们做这一行最关键的就是这么点事,技术好不好都是时间的事!

    好像还少点啥

    怎么觉着好像还是少点东西呢?

    一般的API还有什么东西?-------客户端呐!总不能让使用者自己做吧,那样的结果只会有各种各样的情况产生。

    那再来个客户端呗!

    好的!

    还是上代码吧?

    using System;
    using System.Configuration;
    using System.IO;
    using System.Net;
    
    namespace Beimu.Bfs.Lib
    {
        public class BfsUploader
        {
            public readonly string ApiDomain = ConfigurationManager.AppSettings["bfs_server"];
            public string Directory { get; set; }
            public string UserId { get; set; }
            public string Password { get; set; }
    
            /// <summary>
            /// 构造函数
            /// </summary>
            /// <param name="uid">用户名</param>
            /// <param name="pwd">密码</param>
            /// <param name="dir">操作目录</param>
            public BfsUploader(string uid, string pwd, string dir = "common")
            {
                Directory = dir;
                UserId = uid;
                Password = pwd;
            }
    
            /// <summary>
            /// 写文件
            /// </summary>
            /// <param name="filePath">文件路径</param>
            /// <param name="result">结果</param>
            /// <returns>是否成功</returns>
            public bool WriteFile(string filePath, out string result)
            {
                var ext = Path.GetExtension(filePath);
                using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    using (var reader = new BinaryReader(fs))
                    {
                        byte[] postData = reader.ReadBytes((int)fs.Length);
                        return Post("upload", ext, postData, out result);
                    }
                }
            }
            /// <summary>
            /// 写文件
            /// </summary>
            /// <param name="ext">拓展名</param>
            /// <param name="postData">请求体</param>
            /// <param name="result">结果</param>
            /// <returns>是否成功</returns>
            public bool WriteFile(string ext, byte[] postData, out string result)
            {
                return Post("upload",ext, postData, out result);
            }
    
            /// <summary>
            /// 发送请求
            /// </summary>
            /// <param name="url">请求地址</param>
            /// <param name="ext">拓展名</param>
            /// <param name="postData">请求体</param>
            /// <param name="result">结果</param>
            /// <returns>是否成功</returns>
            private bool Post(string url, string ext, byte[] postData, out string result)
            {
                var request = (HttpWebRequest)WebRequest.Create(ApiDomain + url);
    
                request.Method = "POST";
                request.Headers.Add("bfs-uid", UserId);
                request.Headers.Add("bfs-pwd", Password);
                request.Headers.Add("bfs-dir", Directory);
                request.Headers.Add("bfs-ext", ext);
    
    
                if (postData != null)
                {
                    request.ContentLength = postData.Length;
                    request.KeepAlive = true;
    
                    Stream dataStream = request.GetRequestStream();
                    dataStream.Write(postData, 0, postData.Length);
                    dataStream.Close();
                }
                try
                {
                    var response = (HttpWebResponse)request.GetResponse();
                    var status = response.Headers["bfs-status"];
                    result = response.Headers["bfs-result"];
                    return response.StatusCode == HttpStatusCode.OK && status == "100";
                }
                catch (Exception e)
                {
                    throw e;
                }
                //string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
                //byte[] boundarybytes = Encoding.UTF8.GetBytes("
    --" + boundary + "
    ");
                //byte[] endbytes = Encoding.UTF8.GetBytes("
    --" + boundary + "--
    ");
    
                ////1.HttpWebRequest
                //HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                //request.ContentType = "multipart/form-data; boundary=" + boundary;
                //request.Method = "POST";
                //request.KeepAlive = true;
                //request.Credentials = CredentialCache.DefaultCredentials;
    
                //using (Stream stream = request.GetRequestStream())
                //{
                //    //1.1 key/value
                //    string formdataTemplate = "Content-Disposition: form-data; name="{0}"
    
    {1}";
                //    if (data != null)
                //    {
                //        foreach (string key in data.Keys)
                //        {
                //            stream.Write(boundarybytes, 0, boundarybytes.Length);
                //            string formitem = string.Format(formdataTemplate, key, data[key]);
                //            byte[] formitembytes = encoding.GetBytes(formitem);
                //            stream.Write(formitembytes, 0, formitembytes.Length);
                //        }
                //    }
    
                //    //1.2 file
                //    string headerTemplate = "Content-Disposition: form-data; name="{0}"; filename="{1}"
    Content-Type: application/octet-stream
    
    ";
                //    byte[] buffer = new byte[4096];
                //    int bytesRead = 0;
                //    for (int i = 0; i < files.Length; i++)
                //    {
                //        stream.Write(boundarybytes, 0, boundarybytes.Length);
                //        string header = string.Format(headerTemplate, "file" + i, Path.GetFileName(files[i]));
                //        byte[] headerbytes = encoding.GetBytes(header);
                //        stream.Write(headerbytes, 0, headerbytes.Length);
                //        using (FileStream fileStream = new FileStream(files[i], FileMode.Open, FileAccess.Read))
                //        {
                //            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
                //            {
                //                stream.Write(buffer, 0, bytesRead);
                //            }
                //        }
                //    }
    
                //    //1.3 form end
                //    stream.Write(endbytes, 0, endbytes.Length);
                //}
                ////2.WebResponse
                //HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                //using (StreamReader stream = new StreamReader(response.GetResponseStream()))
                //{
                //    return stream.ReadToEnd();
                //}
            }
        }
    }

    代码实现也很简单,就是封装请求操作罢了!

    没什么逻辑,今天就不分析了!

    The End~

    其实用单独的文件服务器的好处有很多;

    其中比较关键的两点:

    第一、促使浏览器并行下载多个请求、提高加载速度。

    对于浏览器而言,同时对同域名下的请求是有限的,IE应该是五个左右,具体可以监控请求查看,而单独部署静态资源文件可以解决一点问题;

    第二、cookie-free,暨避免不必要的请求头。

    稍微说一点:浏览器请求服务器的时候会带着该域名下的Cookie等请求头的信息,而这些信息对于资源文件似乎是没有任何意义的,只是增加负担。

    顺便提醒一下,外部链接也不要太多,会适得其反!对SEO有影响。据权威(YAHOO)研究表明,应该是5个左右!

  • 相关阅读:
    APP专项测试方法有哪些?
    软件测试基础知识
    软件测试入门随笔——软件测试基础知识
    如何做接口测试
    App测试页面滑动
    什么是接口测试
    自动化测试
    测试用例设计方法
    Monkey测试手机BUG重现及解决方法
    软件测试工程师需要具备哪些数据库知识
  • 原文地址:https://www.cnblogs.com/micua/p/a-simple-file-server-implementation-scheme.html
Copyright © 2011-2022 走看看