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页面具体写这样
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>
实现效果:
发送:
接收:
这里只是简单开发测试,可作参考;
注意:
1,webrtc数据传输是基于数据通道的,而数据通道默认值是256KB,所以大文件分段传输的时候,每包都要小于256KB,建议设置248KB进行传输。
2,webrtc数据传输由于分包时,接收端不及时接收的话,发送端会产生队列,队列过长会报错DataChannel': RTCDataChannel send queue is full
at fileReader.onload 。可以发送端发送一个包,接收端,接收后发送消息给到发送端,让发送端发送,直到发完为止。
2024-03-13 16:30:06 回复