场景:最近由于公司项目中需要使用到断点续传这一功能,并且需要将文件和服务器的文件进行,只好自己去学习和了解一些有关断点续传的知识,其中,主要需要学习的是HTTP相关的知识,比如Header,其实这里面的Range属性就是用来实现断点下载的,下面直接贴代码:
其中服务器端的代码主要是使用ASP.NET MVC写的,如下所示:
/// <summary>
/// 下载的方法
/// </summary>
/// <param name="filename">下载文件的路径</param>
public ActionResult Reget(string filename)
{
long start = 0L, end = 0L;
Stream iStream = null;
var result = GetMD5(filename, out string message);
// 分块读取,每块512 bytes
byte[] buffer = new Byte[512];
//文件的大小
int length;
// 文件的字节数
long dataToRead;
// 得到服务器上面的文件
var path = Server.MapPath("/Files/Files/");
string filepath = path + filename;
try
{
Response.Clear();
Response.ClearHeaders();
//打开文件
iStream = new FileStream(filepath, FileMode.Open,
FileAccess.Read, FileShare.Read);
// 总字节
dataToRead = iStream.Length;
if (Request.Headers["Range"] != null)
{
//如果是续传请求,则获取续传的起始位置,即已经下载到客户端的字节数
Response.StatusCode = 206;
//重要:续传必须,表示局部范围响应。
var range = Request.Headers["Range"].ToString();
var startRange = range.Substring(6);
start = Convert.ToInt64(startRange.Substring(0, startRange.LastIndexOf("-")));
end = Convert.ToInt64(range.Substring(startIndex: range.LastIndexOf("-") + 1));
}
//不是从最开始下载,
////响应的格式是:Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
Response.AddHeader("Range", "bytes=" + start + "-" + end);
Response.AddHeader("Connection", "Keep-Alive");
Response.ContentType = "application/octet-stream"; //MIME类型:匹配任意文件类型
Response.ContentType = "application/octet-stream";
Response.AddHeader("Content-Disposition", "attachment; filename=" + HttpUtility.UrlEncode(filename));
Response.AddHeader("Content-Length", string.Format("{0}", iStream.Length));
Response.AddHeader("Content-MD5", message);//用于验证文件
iStream.Position = start;
dataToRead = end - start;
// 读字节
while (dataToRead > 0)
{
//确认客户端连接
if (Response.IsClientConnected)
{
// Read the data in buffer.
length = iStream.Read(buffer, 0, (int)dataToRead);
// Write the data to the current output stream.
Response.OutputStream.Write(buffer, 0, length);
// Flush the data to the HTML output.
Response.Flush();
buffer = new Byte[length];
dataToRead = dataToRead - length;
}
else
{
//prevent infinite loop if user disconnects
dataToRead = -1;
}
}
}
catch (Exception ex)
{
// Trap the error, if any.
return Content("Error : " + ex.Message);
}
finally
{
if (iStream != null)
{
//关闭流
iStream.Close();
}
Response.Close();
}
return new EmptyResult();
}
接下来就展示客户端的代码,并且对文件进行MD5校验:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DownloadTaskDemo
{
class Program
{
static void Main(string[] args)
{
//1、查看文件是否已下载过和配置文件是否已生成
//2、已下载或已生成文件,直接读取它们
//3、否则,从零开始下载,并生成配置文件
//4、通过对请求头的Range属性进行配置来获取文件某一段内容,服务器并返回内容,同时将已下载的部分记录在配置文件中
//5、判断文件是否下载完成,下载完成就根据服务器返回的MD5进行校验,并记录到配置文件
long start = 0L, end = 0L;
string fn = "";
long contentLength = 0L;
string md5 = "";
var isComplate = false;
string StrFileName = @"d:\my.tmp";
string StrUrl = "http://localhost:56159/Upload/Reget?filename=f512b5fa8cab41b9e8940107ca657862.jar";
// StrUrl = "http://localhost:56159/Upload/Reget?filename=Paper20180412.docx";
//打开上次下载的文件或新建文件
long lStartPos = 0;
FileStream fs;
if (File.Exists(StrFileName))
{
fs = File.OpenWrite(StrFileName);
lStartPos = fs.Length;
fs.Seek(lStartPos, SeekOrigin.Current); //移动文件流中的当前指针
}
else
{
fs = new FileStream(StrFileName, FileMode.Create);
lStartPos = 0;
}
//打开网络连接
try
{
System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(StrUrl);
request.AddRange(lStartPos, lStartPos + 512);
Stream ns;
//向服务器请求,获得服务器回应数据流
int i = 1;
while (!isComplate)
{
i++;
if (start != 0 && end != 0 && contentLength != 0)
{
request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(StrUrl);
//矫正range
if (end > contentLength)
{
request.AddRange(start, contentLength);
}
else
{
request.AddRange(start, end);
}
}
var response = request.GetResponse();
if (response.Headers["Content-Length"] != null)
{
contentLength = Convert.ToInt64(response.Headers["Content-Length"]);
}
md5 = response.Headers["Content-MD5"];
Console.WriteLine(md5);
if (response.Headers["Range"] != null)
{
var range = response.Headers["Range"].ToString();
var startRange = range.Substring(6);
start = Convert.ToInt64(startRange.Substring(0, startRange.LastIndexOf("-")));
end = Convert.ToInt64(range.Substring(startIndex: range.LastIndexOf("-") + 1));
Console.WriteLine(start + "-" + end);
}
var cd = response.Headers["Content-Disposition"];
fn = cd.Substring(cd.LastIndexOf("=") + 1);
ns = response.GetResponseStream();
byte[] nbytes = new byte[512];
int nReadSize = 0;
nReadSize = ns.Read(nbytes, 0, 512);
while (nReadSize > 0)
{
//同时向同一个记录里面取数据 并将数据从相同位置写
fs.Write(nbytes, 0, nReadSize);
nReadSize = ns.Read(nbytes, 0, 512);
}
//下载完成就退出
if (end == contentLength)
{
isComplate = true;
break;
}
start = end;
end = end + 512;
//获取剩余512字节的数据
ns.Close();
}
fs.Close();
var valMd5 = GetMD5(filePath: StrFileName);
Console.WriteLine(valMd5);
if (md5.Equals(valMd5))
{
Console.WriteLine("校验成功");
}
else
{
Console.WriteLine("校验失败");
}
File.Move(StrFileName, @"d:\" + fn);
Console.WriteLine("下载完成");
}
catch (Exception ex)
{
fs.Close();
Console.WriteLine("下载过程中出现错误:" + ex.Message);
}
Console.Read();
}
/// <summary>
/// MD5校验
/// </summary>
/// <param name="fileName"></param>
/// <param name="message"></param>
/// <returns></returns>
public static string GetMD5(string filePath)
{
string path = filePath;
string message = "";
try
{
FileStream file = new FileStream(path, System.IO.FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
string result = BitConverter.ToString(retVal);
message = result.Replace("-", "");
return message;
}
catch (Exception ex)
{
message = "错误信息" + ex.Message;
return message;
}
}
}
}
在这里,主要涉及的知识有HTTP的相关知识可以参考:https://en.wikipedia.org/wiki/List_of_HTTP_header_fields