springboot裏使用websocket實現文件上傳並且顯示進度

首先,我們清晰一下思路

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的握手,然後可以實現信號的瞬時雙向傳輸,實時性強,能耗低,是實現網頁聊天功能的有效手段之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章