引子
最近公司的系統約來越多,基本上每個系統都需要用到“資源”,以前只是簡單的把“資源”放到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; } } }
看完代碼的同學估計已經發現,其實實現的過程也有不順利,其實剛開始就是沒有想好數據放在請求報文的什么位置,最初嘗試了附件的形式,的確可以實現,但是出於某種原因(下面會說到)還是折騰成現在的樣子了!
大概理一下流程:
- 首先一個請求過來,校驗請求頭中身份信息是否存在並且合法,很簡單!我就是讀了配置文件中定義的用戶列表。可以做擴展;
- 獲取文件類型(請求頭中的信息)和文件存放的目錄(如果不存在目錄參數,默認以當前請求的用戶名作為基本目錄);
- 判斷請求體中有沒有內容,沒有內容返回錯誤碼和錯誤消息;
- 判斷上傳文件格式是否為允許格式(有個問題“客戶端欺騙服務端”,因為文件格式是客戶端在請求頭中注明的,但是回頭想想好像沒什么大問題,如果客戶端偽裝拓展名,最終按照他偽裝的拓展名保存,當然這里大家可以辦法提出更好更安全的想法),不允許返回錯誤碼和錯誤消息;
- 判斷文件大小是否允許,不允許返回錯誤碼和錯誤消息;
- 獲取文件流的MD5值作為文件名(防止重復資源占用空間,網盤就是這么干的,所以說所謂的多少T,所謂的秒傳,都是扯淡),
- 最后以/{指定目錄}/{年份}/{月份}/{文件名}{拓展名}保存到服務器!(如果有需要做成集群的話,那就需要我們再做一些拓展了,以后可以詳細介紹,實際上就是弄個數據庫弄個表保存一下每個服務器的資源情況,以及資源的保存位置)
- 最后返回給客戶端文件的地址
這樣我們最簡單的文件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("\r\n--" + boundary + "\r\n"); //byte[] endbytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n"); ////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}\"\r\n\r\n{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}\"\r\nContent-Type: application/octet-stream\r\n\r\n"; // 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個左右!
