首先,我們清晰一下思路
1)先實現文件上傳,我們應該清楚,文件上傳和進度走的是兩條路線,即異步;
2)再使用文件上傳解析器去獲取文件進度信息,這個信息是保存在一個session域裏的,會被實時刷新;
3)websocket定時遍歷,實現點對點發送上傳進度信息;
很簡單,就這三步
接下來開始實現
所需的maven依賴
<!--文件上傳-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<!-- webscoket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
實現第一步和第二步
後臺主要有下面幾個類
1.FileuploadClass 文件上傳配置信息,繼承了CommonsMultipartResolver
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* @ClassName FileUpload
* @Description TODO
* @Author fzj
* @Date 2020-3-26 13:21
* @Version 1.0.0
**/
public class FileUploadClass extends CommonsMultipartResolver {
private UploadProgressListener progressListener = new UploadProgressListener();
public void setFileUploadProgressListener(
UploadProgressListener progressListener) {
this.progressListener = progressListener;
}
public MultipartParsingResult parseRequest(HttpServletRequest request)
throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
progressListener.setSession(request.getSession());
fileUpload.setProgressListener(progressListener);
try {
@SuppressWarnings("unchecked")
List<FileItem> fileItems = ((ServletFileUpload) fileUpload)
.parseRequest(request);
return parseFileItems(fileItems, encoding);
} catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(),
ex);
} catch (FileUploadException ex) {
throw new MultipartException(
"Could not parse multipart servlet request", ex);
}
}
}
2.FileUploadConfig 配置類 用於springboot項目加載時注入文件上傳配置文件的bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @ClassName FileUPDConfig
* @Description TODO
* @Author fzj
* @Date 2020-4-1 8:19
* @Version 1.0.0
**/
@Configuration
public class FileUploadConfig {
@Bean
public FileUploadClass fileUploadConfig(){
return new FileUploadClass();
}
}
3.UploadProgressListener 文件上傳狀態監聽器 實現了ProgressListener
import com.fzj.model.ProgressEntity;
import org.apache.commons.fileupload.ProgressListener;
import javax.servlet.http.HttpSession;
/**
* @ClassName UploadProgressListener
* @Description TODO
* @Author fzj
* @Date 2020-4-1 8:15
* @Version 1.0.0
**/
public class UploadProgressListener implements ProgressListener {
private HttpSession session;
public void setSession(HttpSession session) {
this.session = session;
ProgressEntity status = new ProgressEntity();// 保存上傳狀態
session.setAttribute("status", status);
}
@Override
public void update(long bytesRead, long contentLength, int items) {
ProgressEntity status = (ProgressEntity) session.getAttribute("status");
status.setBytesRead(bytesRead);// 已讀取數據長度
status.setContentLength(contentLength);// 文件總長度
status.setItems(items);// 正在保存第幾個文件
}
}
4.ProgressEntity 進度實體類
/**
* @ClassName ProgressEntity
* @Description TODO
* @Author fzj
* @Date 2020-3-26 12:56
* @Version 1.0.0
**/
public class ProgressEntity {
private long bytesRead;
private long contentLength;
private long items;
private long startTime = System.currentTimeMillis(); // 開始上傳時間,用於計算上傳速率
public ProgressEntity() {
}
public long getBytesRead() {
return bytesRead;
}
public void setBytesRead(long bytesRead) {
this.bytesRead = bytesRead;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
public long getItems() {
return items;
}
public void setItems(long items) {
this.items = items;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
}
5.FileUploadCommonTool 文件上傳通用操作類
import com.fzj.model.ProgressEntity;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName FileUploadCommonTool
* @Description TODO
* @Author fzj
* @Date 2020-4-9 10:05
* @Version 1.0.0
**/
public class FileUploadCommonTool {
public static Map<String, Object> upload(MultipartFile Fdata, String Sid, HttpServletRequest request) {
String infilename = Fdata.getOriginalFilename();
String endstring = infilename.substring(infilename.lastIndexOf("."));
//這裏寫自己的文件名和文件夾即可
String fromstring = SystemDateFormat.SdfForTimeString.format(new Date());
String path = "C:\\Users\\Administrator\\Desktop\\_11_4_1臨時文件夾\\20200326\\" + fromstring + endstring;
Map<String, Object> map = new HashMap<>(1);
InputStream fis = null;
FileOutputStream fos = null;
try {
File fileo = new File(path);
if (!fileo.exists()) {
fileo.createNewFile();
}
fos = new FileOutputStream(fileo);
fis = Fdata.getInputStream();
byte[] bytes = new byte[1024];
int aa = 0;
while (true) {
aa = fis.read(bytes, 0, bytes.length);
if (aa == -1) {
break;
}
fos.write(bytes, 0, aa);
}
} catch (Exception e) {
map.put("issuccess", false);
} finally {
try {
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
try {
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return map;
}
public static Map<String, Object> getUploadInfo(HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
ProgressEntity status = (ProgressEntity) request.getSession(true)
.getAttribute("status");// 從session中讀取上傳信息
if (status == null) {
result.put("error", "沒發現上傳文件!");
return result;
}
long startTime = status.getStartTime(); // 上傳開始時間
long currentTime = System.currentTimeMillis(); // 現在時間
long time = (currentTime - startTime) / 1000 + 1;// 已經傳順的時間 單位:s
double velocity = status.getBytesRead() / time; // 傳輸速度:byte/s
double totalTime = status.getContentLength() / velocity; // 估計總時間
double timeLeft = totalTime - time; // 估計剩餘時間
int percent = (int) (100 * (double) status.getBytesRead() / (double) status
.getContentLength()); // 百分比
double length = status.getBytesRead() / 1024 / 1024; // 已完成數
double totalLength = status.getContentLength() / 1024 / 1024; // 總長度 M
result.put("startTime", startTime);
result.put("currentTime", currentTime);
result.put("time", time);
result.put("velocity", velocity);
result.put("totalTime", totalTime);
result.put("timeLeft", timeLeft);
result.put("percent", percent);
result.put("length", length);
result.put("totalLength", totalLength);
if (length >= totalLength) {
result.put("isfinish", 1);
}
return result;
}
}
到這裏,文件上傳後端的所有工作已經做完了,現在你可以通過Controller裏調用FileUploadCommonTool裏的upload就可以實現頁面上傳文件了,同時可以通過頁面輪訓FileUploadCommonTool裏的getUploadInfo獲得文件上傳的進度信息,當然了,所用的前端框架各不相同,我這裏以miniui做個頁面示例
<!DOCTYPE HTML>
<head>
<%@ include file="head.jsp" %>
<script src="<%=contextPath%>/utils/miniui/ajaxfileupload.js"></script>
</head>
<html>
<body>
<div>
<input class="mini-htmlfile" name="Fdata" id="file1" style="width:300px;"/>
<input type="button" value="上傳" onclick="ajaxFileUpload()"/>
<br/>
<div id="p1" class="mini-progressbar" value="0" visible="false" style="width:300px;"></div>
</div>
</body>
</html>
<script>
mini.parse();
var bar = mini.get("p1");
var timeinterint;
var uploadurl = "/flykoala/system/upload";
var getinfourl = "/flykoala/system/getInfo";
function ajaxFileUpload() {
bar.setVisible(true);
bar.setValue(0);
timeinterint = self.setInterval(up_per, 100);
var inputFile = $("#file1 > input:file")[0];
$.ajaxFileUpload({
url: uploadurl,
fileElementId: inputFile,
data: {},
beforeSend: function (xhr) {
},
complete: function () {
},
success: function (data) {
alert('上傳成功');
myWebSocket.closeSocket();
}
});
}
function up_per() {
$.ajax({
url: getinfourl,
success: function (map) {
if (map.isfinish == 1 && map.error != '') {
clearInterval(timeinterint);
}
bar.setValue(map.percent);
}
});
}
</script>
</body>
</html>
這樣是能實現文件上傳和進度顯示,但是海量的輪詢會給服務器增加很大壓力,所以,就有了第三步websocket
實現第三步
第三步主要涉及到了以下幾個類
1.WebsocketConfiguration 配置類,注入了org.springframework.web.socket.server.standard下的ServerEndpointExporter
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @ClassName WebsocketConfiguration
* @Description TODO
* @Author fzj
* @Date 2020-4-8 14:32
* @Version 1.0.0
**/
@Configuration
public class WebsocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.WebsocketEndPoint 節點類,所有的打開,關閉,錯誤,發送信息的方法都在這個類裏,這樣的東西網上的前輩們都寫的各有特色,主要就是那幾個方法,其他的根據自己的業務需求實現就可以了
/**
* @ClassName WebsocketEndPoint
* @Description TODO
* @Author fzj
* @Date 2020-4-8 13:20
* @Version 1.0.0
**/
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/websocket/{sid}")
@Component
public class WebsocketEndPoint {
static Log log = LogFactory.getLog(WebsocketEndPoint.class);
//靜態變量,用來記錄當前在線連接數
private static AtomicInteger onlineCount = new AtomicInteger(0);
//用來存放每個客戶端對應的WebsocketEndPoint對象
private static CopyOnWriteArraySet<WebsocketEndPoint> webSocketSet = new CopyOnWriteArraySet<WebsocketEndPoint>();
//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
//接收sid
private String sid = "";
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在線數加1
log.info("有新窗口開始監聽:" + sid + ",當前在線人數爲" + getOnlineCount());
this.sid = sid;
try {
sendMessage("連接成功");
} catch (IOException e) {
log.error("websocket IO異常");
}
}
@OnClose
public void onClose() {
webSocketSet.remove(this); //從set中刪除
subOnlineCount(); //在線數減1
log.info("有一連接關閉!當前在線人數爲" + getOnlineCount());
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到來自窗口" + sid + "的信息:" + message);
//羣發消息
for (WebsocketEndPoint item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("發生錯誤");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized Integer getOnlineCount() {
return Integer.valueOf(onlineCount.toString());
}
public static synchronized void addOnlineCount() {
WebsocketEndPoint.onlineCount.set(getOnlineCount() + 1);
}
public static synchronized void subOnlineCount() {
WebsocketEndPoint.onlineCount.set(getOnlineCount() - 1);
}
public static CopyOnWriteArraySet<WebsocketEndPoint> getWebSocketSet() {
return webSocketSet;
}
//提供外部可調用的方法獲取sid,用於查找某個用戶的文件上傳信息
public String getSid() {
return sid;
}
}
3.TimerConfig 定時器
package com.fzj.config;
import com.fzj.common.websocket.WebsocketEndPoint;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Configuration
@EnableScheduling
public class TimerConfig {
//用於存儲用戶的request,<Sid,request> 這裏的concurrentHashMap和WebsocketEndPoint類裏的Sid是一致的,這個值可以在FileUploadCommonTool裏的上傳方法裏去put
public static Map concurrentHashMap = new ConcurrentHashMap(1);
public static Integer cou = 0;
@Scheduled(cron = "0/1 * * * * *")
public void sendmsg() {
CopyOnWriteArraySet<WebsocketEndPoint> copyOnWriteArraySet = WebsocketEndPoint.getWebSocketSet();
copyOnWriteArraySet.forEach(c -> {
String s = "0";
//取得的c表示該用戶會話的類
if (concurrentHashMap.containsKey(c.getSid())) {
//此時當前執行文件上傳操作的用戶request集合裏存在該用戶
//調用FileUploadCommonTool裏的獲取上傳信息的方法
//給s賦值爲取得的進度
if(//判斷是否上傳完成) {
//上傳完成刪除concurrentHashMap裏的該用戶信息
concurrentHashMap.remove(c.getSid());
}
}
try {
//發送進度給客戶端
c.sendMessage(s);
} catch (Exception e) {
}
});
}
}
現在就不用頁面上輪詢去訪問服務器去獲取進度了,而是服務器定時發送給你進度,示例前端代碼,將第一個頁面定時輪詢改爲websocket
var WebsocketUrl = "ws://ip地址:端口/上下文路徑/websocket/445";
var webs = new WebSocket(WebsocketUrl );
//打開連接執行
webs.onopen = function (ev) {
alert(1);
}
//斷開連接執行
webs.onclose = function (ev) {
alert(0);
}
//將接收到的進度值設置到進度條
webs.onmessage = function (me) {
bar2.setValue(me.data);
}
//發生錯誤執行
webs.onerror = function (ev) {
console.log(ev);
}
好了,到這裏就結束了
websocket是一種通信協議,只需要進行一次tcp的握手,然後可以實現信號的瞬時雙向傳輸,實時性強,能耗低,是實現網頁聊天功能的有效手段之一。