由于集团业务需要,需要将大文件,通常是几个G的文件通过浏览器上传到平台里,但目前上大多数的网站的断点上传都是需要安装浏览器插件,例如需要安装FLASH、ActiveX控件、Java Applet插件。有时候服务器端也得自己实现,这样实现一个文件上传功能变得非常麻烦。由于当今的HTML5技术非常成熟了,通过HTML5 File api实现断点是一件非常容易的事情了,因此应该坚决采用HTML5的API来实现文件上传,笔者认为绝对不要在使用安装插件的方式了,安装插件有如下弊端:
- 不能跨浏览器支持,例如ActiveX控件就只能用于IE
- 新的浏览器已经都不支持Flash、java Applet了
- 不能跨平台,例如Windows平台能用,iOS与Android平台就不一定能用
总之,用插件上传文件的方式已经过期了,放着HTML5 File api这么好的API不用,这不是暴殄天物呀,笔者经过短暂的资料查找后,发现只要自己实现一个HTTP服务器,即可以借助HTML5 File api本身的强大功能来实现大文件断点续传功能,借助于强大的HTML5 File api,结合笔者自己实现了一个HTTP上传服务器,实现的HTTP文件上传系统具备如下特色:
- 支持超大文件HTTP上传,文件大小不受限制
- HTTP上传断点续传支持
- 支持HTML5浏览器上传进度显示
- 无需浏览器端安装任何插件
- 支持IE8及以下浏览器上传进度显示
- 支持1000个实时并发连接
- 无缓冲即时写入
- 服务器端极少的CPU、内存占用
- 支持查看客户在线连接 用 http://ip:port/lists
- Compatible with Chrome, FireFox, Safari, IE,Opera and Edge
以下记录如何在浏览器端实现支持大文件断点续传,作为以后的资料备查。
一、制作一个简单的网页,并在网页里添加file字段,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>H5大文件断点续传,失败重连演示</title>
<style>
.container {
width: 640px;
margin: 100px auto 0;
box-sizing: border-box;
border: 1px solid #ccc;
padding: 10px 10px;
border-radius: 2px;
box-shadow: 1px 1px 50px rgba(0,0,0,.3);
}
.fileview {
border:3px dashed silver;
width:100%;
height:360px;
box-sizing: border-box;
text-align: center;
position: relative;
overflow: hidden;
}
.fileview .tiptext {
display: block;
margin-top: 160px;
}
.fileview input[type=file] {
position: absolute;
top: 0;
right: 0;
margin: 0;
opacity: 0;
-ms-filter: 'alpha(opacity=0)';
font-size: 300px !important;
direction: ltr;
cursor: pointer;
display: block;
}
.progressbar {
margin: 5px 0;
overflow: hidden;
position: relative;
padding-right: 40px;
}
.progressbar .control {
display:block;
float:left;
height: 20px;
width: 100%;
}
.progressbar .text {
display: block;
font-size: 14px;
line-height: 20px;
width: 40px;
text-align: center;
position: absolute;
right: 0;
top: 0;
}
.btn-upload {
padding: 3px 20px;
border: 1px solid #aaa;
height: 28px;
line-height: 20px;
border-radius: 2px;
background-color: #f8f8f8;
}
.controlbar {
overflow: hidden;
position:relative;
}
.controlbar button {
float:left;
margin-right: 10px;
}
.controlbar .upload-status {
float:right;
width:400px;
}
.controlbar .upload-status .item{
width:200px;
float: left;
}
.file-object {
border:1px solid #f0f0f0;
border-radus:2px;
padding:5px 6px;
margin-top:5px;
margin-bottom:5px;
}
.file-object .info-row {
overflow: hidden;
}
.file-object .info-row span {
font-size:14px;
display:inline-block;
}
.file-object .info-row span.rlabel {
width:100px;
float: left;
}
.file-object .info-row .rvalue {
max-width: 70%;
display: block;
color: #004302;
float: left;
}
.toast {
position: absolute;
width: 380px;
height: 26px;
line-height:26px;
padding-left:10px;
border: 1px solid #932d0d;
left: 160px;
top: 0px;
z-index: 1000;
background-color: #e39113;
color: #fff;
}
</style>
<script src="js/md5.min.js"></script>
<script src="js/updsrvaddr.js"></script>
</head>
<body>
<div class="container">
<div id="fileview" class="fileview">
<span class="tiptext">点击选择文件或者将文件拖放到此处。</span>
<input id="file-upload" type="file" name="file">
</div>
<div class="progressbar">
<progress value="0" max="100" id="progressbar" class="control"></progress>
<span id="percent-label" class="text">0%</span>
</div>
<div class="controlbar">
<button onclick="doupload();" id="uploadbtn" class="btn-upload">上传</button>
<button onclick="upload_clear();" id="upload_clear" class="btn-upload">清空</button>
<div class="upload-status">
<div class="item"><span class="title">已经上传:</span><span class="value" id="finish">0</span></div>
<div class="item"><span class="title">上传位率:</span><span class="value" id="bitrate">0</span></div>
</div>
<div class="toast" id="toast" style="display:none;">
</div>
</div>
<div class="upload-result" id="upload-result">
</div>
</div>
</body>
</html>
二、计算文件的HASH值
在上传文件之前,先得获得一个文件ID,这个ID是由浏览器端提供,而不是由服务器端提供,这样便于扩充,以后真要实现HTML5秒传文件的时候,改一下浏览器端的代码即可。
计算文件HASH值的思路就是:MD5(文件名称+文件长度+文件修改时间+自定义的浏览器ID)
javascript代码如下。
//简单的Cookie帮助函数 function setCookie(cname,cvalue,exdays) { var d = new Date(); d.setTime(d.getTime()+(exdays*24*60*60*1000)); var expires = "expires="+d.toGMTString(); document.cookie = cname + "=" + cvalue + "; " + expires; } function getCookie(cname) { var name = cname + "="; var ca = document.cookie.split(';'); for(var i=0; i<ca.length; i++) { var c = ca[i].trim(); if (c.indexOf(name)==0) return c.substring(name.length,c.length); } return ""; } // //简单的文件HASH值计算,如果您不是十分考究,应该可以用于产品。 //由于计算文件HASH值用到了多种数据,因此在HYFileUploader系统范围内发生HASH冲突的可能性应该非常小,应该可以放心使用。 //获取文件的ID可以用任何算法来实现,只要保证做到同一文件的ID是相同的即可,获取的ID长度不要超过32字节 // function getFileId (file) { //给浏览器授予一个唯一的ID用于区分不同的浏览器实例(不同机器或者相同机器不同厂家的浏览器) var clientid = getCookie("HUAYIUPLOAD"); if (clientid == "") { //用一个随机值来做浏览器的ID,将作为文件HASH值的一部分 var rand = parseInt(Math.random() * 1000); var t = (new Date()).getTime(); clientid =rand+'T'+t; setCookie("HUAYIUPLOAD",clientid,365); } var info = clientid; if (file.lastModified) info += file.lastModified; if (file.name) info += file.name; if (file.size) info += file.size; //https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.min.js var fileid = md5(info); return fileid; }
三、向服务器端查询文件断点续传信息
在开始上传之前,首先从服务器端查询文件的断点续传信息,以便决定从文件的什么位置读取要上传的数据,代码如下:
function doupload(bReconnect) { if (!currentfile) { alert("请选择文件后再试!") return false; } if (upload_start) { alert("文件上传正在进行中,请稍候再点击重复长传!") return false; } var fileObj = currentfile; var fileid = getFileId(fileObj); var t = (new Date()).getTime(); //通过以下URL获取文件的断点续传信息,必须的参数为fileid,后面追加t参数是避免浏览器缓存 var url = resume_info_url + '?fileid='+fileid + '&t='+t; var ajax = new XMLHttpRequest(); ajax.onerror = function (e) { if (window.reconnectId) return; window.reconnectId = window.setTimeout(function() { doupload(true); },2000); }; ajax.onreadystatechange = function () { if(this.readyState == 4){ if (bReconnect) { //目前是重连状态,清除重连标志 window.reconnectId = 0; } if (this.status == 200){ var response = this.responseText; var result = JSON.parse(response); if (!result) { alert('服务器返回的数据不正确,可能是不兼容的服务器'); return; } //断点续传信息返回的文件对象包含已经上传的尺寸 var uploadedBytes = result.file && result.file.size; if (!result.file.finished && uploadedBytes < fileObj.size) { upload_file(fileObj,uploadedBytes,fileid); } else { //文件已经上传完成了,就不要再上传了,直接返回结果就可以了 showUploadedFile(result.file); //模拟进度完成 //var progressBar = document.getElementById('progressbar'); //progressBar.value = 100; } }else { toast('获取文件断点续传信息失败,正在尝试重连...'); } } } ajax.open('get',url,true); ajax.send(null); currentRequest = ajax; }
四、执行文件分段上传
如果文件已经上传过一部分了,则从文件上传长度的位置上传。以下是Javascript代码实现,关键技术点是:
var blob = fileObj.slice(start_offset,filesize);
以上代码行表示从start_offset偏移位置读取到filesize为结尾位置的数据,放心吧,浏览是不会真正把这些数据一次预读到内存中去的,以上仅仅表示通知浏览器对文件对象的start_offse开始到filesize结尾的数据感兴趣。
以下是文件断点续传的代码,应该非常简单。
/* 文件上传处理代码 fileObj : html5 File 对象 start_offset: 上传的数据相对于文件头的起始位置 fileid: 文件的ID,这个是上面的getFileId 函数获取的, */ function upload_file(fileObj,start_offset,fileid) { var xhr = new XMLHttpRequest(); var formData = new FormData(); var blobfile; if(start_offset >= fileObj.size){ return false; } var bitrateDiv = document.getElementById("bitrate"); var finishDiv = document.getElementById("finish"); var progressBar = document.getElementById('progressbar'); var progressDiv = document.getElementById('percent-label'); var oldTimestamp = 0; var oldLoadsize = 0; var totalFilesize = fileObj.size; if (totalFilesize == 0) return; var uploadProgress = function (evt) { if (evt.lengthComputable) { var uploadedSize = evt.loaded + start_offset; var percentComplete = Math.round(uploadedSize * 100 / totalFilesize); var timestamp = (new Date()).valueOf(); var isFinish = evt.loaded == evt.total; if (timestamp > oldTimestamp || isFinish) { var duration = timestamp - oldTimestamp; if (duration > 500 || isFinish) { var size = evt.loaded - oldLoadsize; var bitrate = (size * 8 / duration /1024) * 1000; //kbps if (bitrate > 1000) bitrate = Math.round(bitrate / 1000) + 'Mbps'; else bitrate = Math.round(bitrate) + 'Kbps'; var finish = evt.loaded + start_offset; if (finish > 1048576) finish = (Math.round(finish / (1048576/100)) / 100).toString() + 'MB'; else finish = (Math.round(finish / (1024/100) ) / 100).toString() + 'KB'; progressBar.value = percentComplete; progressDiv.innerHTML = percentComplete.toString() + '%'; bitrateDiv.innerHTML = bitrate; finishDiv.innerHTML = finish; oldTimestamp = timestamp; oldLoadsize = evt.loaded; } } } else { progressDiv.innerHTML = 'N/A'; } } xhr.onreadystatechange = function(){ if ( xhr.readyState == 4 && xhr.status == 200 ) { console.log( xhr.responseText ); } else if (xhr.status == 400) { } }; var uploadComplete = function (evt) { progressDiv.innerHTML = '100%'; currentfile = null; upload_start = false; var result = JSON.parse(evt.target.responseText); if (result.result == 'success') { showUploadedFile(result.files[0]); } else { alert(result.msg); } } var uploadFailed = function (evt) { if (!currentfile) return; if (window.reconnectId) return; upload_start = false; toast("检测到网络故障!两秒后尝试重连..."); window.reconnectId = window.setTimeout(function() { doupload(true); },2000); } var uploadCanceled = function (evt) { alert("上传被取消或者浏览器断开了连接!"); } //设置超时时间,由于是上传大文件,因此千万不要设置超时 //xhr.timeout = 20000; //xhr.ontimeout = function(event){ // alert('文件上传时间太长,服务器在规定的时间内没有响应!'); //} xhr.overrideMimeType("application/octet-stream"); var filesize = fileObj.size; var blob = fileObj.slice(start_offset,filesize); var fileOfBlob = new File([blob], fileObj.name); //附加的文件数据应该放在请求的前面 formData.append('filename', fileObj.name); //必须将fileid信息传送给服务器,服务器只有在获得了fileid信息后才对文件做断点续传处理 formData.append('fileid', fileid); //请将文件数据放在最后的域 //formData.append("file",blob, fileObj.name); formData.append('file', fileOfBlob); xhr.upload.addEventListener("progress", uploadProgress, false); xhr.addEventListener("load", uploadComplete, false); xhr.addEventListener("error", uploadFailed, false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open('POST', upload_file_url); // xhr.send(formData); currentRequest = xhr; upload_start = true; toast(''); }
五、断点续传实现
通过监视XMLHttpRequest的error时间来判断文件上传是否报错,如果发生错误,重新上传即可。
这样在客户端掉线,宕机,或者服务器停机等各种情况下,均可实现点断续传。不用担心大文件上传到一半,突然故障发生又得重新传送。
var uploadFailed = function (evt) {
if (!currentfile) return;
if (window.reconnectId) return;
upload_start = false;
toast("检测到网络故障!两秒后尝试重连...");
window.reconnectId = window.setTimeout(function() {
doupload(true);
},2000);
}
xhr.addEventListener("error", uploadFailed, false);
为了用户体验好一点,以上代码对上传进度、上传速率进行了计算,当然还可以加入更多的显示,例如剩余时间。
只要在服务器端正确处理浏览器的POST数据,实现HTTP文件上传断点续传确实不难,笔者不得不为当今浏览器的强大功能所折服。
所有的HTML5断点续传代码已经上传到
https://github.com/wenshui2008/HYUploadServer
位于 html/demos/h5resume.html 文件里。