局域网传输文件

注意,该功能已经下架。

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

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