一 介紹
斷點續傳搜索大部分都是下載的斷點續傳,涉及到HTTP協議1.1的Range和Content-Range頭。
來個簡單的介紹
所謂斷點續傳,也就是要從文件已經下載的地方開始繼續下載。在以前版本的 HTTP 協議是不支持斷點的,HTTP/1.1 開始就支持了。一般斷點下載時才用到 Range 和 Content-Range 實體頭。
Range
用於請求頭中,指定第一個字節的位置和最后一個字節的位置,一般格式:
Range:(unit=first byte pos)-[last byte pos]
Content-Range
用於響應頭,指定整個實體中的一部分的插入位置,他也指示了整個實體的長度。在服務器向客戶返回一個部分響應,它必須描述響應覆蓋的范圍和整個實體長度。一般格式:
Content-Range: bytes (unit first byte pos) – [last byte pos]/[entity legth]
請求下載整個文件:
- GET /test.rar HTTP/1.1
- Connection: close
- Host: 116.1.219.219
- Range: bytes=0-801 //一般請求下載整個文件是bytes=0- 或不用這個頭
一般正常回應
- HTTP/1.1 200 OK
- Content-Length: 801
- Content-Type: application/octet-stream
- Content-Range: bytes 0-800/801 //801:文件總大小
而今天要說的是上傳的斷點續傳,用到了Content-Range頭
上傳的續傳原理跟下載的續傳同理。
就是在上傳前把文件拆分后上傳。服務器端接收合並,即使上傳斷了。下次上傳依然從服務器端的文件現有字節后合並文件。最終上傳完成。
二 實現
服務器端
服務端是webapi實現。或是mvc,webform皆可。
服務端的原理就是接收上傳數據流。保存文件。如果此文件已存在。就是合並現有文件。
這里文件的文件名是采用客戶端傳過來的數據。
文件名稱是文件的MD5,保證文件的唯一性。
[HttpGet]
public HttpResponseMessage GetResumFile()
{
//用於獲取當前文件是否是續傳。和續傳的字節數開始點。
var md5str = HttpContext.Current.Request.QueryString["md5str"];
var saveFilePath = HttpContext.Current.Server.MapPath("~/Images/") + md5str;
if(System.IO.File.Exists(saveFilePath))
{
var fs = System.IO.File.OpenWrite(saveFilePath);
var fslength = fs.Length.ToString();
fs.Close();
return new HttpResponseMessage { Content = new StringContent(fslength, System.Text.Encoding.UTF8, "text/plain") };
}
return new HttpResponseMessage(HttpStatusCode.OK);
}
[HttpPost]
public HttpResponseMessage Rsume()
{
var file = HttpContext.Current.Request.InputStream;
var filename = HttpContext.Current.Request.QueryString["filename"];
this.SaveAs(HttpContext.Current.Server.MapPath("~/Images/") + filename, file);
HttpContext.Current.Response.StatusCode = 200;
// For compatibility with IE's "done" event we need to return a result as well as setting the context.response
return new HttpResponseMessage(HttpStatusCode.OK);
}
private void SaveAs(string saveFilePath,System.IO.Stream stream)
{
long lStartPos = 0;
int startPosition = 0;
int endPosition = 0;
var contentRange = HttpContext.Current.Request.Headers["Content-Range"];
//bytes 10000-19999/1157632
if (!string.IsNullOrEmpty(contentRange))
{
contentRange = contentRange.Replace("bytes", "").Trim();
contentRange = contentRange.Substring(0, contentRange.IndexOf("/"));
string[] ranges = contentRange.Split('-');
startPosition = int.Parse(ranges[0]);
endPosition = int.Parse(ranges[1]);
}
System.IO.FileStream fs;
if (System.IO.File.Exists(saveFilePath))
{
fs = System.IO.File.OpenWrite(saveFilePath);
lStartPos = fs.Length;
}
else
{
fs = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
lStartPos = 0;
}
if (lStartPos > endPosition)
{
fs.Close();
return;
}
else if (lStartPos < startPosition)
{
lStartPos = startPosition;
}
else if (lStartPos > startPosition && lStartPos < endPosition)
{
lStartPos = startPosition;
}
fs.Seek(lStartPos, System.IO.SeekOrigin.Current);
byte[] nbytes = new byte[512];
int nReadSize = 0;
nReadSize = stream.Read(nbytes, 0, 512);
while (nReadSize > 0)
{
fs.Write(nbytes, 0, nReadSize);
nReadSize = stream.Read(nbytes, 0, 512);
}
fs.Close();
}
客戶端
這里的客戶端是winform,功能就是選擇文件后即刻上傳。如果中途網絡,斷點等因素沒有傳成功。
可以再次選擇此文件上傳。服務器會合並之前傳送的文件字節。實現斷點續傳。
private void btnSelectFile_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = "c:\\";
openFileDialog.RestoreDirectory = true;
openFileDialog.FilterIndex = 1;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
var fName = openFileDialog.FileName;
FileStream fStream = new FileStream(fName, FileMode.Open, FileAccess.Read);
var mdfstr = GetStreamMd5(fStream);
fStream.Close();
var startpoint = isResume(mdfstr, Path.GetExtension(fName));
MessageBox.Show(UpLoadFile(fName, url, 64, startpoint,mdfstr));
}
}
/// <summary>
/// 根據文件名獲取是否是續傳和續傳的下次開始節點
/// </summary>
/// <param name="md5str"></param>
/// <param name="fileextname"></param>
/// <returns></returns>
private int isResume(string md5str, string fileextname)
{
System.Net.WebClient WebClientObj = new System.Net.WebClient();
var url = "http://localhost:13174/api/file/GetResumFile?md5str="+md5str+fileextname;
byte[] byRemoteInfo = WebClientObj.DownloadData(url);
string result = System.Text.Encoding.UTF8.GetString(byRemoteInfo);
if(string.IsNullOrEmpty(result))
{
return 0;
}
return Convert.ToInt32(result);
}
#region
/// <summary>
/// 上傳文件(自動分割)
/// </summary>
/// <param name="filePath">待上傳的文件全路徑名稱</param>
/// <param name="hostURL">服務器的地址</param>
/// <param name="byteCount">分割的字節大小</param>
/// <param name="cruuent">當前字節指針</param>
/// <returns>成功返回"";失敗則返回錯誤信息</returns>
public string UpLoadFile(string filePath, string hostURL, int byteCount, long cruuent, string mdfstr)
{
string tmpURL = hostURL;
byteCount = byteCount * 1024;
System.Net.WebClient WebClientObj = new System.Net.WebClient();
FileStream fStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
BinaryReader bReader = new BinaryReader(fStream);
long length = fStream.Length;
string sMsg = "上傳成功";
string fileName = filePath.Substring(filePath.LastIndexOf('\\') + 1);
try
{
#region 續傳處理
byte[] data;
if (cruuent > 0)
{
fStream.Seek(cruuent, SeekOrigin.Current);
}
#endregion
#region 分割文件上傳
for (; cruuent <= length; cruuent = cruuent + byteCount)
{
if (cruuent + byteCount > length)
{
data = new byte[Convert.ToInt64((length - cruuent))];
bReader.Read(data, 0, Convert.ToInt32((length - cruuent)));
}
else
{
data = new byte[byteCount];
bReader.Read(data, 0, byteCount);
}
try
{
//*** bytes 21010-47021/47022
WebClientObj.Headers.Remove(HttpRequestHeader.ContentRange);
WebClientObj.Headers.Add(HttpRequestHeader.ContentRange, "bytes " + cruuent + "-" + (cruuent + byteCount) + "/" + fStream.Length);
hostURL = tmpURL + "?filename=" + mdfstr + Path.GetExtension(fileName);
byte[] byRemoteInfo = WebClientObj.UploadData(hostURL, "POST", data);
string sRemoteInfo = System.Text.Encoding.Default.GetString(byRemoteInfo);
// 獲取返回信息
if (sRemoteInfo.Trim() != "")
{
sMsg = sRemoteInfo;
break;
}
}
catch (Exception ex)
{
sMsg = ex.ToString();
break;
}
#endregion
}
}
catch (Exception ex)
{
sMsg = sMsg + ex.ToString();
}
try
{
bReader.Close();
fStream.Close();
}
catch (Exception exMsg)
{
sMsg = exMsg.ToString();
}
GC.Collect();
return sMsg;
}
public static 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;
}
