局域网传输文件

注意,该功能已经下架。

下架原因:没有适用场景,性能差。

实现原理:两台手机同时打开小程序,创建配对,然后选择视频文件,切割视频文件为固定块,然后传输给对端。创建配对时,使用 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: '保存成功', }) } }) }, })