通过WebRTC实现局域网p2p文件传输功能

木头的喵喵拖孩

预览

前言

起初我是想实现一个不论在内网还是公网环境下都能建立 p2p 连接传输文件的功能,后来经过学习发现,内网环境实现起来很简单,但是公网就很麻烦了,要用到 STUN 和 TURN,并且不符合我“只能通过 p2p 传输文件,不能经过中转服务器”的理念,所以这个项目只实现了局域网这部分的功能

已知问题

  • 因为信令服务器没有部署 ssl 证书,所以当前端页面部署到 https 协议的服务器里时,无法访问信令服务器

关键概念

  • p2p:对等连接。
  • 信令服务器:帮助 p2p 双方交换进行 WebRTC 连接所必须的信息,一般用 WebSocket 实现。
  • STUN:帮助 p2p 双方获取到对方的 IP 地址和端口号,_(仅公网和多重内网环境需要)_。
  • TURN:数据中转服务,当 p2p 连接失败时,会使用 TURN 服务中转数据,_(仅公网和多重内网环境需要)_。

代码实现

信令服务器目录结构

  • src
    • server.js 主文件
    • Users.js 用户类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// server.js

"use strict";

const fs = require("fs");
const http = require("http");
const https = require("https");
const ws = require("ws");
const Users = require("./Users");

const port = 9999;

const middleware = (req, res) => {
// 允许跨域连接
res.writeHead(200, {
// 'Content-Type': 'text/plain',
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
});
};

let server;
if (fs.existsSync("./ssl/server.crt")) {
// 如果根目录有ssl证书则使用https
server = https.createServer(
{
key: fs.readFileSync("./ssl/server.key"),
cert: fs.readFileSync("./ssl/server.crt"),
},
middleware
);
} else {
server = http.createServer(middleware);
}

const wsServer = new ws.Server({
server,
});

let users = new Users();

const onMsghandler = (socket) => {
console.log("socket创建成功");
socket.on("message", (msg) => {
let obj;
try {
obj = JSON.parse(msg.toString());
} catch (err) {}
let { type, data } = obj;
switch (type) {
case "login":
{
let flag = users.addUser(socket);
socket.send(
JSON.stringify({
type: "login_success",
data: flag,
})
);
console.log("当前用户列表", users.getUserFlags());
}
break;
case "send_offer":
{
let userRemote = users.getUserByFlag(data.flagRemote);
if (userRemote === undefined) {
socket.send(
JSON.stringify({
type: "remote_disconnected",
})
);
} else {
userRemote.socket.send(
JSON.stringify({
type: "receive_offer",
data,
})
);
}
}
break;
case "send_answer":
{
let userRemote = users.getUserByFlag(data.flagRemote);
if (userRemote === undefined) {
socket.send(
JSON.stringify({
type: "remote_disconnected",
})
);
} else {
userRemote.socket.send(
JSON.stringify({
type: "receive_answer",
data,
})
);
}
}
break;
case "send_ice":
{
let userRemote = users.getUserByFlag(data.flagRemote);
if (userRemote === undefined) {
socket.send(
JSON.stringify({
type: "remote_disconnected",
})
);
} else {
userRemote.socket.send(
JSON.stringify({
type: "receive_ice",
data,
})
);
}
}
break;
default:
break;
}
});
};

// 客户端与服务端建立连接
wsServer.on("connection", onMsghandler);
server.listen(port);
console.log(`---listen on ${port}---`);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Users.js

"use strict";

class Users {
#list;
#timeoutLimit; // 自动断开连接时间
#userLimit; // 最大人数

constructor() {
this.#list = new Map();
this.#timeoutLimit = 3 * 60 * 1000; // 保证足够时间交换进行WebRTC需要的数据
this.#userLimit = 5;
}

getUserFlags() {
return Array.from(this.#list.keys());
}

getUserByFlag(flag) {
return this.#list.get(flag);
}

addUser(socket) {
// 如果用户超过限制,则删除第一个用户
if (this.#list.size >= this.#userLimit) {
this.deleteUser(this.#list.keys().next().value);
}

// 创建用户id
let flag = new Date().getTime().toString().substring(8).split("");
flag.splice(2, 0, "-");
flag = flag.join("");

// 定时删除用户
let timer = setTimeout(() => {
this.deleteUser(flag);
}, this.#timeoutLimit);

this.#list.set(flag, {
timer,
socket,
});
return flag;
}

deleteUser(flag) {
let user = this.#list.get(flag);
// 通知用户登录超时
user.socket.send(
JSON.stringify({
type: "login_timeout",
})
);
// 断开当前用户socket连接
user.socket.close();
// 请除定时器
clearTimeout(user.timer);
// 删除用户
this.#list.delete(flag);
}
}

module.exports = Users;

前端页面目录结构

  • public
    • index.html
  • src
    • index.js 主文件
    • FileTransfer.js 文件传输类(核心类)
    • utils.js 工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文件传输</title>
<style>
* {
box-sizing: border-box;
}

body {
margin: 0;
padding: 12px;
}

#flag {
cursor: pointer;
}

#state > span {
display: none;
}

#state > .wsdisconnected {
color: gray;
}

#state > .wsconnecting {
color: blue;
}

#state > .wsconnected {
color: green;
}

#state > .pcconnecting {
color: blue;
}

#state > .pcconnected {
color: green;
}

#log {
max-height: 10vh;
overflow-y: auto;
}

#userArea {
display: none;
}

#fileArea {
display: none;
}

#fileAreaInner {
display: flex;
justify-content: space-between;
align-items: center;
}

#fileAreaInner > button {
width: 90px;
}

#fileAreaInner > div {
width: calc((100% - 90px - 20px) / 2);
height: 400px;
padding: 0 8px;
overflow-x: hidden;
overflow-y: auto;
}

#fileAreaInner > .sendArea {
border-right: 1px solid black;
}

#fileAreaInner > .receiveArea {
border-left: 1px solid black;
}

.fileList {
width: 100%;
list-style-type: decimal;
}

.fileList > .percent {
position: relative;
height: 22px;
border: 1px solid black;
margin: 8px 0;
font-size: 12px;
}

.fileList > .percent > .bar {
position: relative;
z-index: 0;
display: block;
width: 0%;
height: 100%;
padding: 0 8px;
background-color: gray;
font-size: inherit;
}

.fileList > .percent > .text {
position: absolute;
z-index: 1;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 0 8px;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
font-size: inherit;
}
</style>
</head>

<body>
<h3>状态</h3>
<p id="state">
<span class="wsdisconnected">WebSocket未连接</span>
<span class="wsconnecting">WebSocket连接中...</span>
<span class="wsconnected">WebSocket连接成功</span>
<span class="pcconnecting">WebRTC连接中...</span>
<span class="pcconnected">WebRTC连接成功</span>
</p>

<hr />

<!-- log -->
<h3>日志</h3>
<div id="log"></div>

<hr />

<div id="userArea">
<h3>
<p>当前页面ID:</p>
<input id="flag" readonly></input>
</h3>

<hr />

<h3>
<p>输入对方页面ID:</p>
<input id="flagRemote" type="text" placeholder="xx-xxx"/><button id="flagRemoteBtn">
确认
</button>
</h3>

<hr />

<div id="fileArea">

<div id="fileAreaInner">
<div class="sendArea">
<p>
<input id="uploadFile" type="file" />
</p>
<h4>发送列表:</h4>
<ul id="sendList" class="fileList"></ul>
</div>

<button id="abort">终止传输</button>

<div class="receiveArea">
<h4>接收列表:</h4>
<ul id="receiveList" class="fileList"></ul>
</div>
</div>

</div>
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// index.js

"use strict";
import "core-js";
import {
haveWebSocket,
haveWebRTC,
$,
gen,
initDom,
changeState,
log,
} from "./utils";
import FileTransfer from "./FileTransfer";

const $state = $("state");
const $userArea = $("userArea");
const $flag = $("flag");
const $flagRemote = $("flagRemote");
const $flagRemoteBtn = $("flagRemoteBtn");
const $logDom = $("log");
const $fileArea = $("fileArea");
const $uploadFile = $("uploadFile");
const $sendList = $("sendList");
const $receiveList = $("receiveList");
const $abort = $("abort");

let wsServerUrl;
if (process.env.NODE_ENV === "development") {
// 测试环境信令服务器地址
wsServerUrl = "ws://localhost:9999";
} else if (process.env.NODE_ENV === "production") {
// 正式环境信令服务器地址
wsServerUrl = "";
}

let $sendLi, $receiveLi;

initDom();

(function () {
if (!haveWebSocket()) {
myAlert("当前浏览器不支持WebSocket");
return;
}
if (!haveWebRTC()) {
myAlert("当前浏览器不支持WebRTC");
return;
}

const ft = new FileTransfer(wsServerUrl);
ft.setLogDom($logDom);
ft.addEventListener("statechange", (ev) => {
let state = ev.target.getState();
changeState(state, $state);
log(state, $logDom);
switch (state) {
case "wsdisconnected":
let err = ft.getError();
if (err) {
log(err.message, $logDom);
}
default:
$flag.value = "";
$flagRemote.value = "";
$userArea.hide();
$fileArea.hide();
break;
case "wsconnected":
let flag = ft.getFlag();
$flag.value = flag;
$userArea.show();
break;
case "pcconnecting":
$userArea.show();
break;
case "pcconnected":
$userArea.show();
$fileArea.show();
onRTCConnected();
break;
}
});
$abort.onclick = () => {
if (window.confirm("确认终止传输,该操作不可逆")) {
ft.abort.call(ft);
}
};
$flagRemoteBtn.onclick = onFlagRemoteBtnClick;

function onFlagRemoteBtnClick() {
let flagRemote = $flagRemote.value;
if (flagRemote === "") {
alert("对方id不能为空");
} else if (flagRemote === ft.getFlag()) {
alert("不能输入自己的id");
$flagRemote.value = "";
} else if (!/^\d{2}\-\d{3}$/.test(flagRemote)) {
alert("对方id格式不正确,请输入 xx-xxx 格式");
$flagRemote.value = "";
} else {
ft.setFlagRemote(flagRemote);
ft.connect();
}
}

function onRTCConnected() {
$uploadFile.onchange = (ev) => {
let file = ev.target.files[0];
// 开始发送文件
ft.addEventListener("sendfileinfo", (ev) => {
$uploadFile.disabled = true;
// 文件基本信息
let fileInfo = ev.target.getFileInfo();
log("开始发送文件", $logDom);
log(fileInfo, $logDom);
if (
!Array.from($sendList.children).find(($a) =>
new RegExp(fileInfo.name).test($a.innerText)
)
) {
$sendLi = gen("li");
$sendLi.innerText = `${fileInfo.name}(0%)`;
$sendList.appendChild($sendLi);
}
});
// 正在发送文件
ft.addEventListener("sending", (ev) => {
let sendPercent = ev.target.getSendPercent();
$sendLi.innerText = $sendLi.innerText.replace(
/\(\d{1,3}(\.\d{1,2})?\%\)$/,
`(${sendPercent}%)`
);
});
// 发送文件完毕
ft.addEventListener("sended", () => {
log("发送文件完毕");
$uploadFile.disabled = false;
$uploadFile.value = null;
});
ft.sendFile(file);
};

// 开始接收文件
ft.addEventListener("receivefileinfo", (ev) => {
$uploadFile.disabled = true;
// 文件基本信息
let fileInfo = ev.target.getFileInfo();
log("开始接收文件", $logDom);
log(fileInfo, $logDom);
if (
!Array.from($receiveList.children).find(($a) =>
new RegExp(fileInfo.name).test($a.innerText)
)
) {
$receiveLi = gen("li");
let $a = gen("a");
$a.innerText = `${fileInfo.name}(0%)`;
$receiveLi.appendChild($a);
$receiveList.appendChild($receiveLi);
}
});
// 正在接收文件
ft.addEventListener("receiving", (ev) => {
let receivePercent = ev.target.getReceivePercent();
let $a = $receiveLi.children[0];
$a.innerText = $a.innerText.replace(
/\(\d{1,3}(\.\d{1,2})?\%\)$/,
`(${receivePercent}%)`
);
});
// 接收文件完毕
ft.addEventListener("received", (ev) => {
$uploadFile.disabled = false;
$uploadFile.value = null;
let file = ev.target.getFile();
log("接收文件完毕", $logDom);
let $a = $receiveLi.children[0];
$a.href = URL.createObjectURL(file);
$a.download = $a.innerText.replace(/\(\d{1,3}(\.\d{1,2})?\%\)$/, "");
});
}
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
// FileTransfer.js

import { log } from "./utils";

class FileTransfer extends EventTarget {
// 连接
#wsServerUrl;
#ws;
#pc;
#dc;
#dcRemote;

// 中止标记
#abortFlag = false;

// 状态
#state;
#rtcSuccess = false;

// 日志
#logDom;
#err;

// 对象页面标记
#flag;
#flagRemote;

// 发送文件
#sendFileInterval = 0; // 发送chunk之间的间隔时间,这里单位是毫秒。貌似只要异步就能避免send queue full,但还是给了他一个间隔时间
#sendFileChunkSize = 4 * 1024; // 每次发送的chunk大小,单位为BYTE
#sendFileOffset;
#sendPercent = 0;

// 文件
#fileInfo;
#file;

// 接收文件
#receiveBuffer;
#receiveFileSize;
#receiveFileTotalSize;
#receivePercent = 0;

/**
* 这个类封装了建立WebRTC连接的详细过程
* @param {String} wsServerUrl WebSockets服务器地址
*/
constructor(wsServerUrl) {
super();
this.#wsServerUrl = wsServerUrl;
this.#setState("wsdisconnected");
this.#initFileInfo();
this.#initWebSocket();
this.#initWebRTC();
}

#initWebSocket() {
this.#ws = new WebSocket(this.#wsServerUrl);
this.#ws.onopen = () => {
log("建立WebSocket连接成功", this.#logDom);
this.#setState("wsconnecting");
// 登录
this.#ws.send(JSON.stringify({ type: "login" }));
this.#ws.onmessage = (msg) => this.#wsMsgHandler.call(this, msg);
};
this.#ws.onerror = (err) => {
this.#setState("wsdisconnected");
log("建立WebSocket连接失败" + JSON.stringify(err), this.#logDom, 2);
alert("建立WebSocket连接失败" + JSON.stringify(err));
};
}

/**
* Socket msg事件处理
* @param {Object} msg
*/
#wsMsgHandler(msg) {
let obj;
try {
obj = JSON.parse(msg.data);
} catch (err) {}
let { type, data } = obj;
log("type:" + type, this.#logDom);
switch (type) {
case "login_success":
log("登录成功", this.#logDom);
this.#flag = data;
this.#setState("wsconnected");
break;
case "login_timeout":
if (!this.#rtcSuccess) {
this.#flag = "";
this.#setState("wsdisconnected");
}
break;
case "remote_disconnected":
this.#flagRemote = "";
this.#setState("wsdisconnected");
alert("对方已掉线,请对方刷新网页");
break;
case "receive_offer":
log("接收Offer", this.#logDom);
this.#flagRemote = data.flag;
this.#createAnswer(data);
break;
case "receive_answer":
log("接收Answer", this.#logDom);
this.#receiveAnswer(data);
break;
case "receive_ice":
log("接收Ice", this.#logDom);
this.#receiveIce(data);
break;
default:
break;
}
}

/**
* 创建Offer并发送
*/
async #createOffer() {
this.#setState("pcconnecting");
let offer = await this.#pc.createOffer();
await this.#pc.setLocalDescription(offer);
this.#createIce();

log("发送Offer", this.#logDom);
this.#ws.send(
JSON.stringify({
type: "send_offer",
data: {
flag: this.#flag,
flagRemote: this.#flagRemote,
sdp: offer,
},
})
);
}

/**
* 创建Answer并发送
* @param {Object} data
*/
async #createAnswer(data) {
let offer = data.sdp;
await this.#pc.setRemoteDescription(offer);
let answer = await this.#pc.createAnswer();
await this.#pc.setLocalDescription(answer);
this.#createIce();

log("发送Answer", this.#logDom);
this.#ws.send(
JSON.stringify({
type: "send_answer",
data: {
flag: this.#flag,
flagRemote: this.#flagRemote,
sdp: answer,
},
})
);
}

/**
* 完成Offer/Answer交换
* @param {Object} data
*/
async #receiveAnswer(data) {
let answer = data.sdp;
await this.#pc.setRemoteDescription(answer);
}

/**
* 创建icecandidate并发送
*/
#createIce() {
this.#pc.onicecandidate = (ev) => {
if (ev.candidate) {
log("发送Ice", this.#logDom);
this.#ws.send(
JSON.stringify({
type: "send_ice",
data: {
flag: this.#flag,
flagRemote: this.#flagRemote,
ice: ev.candidate,
},
})
);
}
};
}

/**
* 接收icecandidate并保存
* @param {Object} data
*/
#receiveIce(data) {
let { ice } = data;
this.#pc.addIceCandidate(ice);
}

#initWebRTC() {
this.#pc = new RTCPeerConnection({
/*
虽然只在内网环境实现p2p连接,但是可能出现多重内网的情况,所以还是添加一些STUN服务,
由于这些STUN服务是第三方提供的,所以可能会出现不可用的情况,需要酌情更换
*/
iceServers: [
{ url: "stun:stun.l.google.com:19302" },
{ url: "stun:stun.services.mozilla.com" },
],
sdpSemantics: "plan-b",
});
this.#initDataChannel();
}

#initDataChannel() {
// 本地数据通道
this.#dc = this.#pc.createDataChannel("fileTransfer", { ordered: true });
this.#dcConntHandler(this.#dc);

// 远端数据通道
this.#pc.ondatachannel = (ev) => {
this.#dcRemote = ev.channel;
this.#dcConntHandler(this.#dcRemote);
};
}

/**
* 数据通道连接事件处理
* @param {RTCDataChannel} dc
*/
#dcConntHandler(dc) {
dc.onopen = () => {
log("建立WebRTC连接成功", this.#logDom);
this.#rtcSuccess = true;
this.#setState("pcconnected");
dc.onmessage = (msg) => this.#dcMsgHandler.call(this, msg);
};
dc.onerror = (err) => {
this.#setState("pcdisconnected");
log("建立WebRTC连接失败" + JSON.stringify(err), this.#logDom, 2);
alert("建立WebRTC连接失败" + JSON.stringify(err));
};
}

/**
* 数据通道msg事件处理
* @param {Object} msg
*/
#dcMsgHandler(msg) {
if (!this.#abortFlag) {
let { data } = msg;
if (typeof data === "string") {
log("收到文件基本信息" + data, this.#logDom);
let dataObj;
try {
dataObj = JSON.parse(data);
} catch (err) {}
this.#fileInfo = dataObj;
this.#receiveFileTotalSize = dataObj.size;
this.dispatchEvent(new Event("receivefileinfo"));
} else {
setTimeout(() => {
if (data instanceof ArrayBuffer) {
this.#receiveFileSize += data.byteLength;
} else if (data instanceof Blob) {
this.#receiveFileSize += data.size;
}

this.#receivePercent =
Math.ceil(
(this.#receiveFileSize / this.#receiveFileTotalSize) * 1000
) / 10;
this.dispatchEvent(new Event("receiving"));
this.#receiveBuffer.push(data);
if (this.#receiveFileSize >= this.#receiveFileTotalSize) {
let file = new Blob(this.#receiveBuffer);
this.#file = file;
this.dispatchEvent(new Event("received"));
this.#initFileInfo();
}
}, this.#sendFileInterval);
}
}
}

/**
* 初始化文件信息
*/
#initFileInfo() {
this.#sendFileOffset = 0;
this.#sendPercent = 0;
this.#receiveBuffer = [];
this.#receiveFileSize = 0;
this.#receiveFileTotalSize = 0;
this.#receivePercent = 0;
this.#fileInfo = null;
this.#file = null;
}

#setState(state) {
this.#state = state;
this.dispatchEvent(new Event("statechange"));
}
getState() {
return this.#state;
}
getSendPercent() {
return this.#sendPercent;
}
getReceivePercent() {
return this.#receivePercent;
}
getFileInfo() {
return this.#fileInfo;
}
getFile() {
return this.#file;
}
getFlag() {
return this.#flag;
}
getError() {
return this.#err;
}

setLogDom(dom) {
this.#logDom = dom;
}

setFlagRemote(flag) {
this.#flagRemote = flag;
}

connect() {
this.#createOffer();
}

sendFile(file) {
let fr = new FileReader();

// 先发送文件基本信息
this.#fileInfo = {
name: file.name,
size: file.size,
};
this.#dc.send(JSON.stringify(this.#fileInfo));
this.dispatchEvent(new Event("sendfileinfo"));

fr.onload = (ev) => {
setTimeout(() => {
this.#dc.send(ev.target.result);
this.#sendFileOffset += ev.target.result.byteLength;
this.#sendPercent =
Math.ceil((this.#sendFileOffset / file.size) * 1000) / 10;
this.dispatchEvent(new Event("sending"));
if (this.#sendFileOffset < file.size) {
if (!this.#abortFlag) {
readSlice(this.#sendFileOffset);
}
} else {
this.dispatchEvent(new Event("sended"));
this.#initFileInfo();
}
}, this.#sendFileInterval);
};

// 分段发送文件
const readSlice = (offset) => {
let fileSlice = file.slice(offset, offset + this.#sendFileChunkSize);
fr.readAsArrayBuffer(fileSlice);
};
readSlice(0);
}

abort() {
this.#abortFlag = true;
}
}

export default FileTransfer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// utils.js

export function haveWebSocket() {
if (window.WebSocket) return true;
return false;
}

export function haveWebRTC() {
window.RTCPeerConnection =
window.RTCPeerConnection ||
window.webkitRTCPeerConnection ||
window.mozRTCPeerConnection ||
window.msRTCPeerConnection;
if (window.RTCPeerConnection) return true;
return false;
}

/**
* 日志记录
* @param {String} msg
* @param {HTMLElement} dom
* @param {Number} errLevel
*/
export function log(msg, dom, errLevel = 0) {
let p = document.createElement("p");
p.innerText = JSON.stringify(msg);
switch (errLevel) {
case 0:
default:
p.style.color = "gray";
console.log(msg);
break;
case 1:
p.style.color = "orange";
console.warn(msg);
break;
case 2:
p.style.color = "red";
console.error(msg);
break;
}
if (dom) dom.prepend(p);
}

/**
* DOM ID选择器简化版
* @param {String} selector
* @returns {HTMLElement}
*/
export function $(selector) {
return document.getElementById(selector);
}

/**
* 创建DOM 简化版
* @param {String} tag
* @returns {HTMLElement}
*/
export function gen(tag) {
return document.createElement(tag);
}

/**
* 初始化DOM并为其添加常用方法
*/
export function initDom() {
if (!Element.prototype.show) {
Element.prototype.show = function () {
this.style.display = "initial";
};
}
if (!Element.prototype.hide) {
Element.prototype.hide = function () {
this.style.display = "none";
};
}
if (!Element.prototype.isShow) {
Element.prototype.isShow = function () {
return this.style.display !== "none";
};
}
if (!Element.prototype.hasClass) {
Element.prototype.hasClass = function (className) {
return this.classList.contains(className);
};
}
}

/**
* 修改连接状态
* @param {String} status
* @param {HTMLElement} dom
*/
export function changeState(status, dom) {
for (let $child of dom.children) {
if ($child.hasClass(status)) {
$child.show();
} else {
$child.hide();
}
}
}
  • 标题: 通过WebRTC实现局域网p2p文件传输功能
  • 作者: 木头的喵喵拖孩
  • 创建于: 2023-05-16 11:01:07
  • 更新于: 2024-05-21 10:59:50
  • 链接: https://blog.xx-xx.top/2023/05/16/通过WebRTC实现局域网p2p文件传输功能/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。