网站首页> 文章专栏> 用Webrtc来做一个文件传输
用Webrtc来做一个文件传输
原创 时间:2024-03-13 08:03 作者:AI智能 浏览量:2907
Webrtc 文件传输
这里用Webrtc来做一个文件传输,减轻了服务器的压力的同时也多一种传输方式。
准备工作:websocket ,webrtc, springboot ,html
一,创建一个springboot项目,并搭建websocket后台交互

1,创建好springboot项目后,导入websocket依赖的包。版本同springboot一样

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

2,创建 WebSocketConfig 配置类

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class WebSocketConfig implements ServletContextInitializer {

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }

    //设置 websocket 文件传输大小,这里和webrtc传输文件无关系
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addListener(WebAppRootListener.class);
        servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize", "52428800");
        servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize", "52428800");
    }
}
3,创建 File_WebSocket 接口交互类

备注:这里的websocket 主要是用于webrtc之间交互信令的

@EnableWebSocket
@ServerEndpoint("/file/websocket/{user_id}")
@Component
public class File_WebSocket {

    /**
     *  静态变量,用来记录当前的连接数
     */
    private static   int onLineCount = 0;

    /**
     * 存放用户的session
     */
    private static Map sessionMap = new HashMap<>();


    @OnOpen
    public void onOpen(@PathParam("user_id")Integer userId,Session session){
        if (userId != null && userId != 0){
            System.out.println("userId====="+ userId + ":连接成功");
            sessionMap.put(userId,session);
            onLineCount ++;
        }
    }

    @OnMessage
    public void onMessAge(@PathParam("user_id")Integer userId,String message){
        for (Map.Entry entry : sessionMap.entrySet()) {
            if (!entry.getKey().equals(userId)){
                System.out.println(entry.getKey());
                Session session = entry.getValue();
                session.getAsyncRemote().sendText(message);
            }
        }
    }

    /**
     * 连接错误的方法
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("user_id")Integer userId,CloseReason reason) {
        if (reason.getReasonPhrase() != null){
            System.out.println("ws 异常关闭" + reason);
        }
        sessionMap.remove(userId);
        onLineCount --;   // 在线数减1
        System.out.println("有一连接关闭!当前在线人数为" + onLineCount);
    }
}
二,webrtc 传输文件

1,这里写一个html页面具体写这样

1710316354899.webp

2,开发前的流程梳理
  • 发送者:发送SDP信息给接收者
    • SDP文件需要创建webrtc连接 这里参考一下下方html中 prepareNewConnection()
    • 在调用 .createOffer() 生成SDP 并且调用 .setLocalDescription() ,通过WebSocket传送给到接收者。
  • 接受者:接收到发送者的SDP信息
    • 创建webrtc连接 这里参考一下下方html中 prepareNewConnection()
    • 调用 .setRemoteDescription(new RTCSessionDescription(SDP))
    • 返回一个应答信息给到发送者:调用 createAnswer() 生成SDP 并且调用 .setLocalDescription() ,通过WebSocket传送给到发送者。
  • 发送者接收到接受者返回的Answer应答消息
    • 调用.setRemoteDescription(new RTCSessionDescription(SDP))
    • 连接成功
  • 发送者和接收者连接成功后会互相发送两者之间的ICE信息,这样发送ICE才能进行打洞。
    • 参考下方html onicecandidate()
  • 建立数据通道createDataChannel()
RTCSessionDescription

在两个对等点之间协商连接的过程涉及来回交换对象,每个描述都表示描述的发送者支持的连接配置选项的一个组合。一旦两个对等方就连接的配置达成一致,协商就完成了。

html 内容

1
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>文件传输</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"/> <script src="jsLib/jquery-1.8.js"></script> </head> <body style="margin: 0;padding: 0"> <div style="width: 100vw;height: 100vh;display: flex;align-items: center;justify-content: center"> <div style="width: 80%;height:60%;"> <div style="width: 100%;height:10%;background-color: #4896ff;display: flex;align-items: center;justify-content: center;"> <div style="color: white" id="user_id"></div> </div> <div style="width: 100%;height:10%;display: flex;align-items: center"> <label>上传文件: <input id="input_file" type="file"></label> </div> <div style="width: 100%;height: 20%;display: flex;align-items: center;justify-content: center"> <button style="width: 80%;border-radius: 5px;height: 40px" onclick="p2pConnection()">连接</button> <button style="width: 80%;border-radius: 5px;height: 40px" onclick="TestDatachannel()">测试数据通道</button> </div> </div> </div> <script> var user_id = Math.floor(Math.random() * 1000 + 1); //生成随机数 var userIdHtml = '<h1> 当前用户:' + user_id + ' </h1>'; $("#user_id").html(userIdHtml); // ---------------- websocket -------------- // var url = "ws://192.168.1.49:8081/file/websocket/" + user_id; var websocket = null; $(function () { if (websocket) { alert("已链接,无需重复连接"); return; } websocket = new WebSocket(url); websocket.onopen = function (ev) { alert("成功连接服务器"); }; websocket.onmessage = function (ev) { var evt = JSON.parse(ev.data); if (evt.type === 'offer') { onOffer(evt); // console.log("接收到offer,设置offer,发送answer...."); } else if (evt.type === 'answer') { // console.log('接收到answer,设置answer SDP'); onAnswer(evt); } else if (evt.type === 'candidate') { // console.log('接收到ICE候选者..'); onCandidate(evt); } else if (evt.type === 'bye') { // console.log("WebRTC通信断开"); stop(); } }; //发生错误 websocket.onerror = function (ev) { console.log(ev); alert("异常关闭") }; //正常关闭 websocket.onclose = function (ev) { console.log("关闭=====" + ev); }; }); var peerConnection = null; var file = null; var localChannel = null;//本地数据通道 //创建数据传输通道配置 var localChannelOptions = { ordered: true, //按顺序发送 maxRetransmitTime: 5000, }; //发送文件 function p2pConnection() { sendOffer(); // 建立连接和创建SDP } // 建立连接和创建SDP function sendOffer() { if (peerConnection === null) { peerConnection = prepareNewConnection(); peerConnection.createOffer(function (sessionDescription) { peerConnection.setLocalDescription(sessionDescription); // console.log("发送: SDP"); // console.log(sessionDescription); sendSDP(sessionDescription); console.log("发送sdp"); }, function (error) { console.log("创建offer失败"); }) } } //建立ICE和连接 function prepareNewConnection() { var pc_config = { "iceServers": [{ urls: "stun:p2p.sharjeck.com:3478" // urls: "stun:stun.l.google.com:19302" }, { urls: "turn:p2p.sharjeck.com:3478", username: "ddssingsong", credential: "123456" }] }; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config) } catch (e) { console.log("建立连接错误:" + e.message); } //发送所有ICE候选者给对方 peer.onicecandidate = function (evt) { if (evt.candidate) { // console.log(evt.candidate); sendCandidate({ type: "candidate", sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate }); } }; localChannel = peer.createDataChannel('localChannel',localChannelOptions);//本地通道,本地通道接收由远程通道发送过来的数据 localChannel.onerror = datachannel_error; localChannel.onopen = datachannel_open; localChannel.onclose = datachannel_close; localChannel.onmessage = datachannel_message; peer.ondatachannel = pc_datachannel; // return peer; } //发送sdp请求 function sendSDP(sdp) { var text = JSON.stringify(sdp); websocket.send(text); } //-----------------交互信息-------------------- function onOffer(evt) { console.log("接收到offer..."); setOffer(evt); sendAnswer(evt); } function onCandidate(evt) { var candidate = new RTCIceCandidate({ sdpMLineIndex: evt.sdpMLineIndex, sdpMid: evt.sdpMid, candidate: evt.candidate }); peerConnection.addIceCandidate(candidate); } //发送候选者 function sendCandidate(candidate) { var text = JSON.stringify(candidate); console.log(text); // "type":"candidate","sdpMLineIndex":0,"sdpMid":"0","candidate":".... websocket.send(text) // socket发送 } function setOffer(evt) { //为空才设置 if (peerConnection) { console.error('peerConnection已存在!'); return; } peerConnection = prepareNewConnection(); peerConnection.setRemoteDescription(new RTCSessionDescription(evt)); } function sendAnswer() { if (!peerConnection) { console.error('peerConnection不存在!'); return; } console.log('发送Answer,创建远程会话描述...'); peerConnection.createAnswer(function (sessionDescription) { //成功时 peerConnection.setLocalDescription(sessionDescription); sendSDP(sessionDescription); }, function () { //失败时 console.log("创建Answer失败"); }); } function onAnswer(evt) { console.log("设置Answer..."); setAnswer(evt); } function setAnswer(evt) { if (!peerConnection) { console.error('peerConnection不存在!'); return; } peerConnection.setRemoteDescription(new RTCSessionDescription(evt)); console.log("==== 连接成功 ======"); } //数据通道回调方法 var datachannel_error = function (error) { console.log("数据传输通道建立异常:", error); }; var datachannel_open = function () { console.log("本地数据通道建立成功"); }; var datachannel_close = function () { console.log("关闭数据传输通道"); }; var datachannel_message = function(event){ console.log(event.data); }; function TestDatachannel() { //上传文件 file = document.getElementById('input_file').files[0]; var fileSize = file.size; var fileName = file.name; var sendMaxSize = 1024 * 248;//设定每次发送的最大字节 这里最大不超过255KB 不然会报错 console.log(JSON.stringify(param)); localChannel.send(JSON.stringify(param));//给远方发送即将要发送的文件信息 var fileReader = new FileReader(); fileReader.onload = function(){//每次加载数据后则发送过去 localChannel.send(fileReader.result);//给远方传送文件数据 if(done < fileSize){ tempLoad(); } }; var done = 0; var tempLoad = function(){ setTimeout(function () { fileReader.readAsArrayBuffer(tempFile.slice(done, sendMaxSize + done)); done = done + sendMaxSize; },100); }; tempLoad(); document.getElementById('input_file').value = ""; } var downloadFileData = {'maxsize':0,'filename':null,'data':[]};//下载文件数据预存 //远程数据通道 var pc_datachannel = function (evt) { var receiveChannel = event.channel; receiveChannel.onmessage = function (event) { var msg = null; try { msg = JSON.parse(event.data) }catch (e) { if (downloadFileData.filename != null){ downloadFileData.data.push(event.data); var doneSize = 0; for (var i = 0; i< downloadFileData.data.length; i++){ doneSize += downloadFileData.data[i].byteLength; } if (downloadFileData.maxsize <= doneSize){ //如果已完成 <= 最大长度,则代表传输结束 var fileBlob = new Blob(downloadFileData.data); var anchor = document.createElement("a"); anchor.href = URL.createObjectURL(fileBlob); alert(anchor.href); anchor.download = downloadFileData.filename; anchor.click(); downloadFileData = {'maxsize':0,'filename':null,'data':[]};//初始化 } } } if (msg != null && msg.type === 'file'){ downloadFileData.maxsize = msg.data.fileSize; downloadFileData.filename = msg.data.fileName; } } } </script> </body> </html>

实现效果:

发送:

1710316923157.webp

接收:

1710316942594.webp

这里只是简单开发测试,可作参考;
注意:
1,webrtc数据传输是基于数据通道的,而数据通道默认值是256KB,所以大文件分段传输的时候,每包都要小于256KB,建议设置248KB进行传输。
2,webrtc数据传输由于分包时,接收端不及时接收的话,发送端会产生队列,队列过长会报错DataChannel': RTCDataChannel send queue is full
at fileReader.onload 。可以发送端发送一个包,接收端,接收后发送消息给到发送端,让发送端发送,直到发完为止。
动动小手 !!!
来说两句吧
最新评论
  • 初时模样
    优质好文,收藏起来慢慢学习~

  • 帅到冷场
    文章写得深入、细致、专业,收藏啦

  • 故渊
    支持博主优秀好文

  • 工程创客
    优质好文,博主的文章细节很到位,兼顾实用性和可操作性,感谢博主的分享,期待博主持续带来更多好文

  • 帅到冷场
    厉害了学习一下

  • 深海有鱼
    大佬牛呀,太实用了!