对于打文件上传如果网突然断了,得重新来一遍,是不是很郁闷,怎么办,如果浏览器不支持FileAPI 那就只能使用 Flash来做断点上传,有了HTML5 基于Ajax或者WebSocket 的原生实现也很棒。
下面就是基于websocket + spring 的一个实现例子
好了直接上代码
<!--
文件上传例子
使用websocket上传文件
支持断点续传
-->
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- <link href="../../res/css/bootstrap.css" rel="stylesheet"/>
<script type="text/javascript" src="../../res/js/jquery-2.0.2.js"></script>
<script type="text/javascript" src="../../res/js/bootstrap.js"></script>-->
<link href="/bizplant/res/css/bootstrap.css" rel="stylesheet"/>
<script type="text/javascript" src="/bizplant/res/js/jquery-2.0.2.js"></script>
<!-- <script type="text/javascript" src="/bizplant/res/js/bootstrap.js"></script>-->
<script type="text/javascript">
ws = new WebSocket( "ws://localhost:8080/bizplant/document/receive.ws" );
$( document ).ready( function() {
$( "#disconnect" ).click( function() {
if ( ws != null ) {
ws.close();
ws = null;
}
} );
$( "#connect" ).click( function() {
if ( ws == null ) {
ws = new WebSocket( "ws://localhost:8080/bizplant/document/receive.ws" );
}
} );
$( "#continue" ).click( function() {
var id = $("#fileId").val();
if ( !ws ) {
ws = new WebSocket( "ws://localhost:8080/bizplant/document/receive.ws" );
}
$.post( "http://localhost:8080/bizplant/document/queryFileUploadState.json", { "id": id }, function( data ) {
if ( data ) {
alert( "请选择续传文件" );
$( "#uploadImage" ).change( function() {
$( '<div file=' + data.fileUploadState.file.name + ' class="progress progress-striped active"><div class="progress-bar" role="progressbar" aria-valuenow="'+data.fileUploadState.loadedSize+'" aria-valuemin="0" aria-valuemax="100" style="width: '+data.fileUploadState.loadedSize+'%"></div><div class="sr-only"><span class="file fileName" fileId="' + data.fileUploadState.file.id + '"> ' + data.fileUploadState.file.name + ' </span> <span>已经上传【</span><span class="fileLoadSize">'+data.fileUploadState.loadedSize+'</span><span>%】</span></div></div>' ).insertAfter( this );
ws.send( JSON.stringify( data.fileUploadState.file ) );
var files = $( this ).get( 0 ).files;
var size = files[0].size;
var total = data.fileUploadState.loadedSize;
var unit = 8192;
var buffer = null;
log( "Info:" + data.fileUploadState.file.name + " 开始发送文件" );
while ( total < size ) {
if ( total + unit < size ) {
buffer = files[0].slice( total, total + unit );
total = total + unit;
} else {
buffer = files[0].slice( total, size );
total = size;
}
ws.send( buffer );
}
} );
}
}, "json" );
} );
$( 'input[type="file"]' ).not("#uploadImage").change( function() {
var files = $( this ).get( 0 ).files;
var fileCount = files.length;
$( this ).hide();
$( this ).siblings( "input" ).hide();
for ( var i = 0; i < fileCount; i++ ) {
var file = { };
file.size = files[i].size;
file.name = files[i].name;
file.fileType = { };
file.fileType.extension = files[i].name.substring( files[i].name.lastIndexOf( "." ) + 1 );
file.fileType.mimeType = files[i].type;
$( '<div file=' + file.name + ' class="progress progress-striped active"><div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div><div class="sr-only"><span class="file fileName" fileId="' + file.id + '"> ' + file.name + ' </span> <span>已经上传【</span><span class="fileLoadSize"></span><span>%】</span></div></div>' ).insertAfter( this );
ws.send( JSON.stringify( file ) );
var size = files[i].size;
var total = 0;
var unit = 8192;
var buffer = null;
log( "Info:" + i + files[i].name + " 开始发送文件" );
while ( total < size ) {
if ( total + unit < size ) {
buffer = files[i].slice( total, total + unit );
total = total + unit;
} else {
buffer = files[i].slice( total, size );
total = size;
}
ws.send( buffer );
}
}
} );
ws.onopen = function() {
log( 'Info: connection opened.' );
};
ws.onmessage = function( event ) {
var loader = JSON.parse( event.data );
if ( parseInt( loader.load ) !== parseInt( loader.total ) ) {
$( 'div[file="' + loader.name + '"] .progress-bar' ).attr( "aria-valuenow", loader.percent ).css( "width", loader.percent + "%" );
$( 'div[file="' + loader.name + '"] .fileLoadSize' ).text( loader.percent );
} else {
$( 'div[file="' + loader.name + '"] .progress-bar' ).attr( "aria-valuenow", "100" ).css( "width", "100%" );
$( 'div[file="' + loader.name + '"] .fileLoadSize' ).text( "100" );
$( 'div[file="' + loader.name + '"]' ).removeClass( "active" );
}
log( 'Received: ' + event.data );
};
ws.onclose = function( event ) {
alert(event.reason);
var status = { "type": event.type, "code": event.code, "reason": event.reason, "time": new Date( event.timeStamp ) };
log( 'Info: connection closed.' );
log( JSON.stringify( status ) );
};
function log( message ) {
// var console = document.getElementById( 'console' );
// var p = document.createElement( 'p' );
// p.style.wordWrap = 'break-word';
// p.appendChild( document.createTextNode( message ) );
// console.appendChild( p );
// while ( console.childNodes.length > 10 ) {
// console.removeChild( console.firstChild );
// }
// console.scrollTop = console.scrollHeight;
}
} );
</script>
</head>
<body>
<button type="button" id="disconnect">断开</button>
<button type="button" id="connect">连接</button>
<button type="button" id="continue">续传</button>
<input type="text" id="fileId"/>
<input type="file" style="opacity: 100" id="uploadImage" name="file"/>
<div class="container">
<h2 id="forms-horizontal">用户注册</h2>
<form class="form-horizontal">
<div class="form-group">
<label for="account" class="col-lg-2 control-label">账号:</label>
<div class="col-lg-10 input string">
<input class="form-control" id="account" placeholder="账号" type="text">
</div>
</div>
<div class="form-group">
<label for="pswd" class="col-lg-2 control-label">密码</label>
<div class="col-lg-10 input string">
<input class="form-control" id="pswd" placeholder="密码" type="password">
</div>
</div>
<div class="form-group">
<label for="repswd" class="col-lg-2 control-label">再输密码</label>
<div class="col-lg-10">
<input class="form-control" id="repswd" placeholder="再输密码" type="password">
</div>
</div>
<div class="form-group">
<label for="name" class="col-lg-2 control-label">姓名</label>
<div class="col-lg-10">
<input class="form-control" id="name" placeholder="姓名" type="text">
</div>
</div>
<div class="form-group">
<label for="photo" class="col-lg-2 control-label">照片</label>
<div class="col-lg-10">
<input class="form-control" type="file">
<input class="form-control" id="photo" placeholder="照片" type="text">
</div>
</div>
<div class="form-group">
<label for="lifePhoto" class="col-lg-2 control-label">生活照片</label>
<div class="col-lg-10">
<input class="form-control" multiple = "multiple" type="file">
<input class="form-control" id="lifePhoto" placeholder="生活照片" type="text">
</div>
</div>
<div class="form-group">
<label for="age" class="col-lg-2 control-label">年龄</label>
<div class="col-lg-10">
<input class="form-control" id="lifePhoto" placeholder="年龄" type="number">
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<div class="checkbox">
<label>
<input type="checkbox"> Remember me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
</div>
<div class="input file">
<img id="uploadPreview" style="width: 100px; height: 100px;" src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3Csvg%20width%3D%22153%22%20height%3D%22153%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%0A%20%3Cg%3E%0A%20%20%3Ctitle%3ENo%20image%3C/title%3E%0A%20%20%3Crect%20id%3D%22externRect%22%20height%3D%22150%22%20width%3D%22150%22%20y%3D%221.5%22%20x%3D%221.500024%22%20stroke-width%3D%223%22%20stroke%3D%22%23666666%22%20fill%3D%22%23e1e1e1%22/%3E%0A%20%20%3Ctext%20transform%3D%22matrix%286.66667%2C%200%2C%200%2C%206.66667%2C%20-960.5%2C%20-1099.33%29%22%20xml%3Aspace%3D%22preserve%22%20text-anchor%3D%22middle%22%20font-family%3D%22Fantasy%22%20font-size%3D%2214%22%20id%3D%22questionMark%22%20y%3D%22181.249569%22%20x%3D%22155.549819%22%20stroke-width%3D%220%22%20stroke%3D%22%23666666%22%20fill%3D%22%23000000%22%3E%3F%3C/text%3E%0A%20%3C/g%3E%0A%3C/svg%3E" alt="Image preview" />
<button type="button">上传</button>
</div>
<div id="console-container">
<div id="console"></div>
</div>
</body>
</html>
spring 配置
<websocket:handlers>
<websocket:mapping path="/document/receive.ws" handler="fileUploadSocketHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean name="fileUploadSocketHandler" class="com.chameleon.document.service.FileUploadSocketHandler"/>
<bean class="org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
FileUploadSocketHandler
import com.chameleon.document.DocumentBusiness;
import com.chameleon.document.model.File;
import com.chameleon.document.model.FileDiskContent;
import com.chameleon.document.model.FileUploadState;
import com.chameleon.util.Sequence;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
/**
*
* @author Johnson.zhang
*/
public class FileUploadSocketHandler extends AbstractWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(FileUploadSocketHandler.class);
private Map result = null;
@Resource(name = "documentBusinessImpl")
private DocumentBusiness documentBusiness;
private ObjectMapper objectMapper = new ObjectMapper();
private OutputStream outputStream;
private long fileTotalSize;
private long fileLoadSize;
private DecimalFormat percentFormat = new DecimalFormat("0");
private FileDiskContent fileDiskContent = null;
@Resource(name = "systemFileUploadPath")
private String uploadFilePath;
@Resource(name = "sequenceSerialNumberImpl")
private Sequence sequence;
public void setUploadFilePath(String uploadFilePath) {
this.uploadFilePath = uploadFilePath;
}
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void setSequence(Sequence sequence) {
this.sequence = sequence;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String id = session.getId();
logger.debug("WebSocket连接已经建立,连接ID为:" + id);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
result = new HashMap();
result.put("buffer", 8192);
fileTotalSize = 0l;
fileLoadSize = 0l;
String msg = message.getPayload();
fileDiskContent = objectMapper.readValue(msg, FileDiskContent.class);
if (fileDiskContent.getId() != null && !fileDiskContent.getId().isEmpty()) {
//续传文件
CloseStatus status = null;
String filePath = fileDiskContent.getPath();
if (filePath == null || filePath.isEmpty()) {
status = new CloseStatus(2001, "续传文件路径不能为空!");
}
java.io.File tempFile = new java.io.File(uploadFilePath + "/" + filePath);
if (!tempFile.exists()) {
status = new CloseStatus(2002, "续传文件不存在!");
}
if (!tempFile.isFile()) {
status = new CloseStatus(2002, "续传文件不能是文件夹!");
}
if (status != null) {
session.close(status);
String rs = objectMapper.writeValueAsString(status);
logger.debug("WebSocket 文件断点续传初始化发生错误: " + rs);
return;
}
outputStream = new FileOutputStream(uploadFilePath + "/" + filePath, true);
fileTotalSize = fileDiskContent.getSize();
fileLoadSize = tempFile.length();
if (fileTotalSize > 0) {
double percent = ((double) fileLoadSize) / fileTotalSize * 100;
result.put("percent", percentFormat.format(percent));
}
result.put("id", fileDiskContent.getId());
result.put("name", fileDiskContent.getName());
result.put("total", fileDiskContent.getSize());
result.put("path", filePath);
result.put("load", fileLoadSize);
logger.debug("WebSocket 文件上传信息为: " + msg);
} else {
//新上传文件
String filePath = sequence.getId();
String tempPath = uploadFilePath + "/" + filePath;
outputStream = new FileOutputStream(tempPath);
fileDiskContent.setPath(filePath);
fileDiskContent.setId(sequence.getId());
fileTotalSize = fileDiskContent.getSize();
result.put("id", fileDiskContent.getId());
result.put("name", fileDiskContent.getName());
result.put("total", fileDiskContent.getSize());
result.put("path", filePath);
result.put("load", fileLoadSize);
result.put("percent", "0");
logger.debug("WebSocket 文件上传信息为: " + msg);
logger.debug("WebSocket 文件保存路径为: " + tempPath);
}
String rs = objectMapper.writeValueAsString(result);
session.sendMessage(new TextMessage(rs));
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
ByteBuffer msg = message.getPayload();
byte[] buffer = msg.array();
outputStream.write(buffer);
fileLoadSize = fileLoadSize + buffer.length;
logger.debug("WebSocket 文件已经上传了 [ " + fileLoadSize + " ] 字节");
result.put("load", fileLoadSize);
if (fileTotalSize > 0) {
double percent = ((double) fileLoadSize) / fileTotalSize * 100;
result.put("percent", percentFormat.format(percent));
}
if (fileLoadSize == fileTotalSize) {
closeStream();
if (fileDiskContent != null) {
if (fileDiskContent.getMeta() == null) {
documentBusiness.saveFile(fileDiskContent);
} else {
documentBusiness.deleteFileUploadStateByFile(fileDiskContent);
}
}
logger.debug("WebSocket 文件已经保存到磁盘:【" + uploadFilePath + "/" + fileDiskContent.getPath() + "】");
logger.debug("WebSocket 文件信息已经保存到数据库 文件ID为:【" + fileDiskContent.getId() + "】");
}
String rs = objectMapper.writeValueAsString(result);
logger.debug("WebSocket 文件上传回显数据位 :【" + rs + "】");
session.sendMessage(new TextMessage(rs));
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
String rs = objectMapper.writeValueAsString(exception);
if (session.isOpen()) {
session.sendMessage(new TextMessage(rs));
}
logger.debug("WebSocket 程序发生异常 :【" + rs + "】");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
closeStream();
saveFileUploadState();
String rs = objectMapper.writeValueAsString(status);
logger.debug("WebSocket 连接关闭 :【" + rs + "】");
}
private void closeStream() throws Exception {
if (outputStream != null) {
outputStream.close();
outputStream = null;
}
}
private void saveFileUploadState() {
if (fileLoadSize < fileTotalSize) {
if (fileDiskContent.getMeta() == null) {
FileUploadState fileUploadState = new FileUploadState();
fileUploadState.setId(sequence.getId());
fileUploadState.setLoadedSize(fileLoadSize);
fileUploadState.setFile(fileDiskContent);
documentBusiness.saveFileUploadState(fileUploadState);
} else {
documentBusiness.updateFileUploadStateByFile(fileDiskContent, fileLoadSize);
}
}
}
}