局域网传输文件
注意,该功能已经下架。
下架原因:没有适用场景,性能差。
实现原理:两台手机同时打开小程序,创建配对,然后选择视频文件,切割视频文件为固定块,然后传输给对端。创建配对时,使用 Websocket 连接到服务器,互相传送双方的 UDP 地址和端口。传输时使用 UDP 传输。因为 UDP 不用建立连接。在局域网中,因为网络环境好,非常适合 UDP,并且小程序不支持建立 TCP 监听。
问题记录:在和 PC 上开发环境联调时,会发现数据单向传输,排查原因是防火墙原因。手机端未发生此现象。但是设计的算法不好,传输非常慢,并且带宽无法完全利用。也没有纠错和重传机制。这个还是有一点技术含量的。花费了不少的时间。
wxml 文件
<!--pages/loctransfile/index.wxml-->
<view class="page">
<view class="weui-form">
<view class="weui-form__bd">
<view class="weui-form__text-area">
<h2 class="weui-form__title">局域网视频文件传输</h2>
<view class="weui-form__desc">打开手机WIFI后,可将本手机的视频文件发送到局域网内的另一个手机。两个手机需先建立配对,才能发送视频文件。</view>
</view>
<view class="weui-form__control-area">
<view class="weui-cells__group weui-cells__group_form">
<view class="weui-cells__group weui-cells__group_form">
<view class="weui-cells__title">文件信息</view>
<view class="weui-cells weui-cells_form">
<view class="weui-cell weui-cell_uploader">
<view class="weui-cell__bd">
<view class="weui-uploader">
<view class="weui-uploader__hd">
<view aria-role="option" class="weui-uploader__overview">
<view class="weui-uploader__title">选择视频文件</view>
</view>
<view class="weui-uploader__tips">
请选择单个哟
</view>
</view>
<view class="weui-uploader__bd">
<view class="weui-uploader__files" id="uploaderFiles">
<block wx:for="{{files}}" wx:key="*this">
<view class="weui-uploader__file" bindtap="viewVideo" id="{{item}}">
<video class="weui-uploader__img" src="{{item}}" />
</view>
</block>
</view>
<view class="weui-uploader__input-box">
<view aria-role="button" aria-label="选择视频" class="weui-uploader__input" bindtap="getVideo"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="weui-cells__group weui-cells__group_form">
<view class="weui-cells__title">传输信息</view>
<view class="weui-cells weui-cells_form">
<view class="weui-cell" hover-class="weui-cell_active">
<view class="weui-cell__bd">
<progress percent="{{pi}}" stroke-width="3" show-info="true"/>
</view>
</view>
<view class="weui-cell">
<view class="weui-cell__bd">
您分配的ID:{{myId}}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="weui-form__ft">
<view class="weui-form__tips-area">
<view class="weui-form__tips">
{{tips}}
</view>
</view>
<view class="weui-form__opr-area">
<button aria-role="button" loading="{{isLoading}}" disabled="{{isPair}}" class="weui-btn weui-btn_primary" bind:tap="doConnServer">建立配对</button>
<button aria-role="button" class="weui-btn weui-btn_default" bind:tap="doTransfile">传输文件</button>
<button aria-role="button" class="weui-btn weui-btn_default" bind:tap="doSaveFile">保存到相册</button>
</view>
</view>
</view>
</view>
</view>
wxss 文件
无
js 文件
const utils = require("../../utils/utils.js");
const ohttp = require("../../utils/ostrichHttp")
import MyWs from './myws.js';
import MyUdp from './myudp.js';
import MyFile from './myfile.js';
// 定义常量
const UdpPacketSize = 1038; // 1038 字节 TLV (Tag 2|Length 4|PkgIdx 4|VerifyCode 4|Content 1024)
const UdpContentSize = 1024;
// 超时定时器
var tt = null;
// Websockeet相关
var cws = null; // ws 类
// UDP相关
var cu = null; // udp 类
// 文件缓存发送
var sbuf = null;
var sbufIdx = 0;
// 文件接收缓冲
var rbuf = null;
var rbufIdx = 0;
// 包序号
var pkgIdx = 1;
// 块计数
var pieceIdx = 0;
var destPieceIdx = 0;
Page({
/**
* 页面的初始数据
*/
data: {
tips: '',
openid: '',
secret: '',
isLoading: false,
isPair: false,
pi: 0, // 进度条,发送和接收各自显示
// 文件信息
files: [],
fileSize: 0,
pieceCount: 0, // 文件切片数量,文件可以分成几个1024块,向上取整。
cacheSize: 0, // 文件内存缓存大小,是文件切片数量*1024的大小。
fileType: '', // 文件类型
// 发送文件信息记录
destCacheSize: 0,
destFileSize: 0,
destFileType: '',
destPieceCount: 0,
// 我的信息
myId: '',
myIp: '',
myPort: 0,
// 对方信息
destId: '',
destIp: '',
destPort: 0,
// savefile
locfilepath: '',
},
// 选择视频
getVideo() {
const that = this;
wx.chooseMedia({
count: 1,
mediaType: ['video'],
sourceType: ['album'],
sizeType: ['original'],
success(res) {
var tempFiles = res.tempFiles;
const tfp = tempFiles[0].tempFilePath;
let fileType = utils.getExtension(tfp);
let myfiles = [];
myfiles.push(tfp);
let fz = tempFiles[0].size;
let bk1 = Math.ceil(fz / UdpContentSize);
let bk2 = bk1 * UdpContentSize;
console.log('文件大小,', fz, bk2);
that.setData({
files: myfiles,
fileSize: fz,
fileType: fileType,
pieceCount: bk1,
cacheSize: bk2,
pi: 0,
});
console.log('debug,', that.data.files, that.data.fileSize, that.data.fileType)
// 选择文件后,重新初始化设置。
sbuf = null;
sbufIdx = 0;
pkgIdx = 1;
pieceIdx = 0;
},
});
},
// 预览视频
viewVideo(e) {
wx.previewMedia({
current: e.currentTarget.id,
sources: [{
url: this.data.files[0],
type: 'video',
}],
});
},
// 获取openid,连接Websocket使用
getOpenid() {
const that = this;
ohttp.httpGet('/getOpenid').then((res) => {
console.log(res);
if (res.data.code == 1) {
const rdata = res.data.data;
that.setData({
openid: rdata,
})
}
}).catch((err) => {
console.log(err);
})
},
// 获取文件内容
getFileContent() {
const that = this;
let crf1 = new MyFile();
let rbuf = crf1.readFile(that.data.files[0]);
console.log(rbuf.byteLength);
return rbuf;
},
// 保存到文件
save2File() {
const that = this;
let filePath2 = `${wx.env.USER_DATA_PATH}/wxtemp` + that.data.destFileType;
that.setData({
locfilepath: filePath2,
})
let fileSize = that.data.destFileSize;
let ab = new ArrayBuffer(fileSize);
utils.copyBuffer(ab, 0, rbuf, 0, fileSize);
let crf2 = new MyFile();
crf2.writeFile(filePath2, ab);
},
// ======================UDP代码片段=========================== //
// udp 错误监听
uErr(res) {
console.log('udp err,', res.errMsg);
const that = this;
// 关闭udp
cu.close();
},
// UDP消息
uMsg(res) {
// console.log('recv msg,', res);
const that = this;
const msg = res.message;
const mdv = new DataView(msg);
const tag = mdv.getUint16(0, true);
// console.log('tag,', tag);
// 收到确认数据
switch (tag) {
case 0x0001:
pkgIdx = mdv.getUint32(6, true);
let fileSize = mdv.getUint32(14, true);
let cacheSize = mdv.getUint32(18, true);
let fileTypeCode = mdv.getUint32(22, true);
let pieceCount = mdv.getUint32(26, true);
let fileType = utils.getFileType(fileTypeCode);
console.log('0x1,', fileSize, cacheSize, fileType, pieceCount, );
console.log('0x1,', that.data.destIp, that.data.destPort, that.data.myIp, that.data.myPort);
that.setData({
destFileSize: fileSize,
destCacheSize: cacheSize,
destPieceCount: pieceCount,
destFileType: fileType,
})
rbuf = new ArrayBuffer(cacheSize);
rbufIdx = 0;
var ab = new ArrayBuffer(10);
var dv = new DataView(ab);
dv.setUint16(0, 0x0002, true);
dv.setUint32(2, 4, true);
dv.setUint32(6, pkgIdx, true);
cu.send(that.data.destIp, that.data.destPort, ab);
break;
case 0x0002:
pkgIdx = pkgIdx + 1;
// console.log('0x2,', pieceIdx, that.data.pieceCount, that.data.destPieceCount);
// console.log('t1,',pieceIdx,that.data.pieceCount)
if (pieceIdx >= that.data.pieceCount) {
console.log('已经发送完毕');
const ab200 = new ArrayBuffer(10);
const dv200 = new DataView(ab200);
dv200.setUint16(0, 0x0005, true);
dv200.setUint32(2, 4, true);
dv200.setUint32(6, pkgIdx, true);
cu.send(that.data.destIp, that.data.destPort, ab200);
} else {
// 复制内容
// console.log('t2,',bufidx,that.data.cacheSize)
const ab201 = new ArrayBuffer(UdpPacketSize);
const dv201 = new DataView(ab201);
dv201.setUint16(0, 0x0003, true);
dv201.setUint32(2, UdpPacketSize, true);
dv201.setUint32(6, pkgIdx, true);
dv201.setUint32(10, 0, true);
utils.copyBuffer(ab201, 14, sbuf, sbufIdx, sbufIdx + UdpContentSize);
sbufIdx = sbufIdx + UdpContentSize;
cu.send(that.data.destIp, that.data.destPort, ab201);
// 计数
pieceIdx = pieceIdx + 1;
let pi = Math.ceil(pieceIdx / that.data.pieceCount * 100);
that.setData({
pi: pi,
})
}
break;
case 0x0003:
// 计数
destPieceIdx = destPieceIdx + 1;
let pi = Math.ceil(destPieceIdx / that.data.destPieceCount * 100);
that.setData({
pi: pi,
})
// get data
pkgIdx = mdv.getUint32(6, true);
utils.copyBuffer(rbuf, rbufIdx, msg, 14, UdpPacketSize);
rbufIdx = rbufIdx + UdpContentSize;
// send resp
const ab300 = new ArrayBuffer(10);
const dv300 = new DataView(ab300);
dv300.setUint16(0, 0x0002, true);
dv300.setUint32(2, 4, true);
dv300.setUint32(6, pkgIdx, true);
cu.send(that.data.destIp, that.data.destPort, ab300);
break;
case 0x0004:
break;
case 0x0005:
that.save2File();
break;
}
},
// 发送文件
sendFile() {
const that = this;
const mfilesize = that.data.fileBlockSize;
let ip = that.data.destIp;
let port = that.data.destPort;
if (utils.isEmpty(that.data.files)) {
wx.showToast({
title: '请选择文件',
})
return
}
rbufidx = 0;
rbuf = that.getFileContent();
console.log('rbuf,', rbuf.byteLength);
const ab1000 = new ArrayBuffer(UdpPacketSize + 1);
const uintArr = new Uint8Array(ab1000);
uintArr[0] = 0x1;
utils.copyBuffer(ab1000, 1, rbuf, rbufidx, rbufidx + UdpPacketSize);
// const dv3 = new DataView(ab1000);
// dv3.setUint8(0, 0x1);
// 更新进度条
smc = smc + UdpPacketSize;
let pi = Math.ceil(smc / mfilesize * 100);
console.log('b3,', smc, pi);
that.setData({
pi: pi,
})
cu.send(ip, port, ab1000);
console.log('send start,', ip, port, );
},
// ====================UDP 代码片段 =========================//
// ====================Websocket 代码片段 ===================//
// 创建连接
wsConn(res) {
console.log('ws conn,', res)
const that = this;
that.doWsMakePair();
},
// 关闭连接
wsClose(res) {
console.log('ws close,', res)
const that = this;
that.setData({
isPair: false,
tips: '',
})
},
// 发生错误
wsErr(res) {
console.log('ws err,', res)
const that = this;
that.setData({
isPair: false,
tips: '',
})
},
// 接收消息
wsMsg(res) {
console.log('ws msg,', res)
const that = this;
const msg = JSON.parse(res.data);
console.log(msg);
switch (msg.msgType) {
case 100:
that.setData({
myId: msg.content,
})
console.log('myId,', that.data.myId);
break;
case 102:
let udpContent = msg.content;
let udpInfoArr = utils.str2arr3(udpContent);
that.setData({
destId: msg.fromId,
destIp: udpInfoArr[0],
destPort: Number(udpInfoArr[1]),
tips: '与' + msg.fromId + '配对成功',
isLoading: false,
isPair: true,
})
// TODO 关闭WebSocket连接
break;
case 501:
// 用户文件信息
const content = msg.content;
console.log('501,', content)
const obj = JSON.parse(content)
that.setData({
destIp: obj.udpAddr,
destPort: obj.udpPort,
destFileSize: obj.fileSize,
destFileType: obj.fileType,
destFileBlockCount: obj.fileBlockCount,
destFileBlockSize: obj.fileBlockSize,
})
// 发送502信息,携带UDP地址
const mc = {
"udpAddr": that.data.ip,
"udpPort": that.data.port,
}
const jmc = JSON.stringify(mc);
console.log('502 content,', jmc)
let vmsg = {
"msgId": utils.uuid(),
"fromId": that.data.myId,
"destId": that.data.destId,
"cmdType": 2,
"msgType": 502,
"content": jmc,
"sendTime": utils.getMilliTimeStamp(),
}
cws.send(vmsg)
console.log('502消息发送成功')
break;
case 502:
const mc502 = msg.content;
console.log('502 debug,', mc502)
const obj502 = JSON.parse(mc502)
that.setData({
destIp: obj502.udpAddr,
destPort: obj502.udpPort,
})
// 开始发送文件
that.sendFile();
break;
default:
console.log(msg);
}
},
// 创建配对
doWsMakePair() {
const that = this;
// 创建udp监听,然后发送给服务端。带上自己的UDP地址信息。
// 创建本地监听接口
cu = new MyUdp();
cu.setup(that.uMsg, that.uErr);
let port = cu.port;
console.log('local udp,', that.data.myIp, port);
that.setData({
myPort: port,
})
let obj = {
"secret": that.data.secret,
"ip": that.data.myIp,
"port": port.toString(),
}
let objStr = JSON.stringify(obj);
let msg = {
"msgId": utils.uuid(),
"fromId": "",
"destId": "",
"cmdType": 1,
"msgType": 101,
"content": objStr,
"sendTime": utils.getMilliTimeStamp(),
}
cws.send(msg);
},
// 连接服务
doConnServer() {
const that = this;
wx.getNetworkType({
success(res) {
const networkType = res.networkType
// 判断是否WiFi
if (networkType !== 'wifi') {
wx.showToast({
title: '请打开WIFI',
})
return
}
// 获取本机地址
wx.getLocalIPAddress({
success(res) {
console.log('本地IP,', res)
that.setData({
myIp: res.localip,
})
// 弹出密令
wx.showModal({
title: '密令',
editable: true,
placeholderText: '请输入约好的密令',
success(res) {
if (res.confirm) {
console.log(res.content);
if (utils.isEmpty(res.content)) {
wx.showToast({
title: '密令不能为空',
})
return
} else {
that.setData({
secret: res.content,
isLoading: true,
})
that.connWs();
that.startTimeout();
}
}
}
})
}
})
}
});
},
// 连接websocket
connWs() {
const that = this;
const url = "wss://domain.com/ws?uid=" + that.data.openid;
cws = new MyWs(url)
cws.setup(that.wsErr, that.wsConn, that.wsMsg, that.wsClose)
},
// 启动超时定时器
startTimeout() {
const that = this;
tt = setTimeout(function () {
console.log('30秒超时函数调用');
clearTimeout(tt)
if (!that.data.isPair) {
if (!utils.isEmpty(cws)) {
console.log('timeout close websecket')
cws.close(); // 关闭
cws = null; // ws 类
}
that.setData({
isLoading: false,
isPair: false,
})
}
}, 30000);
},
// 传输文件,先传送文件信息,再传输分块数据。收到对方回应后,开始传输文件。
doTransfile() {
const that = this;
// 判断是否建立配对
if (!that.data.isPair) {
wx.showToast({
title: '请先建立配对',
})
return
}
// 判断视频文件是否选择
if (that.data.fileSize == 0) {
wx.showToast({
title: '请选择视频文件',
})
return
}
// 使用UDP发送文件消息
sbufIdx = 0;
sbuf = new ArrayBuffer(that.data.cacheSize);
const fileContent = that.getFileContent();
utils.copyBuffer(sbuf, 0, fileContent, 0, that.data.fileSize);
// u32 文件大小 u32 缓存大小 u32 文件类型 u32 切片数量
pkgIdx = 1;
let fileTypeCode = utils.getFileTypeCode(that.data.fileType);
let ab = new ArrayBuffer(30);
let dv = new DataView(ab);
dv.setUint16(0, 0x0001, true);
dv.setUint32(2, 24, true);
dv.setUint32(6, pkgIdx, true);
dv.setUint32(10, 0, true);
dv.setUint32(14, that.data.fileSize, true);
dv.setUint32(18, that.data.cacheSize, true);
dv.setUint32(22, fileTypeCode, true);
dv.setUint32(26, that.data.pieceCount, true);
// console.log('ab,', ab, ab.byteLength);
cu.send(that.data.destIp, that.data.destPort, ab);
console.log('send start');
},
// ====================存到相册 代码片段 ===================//
doSaveFile() {
const that = this;
wx.saveVideoToPhotosAlbum({
filePath: that.data.locfilepath,
success(res) {
console.log('save file ok')
wx.showToast({
title: '保存成功',
})
}
})
},
})