webSocket实现全自动投简历!

4,594 阅读9分钟

一、屌炸天效果

1、网站效果

guoshao-service.test.upcdn.net/file-path/v…

2、小程序效果

guoshao-service.test.upcdn.net/file-path/v…

二、实现思路

要实现云手机四端的通讯【设备客户端+设备服务端+网站/小程序客户端+网站/小程序服务端】,流程是用户操作屏幕=>传递像素点和指令类型给网站/小程序服务端=>网站/小程序服务端里面去执行对应的设备客户端的send方法,去通知设备服务端触发指令,从而实现云手机应用、滑动屏幕等功能。

三、准备工作

1、准备一台二手iphone7 13系统的手机【咸鱼上230块钱】,然后进行设备越狱【淘宝30块钱解决】,然后安装触动专业版。

下载链接:guoshao-service.test.upcdn.net/file-path/a…

安装方法:zhuanlan.zhihu.com/p/467301188

2、打开触动专业版、启动服务。

微信截图_20221213113617.png

QQ图片20221213113558.png

3、手机需要跟电脑【或者上线后的服务器】在同一个局域网内,所以手机需要连接wifi。

四、代码实现

代码如下,只需要更改ts_screen.prototype.init方法里的最上面两行即可。

前端部分:

<html>

<head>
  <title>跨屏控制</title>
  <link rel="stylesheet" href="https://www.guoshao520.com/my-file-path/css/phone-style.css">
  <script type="text/javascript">
    function ts_screen() {
      this.init();
    }

    /**
     * 初始化
     * @Author   郭少
     * @DateTime 2021-05-29T15:15:42+0800
     */
    ts_screen.prototype.init = function () {
      let ip = "192.160.0.24"; // 手机连接wifi后的ip地址
      this.wsUrl = "ws://192.160.0.95:8866"; // 本地ws服务的地址
      this.params = {
        ip: ip,// 设备ip
        port: "50005",// 截屏端口
        ws_port: "51008",// ws控制端口
        frame_number: 8, // 每秒获取屏幕帧数
        screen_render: ""// 参数为:uninterrupted 不间断渲染屏幕 为空则 一张渲染完再渲染下一张
      };
      this.params.imgScreen = document.querySelector("#imgScreen");
      this.params.iframe = document.querySelector("#iframe");
      // 获取屏幕
      this.request_img();
      // 绑定设备操作
      this.screen_action();
    }

    /**
     * 发送文字
     * @param message 文字
     * @param ip 设备ip
     * @constructor
     */
    ts_screen.prototype.Send_Text = function (type, x, y, auth, ip, state, cb, text) {
      let params = this.params;
      let move_type = 1;
      switch (type) {
        case "down":
          this.params.Mouse_Down = true;
          move_type = 1;
          break;
        case "move":
          if (!this.params.Mouse_Down) {
            move_type = 0;
          } else
            move_type = 2;
          break;
        case "up":
          move_type = 3;
          this.params.Mouse_Down = false;
          break;
        default:
          move_type = type;
          break;
      }
      let that = this;
      if (move_type > 0) {
        x = x * 2;
        y = y * 2;
        params.doSend({ input: { type: move_type, x: x, y: y, state: state || 0, text: text || "" } });
      }
    }

    /**
     *  输入文字加载
     * @Author   郭少
     * @DateTime 2021-05-29T11:36:09+0800
     */
    ts_screen.prototype.init_input_content = function () {
      let params = this.params;
      let imgScreen = params.imgScreen;
      let that = this;
      // 对屏幕操作
      let Mouse_Option = this.Mouse_Option.bind(this);
      let input_content = document.querySelector(".input_content");
      // 鼠标拖动输入框
      let input_content_header = input_content.querySelector(".input_content_header");
      input_content_header.onmousedown = function (ev) {
        input_header_mouse("down", ev);
      }
      document.body.onmousemove = function (ev) {
        input_header_mouse("move", ev);
      }
      input_content_header.onmouseup = function (ev) {
        input_header_mouse("up", ev);
      }

      // 鼠标移动
      let input_header_params = {};
      // 鼠标拖动输入框事件
      let input_header_mouse = function (type, ev) {
        switch (type) {
          case "down":
            input_header_params.down = true;
            // 记录输入框当前所在位置
            input_header_params.clientX = input_content.offsetLeft;
            input_header_params.clientY = input_content.offsetTop;
            // 记录鼠标所在位置
            input_header_params.x = ev.x;
            input_header_params.y = ev.y;
            break;
          case "move":
            if (input_header_params.down) {
              // 计算移动到的位置 原始位置 - (鼠标按下的位置 - 鼠标当前的位置)
              input_content.style.left = input_header_params.clientX - (input_header_params.x - ev.x) + "px";
              input_content.style.top = input_header_params.clientY - (input_header_params.y - ev.y) + "px";
            }
            break;
          case "up":
            input_header_params.down = false;
            break;
        }
      }

      let main = document.querySelector(".main");
      // 点击输入文字按钮显示文本框
      let btnInput = document.querySelector("#btnInput");
      btnInput.onclick = function () {
        input_content.style.display = "block";
        let x = main.offsetLeft + imgScreen.offsetWidth + 20;
        let y = main.offsetTop + main.offsetHeight - input_content.offsetHeight;
        input_content.style.left = x + "px";
        input_content.style.top = y + "px";
      }
      // 隐藏输入文本框
      let close_input = input_content.querySelector("#close_input");
      close_input.onclick = function () {
        input_content.style.display = "none";
      }
      // 点击发送文字到设备
      let btnSend = input_content.querySelector("#btnSend");
      let textarea = input_content.querySelector(".textarea");
      btnSend.onclick = function () {
        Mouse_Option(11, 0, 0, 0, function () { }, textarea.value);
      }
    }

    /**
     * 屏幕动作
     * @Author   郭少
     * @DateTime 2021-05-29T12:40:03+0800
     */
    ts_screen.prototype.screen_action = function () {
      let params = this.params;
      let imgScreen = params.imgScreen;
      let that = this;

      // 首页
      let btnHome = document.querySelector("#btnHome");
      btnHome.onclick = function () {
        Mouse_Option(20, 0, 0, 0);
      }

      // 多任务
      let btnMultitask = document.querySelector("#btnMultitask");
      btnMultitask.onclick = function () {
        Mouse_Option(21, 0, 0, 0);
      }
      // 加载输入文字
      this.init_input_content();

      /**
       * 屏幕加载出错
       * @Author   郭少
       * @DateTime 2021-05-29T12:32:02+0800
       */
      imgScreen.onerror = function (err) {
        console.log("error", err)
      }

      /**
       * 屏幕加载完成
       * @Author   郭少
       * @DateTime 2021-05-29T12:32:27+0800
       */
      imgScreen.onload = function () {
        if (!params.sourceSize ||
          (params.nowSize && params.nowSize.width != this.width) ||
          (params.nowSize && params.nowSize.height != this.height)) {
          let img = document.createElement("img");
          img.src = this.src;
          img.onload = function () {
            params.sourceSize = { width: this.width, height: this.height };
          }
          params.nowSize = { width: this.width, height: this.height };
        }
        if (!params.websocket) {
          that.websocket_connection();
        }
        if (params.screen_render !== "uninterrupted") {
          setTimeout(function () {
            that.request_img();
          }, 1000 / params.frame_number);
        }
      }

      let mousedown = false;
      // 鼠标按下
      imgScreen.onmousedown = function (ev) {
        if (!params.websocket) return;
        Mouse_Option("down", ev.offsetX, ev.offsetY);
        mousedown = true;
      }
      // 鼠标在屏幕拖动
      imgScreen.onmousemove = function (ev) {
        if (!params.websocket) return;
        Mouse_Option("move", ev.offsetX, ev.offsetY);
      }
      // 鼠标离开屏幕
      imgScreen.onmouseout = function (ev) {
        if (!params.websocket) return;
        Mouse_Option("up", ev.offsetX, ev.offsetY);
        mousedown = false;
      }
      // 鼠标在屏幕抬起
      imgScreen.onmouseup = function (ev) {
        if (!params.websocket) return;
        Mouse_Option("up", ev.offsetX, ev.offsetY);
        mousedown = false;
      }
      // 鼠标出现在body
      document.body.onmouseout = function (ev) {
        if (!params.websocket) return;
        Mouse_Option("up", ev.offsetX, ev.offsetY);
        mousedown = false;
      }

      // 对屏幕操作
      let Mouse_Option = this.Mouse_Option.bind(this);
    }

    /**
     * 对屏幕操作
     * @Author   郭少
     * @DateTime 2021-05-29T12:45:31+0800
     */
    ts_screen.prototype.Mouse_Option = function (type, x, y, state, cb, text = this.params.ip) {
      let params = this.params;
      let move_type = 1;
      switch (type) {
        case "down":// 鼠标按下
          params.Mouse_Down = true;
          move_type = 1;
          break;
        case "move":// 鼠标在屏幕移动
          if (!params.Mouse_Down) {
            move_type = 0;
          } else
            move_type = 2;
          break;
        case "up":
          move_type = 3;
          params.Mouse_Down = false;
          break;
        default:
          move_type = type;
          break;
      }
      let that = this;
      if (move_type > 0) {
        if (!params.sourceSize) return;
        // 通过获取到的原始屏幕大小计算出当前鼠标所在屏幕位置
        let w_b = x / params.nowSize.width;
        let h_b = y / params.nowSize.height;
        let nowX = params.sourceSize.width * (w_b);
        let nowY = params.sourceSize.height * (h_b);

        if (typeof (type) == "number") {
          params.iframe.src = "http://" + params.ip + ":" + params.port + "/event?type=" + move_type + "&state=" + (state || 0) +
            "&text=" + encodeURIComponent(text || "");
        } else {
          // 往设备发送拖动屏幕请求
          params.doSend({ input: { type: move_type, x: nowX, y: nowY, state: state || 0, text: text || "" } });
          if (cb) {
            setTimeout(function () {
              cb();
            }, 100);
          }
        }
      }
    }

    /**
     * 获取屏幕
     * @Author   郭少
     * @DateTime 2021-05-29T12:31:32+0800
     */
    ts_screen.prototype.request_img = function () {
      let params = this.params;
      imgScreen.src = "http://" + params.ip + ":" + params.port + "/snapshot1?ext=jpg&orient=0&compress=0.00001&scale=1&t-" + new Date().getTime();
      if (params.screen_render === "uninterrupted") {
        let that = this;
        setTimeout(function () {
          that.request_img();
        }, 1000 / params.frame_number);
      }
    }

    ts_screen.prototype.heartCheck = function (ws) {
      return {
        timeout: 1000 * 10,// 60ms
        timeoutObj: null,
        serverTimeoutObj: null,
        reset: function () {
          clearTimeout(this.timeoutObj);
          clearTimeout(this.serverTimeoutObj);
          this.start();
        },
        start: function () {
          let self = this;
          this.timeoutObj = setTimeout(function () {
            self.serverTimeoutObj = setTimeout(function () {
              ws.close();// 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
            }, self.timeout)
          }, this.timeout)
        },
      }
    }

    /**
     * websocket连接
     * @Author   郭少
     * @DateTime 2021-05-29T12:33:11+0800
     */
    ts_screen.prototype.websocket_connection = function () {
      let params = this.params;
      let wsUri = this.wsUrl;
      let that = this;
      let WebSocket_start = function () {
        let websocket = new WebSocket(wsUri);
        let heartCheck = that.heartCheck(websocket);
        websocket.onopen = function (evt) {
          onOpen(evt)
        };
        websocket.onclose = function (evt) {
          onClose(evt)
        };
        websocket.onmessage = function (evt) {
          onMessage(evt)
        };
        websocket.onerror = function (evt) {
          onError(evt)
        };
        params.websocket = websocket;
      }

      let onOpen = function (evt) {
        writeToScreen("CONNECTED");
      }

      let onClose = function (evt) {
        writeToScreen("DISCONNECTED");
        setTimeout(function () {
          WebSocket_start();
        }, 3000);
      }

      let onMessage = function (evt) {
        writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data + '</span>');
        websocket.close();
      }

      let onError = function (evt) {
        writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
      }

      params.doSend = function (message) {
        message = JSON.stringify(message);
        writeToScreen("SENT: " + message);
        if (params.websocket.readyState === 1) {
          params.websocket.send(message);
        }
      }
      let writeToScreen = function (message) {
        // console.log(message)
      }
      WebSocket_start();
    }

    let load = function () {
      new ts_screen();
    }

  </script>
</head>

<body class="valign" onload="load()">
  <div class="main">
    <img draggable="false" id="imgScreen" class="screen">
    <iframe id="iframe" src=""></iframe>
    <div class="btnOption valign">
      <button id="btnHome" class="valign">
        <span>
          <i class="icon"></i>
          <label>首页</label>
        </span>
      </button>
      <button id="btnMultitask" class="valign">
        <span>
          <i class="icon"></i>
          <label>多任务</label>
        </span>
      </button>
      <button id="btnInput" class="valign">
        <span>
          <i class="icon"></i>
          <label>输入</label>
        </span>
      </button>
    </div>
    <div class="input_content">
      <div class="input_content_header">
        <label>发送文字</label>
      </div>
      <div class="input_content_body">
        <label class="tips">
          <span class="tips_icon"></span>
          <span>请保证设备页面处于可输入状态</span>
        </label>
        <input type="text" class="textarea">
      </div>
      <div class="input_content_footer valign">
        <button id="btnSend">发送</button>
        <button id="close_input">取消</button>
      </div>
    </div>
  </div>
</body>

</html>

后端部分:

实现:建立一个webSocket文件夹,然后下面新建一个index.js和一个modules文件夹,index.js用来启动服务,modules文件夹里面分别放2个端的对应文件【设备客户端phoneClient.js、网站/小程序服务端webServer.js】、初始化服务文件socketInit.js、公用方法文件common.js、配置全局变量文件globalData.js,然后设备服务端是一个公网ip的服务【端口51008】,并不是本地服务实现的。

1、phoneClient.js,通过循环创建了n个设备的客户端对象。然后分别去连接服务器(wx:ip地址:51008)。并将这些对象保存在全局变量connectionObj里面,通过ip去标识key值。

2、webServer.js,在text方法里面,接受消息里面的设备ip和文本内容,然后调用封装好的doSend方法,传递connectionObj[ip]和文本内容,触发设备客户端的send方法,发送给设备服务端去打开云手机应用、滑动屏幕等。

3、socketInit.js,引入phoneClient.js和webServer.js,先获取所有的设备,然后做初始化操作【比如:设置全局变量、检测断开重连等(根据测试发现,如果设备一直在操作的话,基本不会出现断开的情况。很长时间不操作会断开,所以检测某台设备如果10分钟都没点击,就进行重连操作)】

4、在node项目入口引入webSocket文件夹。然后cnpm i websocket => cnpm i nodejs-websocket => node index。

下面是代码的实现:

index.js文件代码如下:

const socInit = require("./modules/socketInit")
// 启动webSocket服务
socInit.webSocketStart()

/modules/phoneClient.js文件代码如下:

const global = require("./globalData")
const { client: WebSocketClient }= require('websocket')

// 遍历设备列表,构建手机的客户端 
async function phone_Init(ip) {
  const ipList = global.get("ipList")
  const connecObj = global.get("connecObj")
  if (ipList.length == 0) return
  try {
    for (let i = 0; i < ipList.length; i++) {
      // 如果有设备ip传过来,就重新连接指定设备,否则就是所有设备连接
      if (ip ? ipList[i] == ip : true) {
        const client = new WebSocketClient(); // 创建客户端对象

        // 连接失败执行
        client.on('connectFailed',
          function (error) {
            console.log('Connect Error: ' + error.toString());
          }
        );

        client.on('connect', function (connection) {
          global.setProp("connecObj", ipList[i], connection) // 存放连接对象
          // connecObj[ipList[i]] = connection 
          console.log('正在启动:' + ipList[i]);

          // 连接错误抛出
          connection.on('error', function (error) {
            console.log("Connection Error: " + error.toString());
          });

          // 连接关闭执行
          connection.on('close', function () {
            console.log('echo-protocol Connection Closed');
          });

          // 收到服务器的返回消息
          connection.on('message', function (message) {
            if (message.type === 'utf8') {
              console.log("Received: '" + message.utf8Data + "'");
            }
          });

        });

        client.connect(`ws://${ipList[i]}:51008/`); // 连接服务器
      }
    }
  } catch (e) {
    console.log("发生异常:" + e)
  }
}

module.exports = {
  phone_Init
}

/modules/webServer.js文件代码如下:

const ws = require('nodejs-websocket')
const global = require("./globalData")
const phoneClient = require("./phoneClient")
const { doSend, isJSON } = require("./common")

async function createWebServer(port = 8866) {
  ws.createServer(function (conn) {
    
    conn.on('text', function (text) {
      const timeObj = global.get("timeObj")
      const connecObj = global.get("connecObj")
      // 重连设备
      if (text.indexOf("rebinding") != -1) {
        let ipText = text.split(":")[1]
        let relloadIp = ipText
          .substring(0, ipText.length - 1)
        console.log("正在重连设备:" + relloadIp)
        phoneClient.phone_Init(relloadIp) // 重连指定设备 
      } else {
        console.log(text)
        if (isJSON(text)) {
          let data = JSON.parse(text)
          if (data.hasOwnProperty("input")) {
            let ip = data["input"]["text"]
            console.log('发送的手机ip:' + ip);
            console.log('收到的信息为:' + text);
            if (timeObj[ip]) {
              if ("isclick" in timeObj[ip]) {
                global.setProp("timeObj", "ip", true, "isclick")
              }
            }
            doSend(connecObj[ip], text)
          }
        } else {
          console.log("这里接受到的消息:", text)
        }
      }
    });
  
    conn.on('message', function (code, reason) {
      console.log('传递数据中');
    });
  
    conn.on('close', function (code, reason) {
      console.log('关闭连接');
    });
  
    conn.on('error', function (code, reason) {
      console.log('异常关闭');
    });
  
  }).listen(port);
}
console.log("服务器启动成功")

module.exports = {
  createWebServer
}

/modules/socketInit.js文件代码如下:

在global.set("ipList"...这个地方,设置你自己的手机ip

const http = require('http');
const global = require("./globalData")
const socketCom = require("./common")
const phoneClient = require("./phoneClient")
const webServer = require("./webServer")
const phone_Init = phoneClient.phone_Init
//const PhoneallModel = require('../../../models/PhoneallModel')

async function webSocketStart() {
  //const data = await PhoneallModel.findAllPhoneallList({
  //  pageIndex: 1,
  //  pageSize: 500
  //});
  //const phoneList = []
  //data.rows.forEach(element => {
  //  const phoneIp = 
  //    element.dataValues.phone_ip
  //  phoneList.push(phoneIp)
  //});
 
  // 设置全局变量
  global.set("timeObj", {})
  global.set("connecObj", {})
  global.set("ipList", ["192.160.0.24"]) // 这里可以更改为phoneList
  // 设备初始化操作
  await socketCom.delay(1000)
  await phoneClient.phone_Init()
  await webServer.createWebServer(8866)
  const ipList = global.get("ipList")
  const timeObj = global.get("timeObj")
  // 时间对象初始化
  socketCom.timeObj_DataInit(ipList, timeObj)
  // 检测设备连接状态
  socketCom.timeObj_Examine(ipList, timeObj, phone_Init)
}

module.exports = {
  webSocketStart
}

/modules/common.js文件代码如下:

const global = require("./globalData")

/***
 * 时间方法
 */

// 日期格式转换
Date.prototype.format = function (fmt) {
  var o = {
    "M+": this.getMonth() + 1, //月份 
    "d+": this.getDate(), //日 
    "h+": this.getHours(), //小时 
    "m+": this.getMinutes(), //分 
    "s+": this.getSeconds(), //秒 
    "q+": Math.floor((this.getMonth() + 3) / 3), //季度 
    "S": this.getMilliseconds() //毫秒 
  };
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
  }
  for (var k in o) {
    if (new RegExp("(" + k + ")").test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k])
        .length)));
    }
  }
  return fmt;
}

// 延迟时钟
async function delay(times) {
  return new Promise((reslove, reject) => {
    setTimeout(() => {
      reslove()
    }, times)
  })
}

// 计算时间差
function times(start_time, end_time, type) {
  start_time = new Date(start_time)
  end_time = new Date(end_time)
  switch (type) {
    case "秒":
      return parseInt(end_time - start_time) / 1000;
    case "分":
      return parseInt(end_time - start_time) / 60000;
    case "时":
      return parseInt(end_time - start_time) / 3600000;
  }
}

/***
 * 功能方法
 */

// 发送消息到网页客户端
function doSend(connection, text) {
  if (connection) {
    console.log(connection.connected ? "连接成功!" : "连接失败!")
    if (connection.connected) {
      connection.send(text); //发送数据    
    }
  }
}

// 判断是否为Json数据
function isJSON(str) {
  if (typeof str == 'string') {
    try {
      var obj = JSON.parse(str);
      if (typeof obj == 'object' && obj) {
        return true;
      } else {
        return false;
      }
    } catch (e) {
      console.log('error:' + str + '!!!' + e);
      return false;
    }
  }
  console.log('It is not a string!')
}

/***
 * 状态设置方法
 */

// 时间对象初始化
function timeObj_DataInit(ipList, timeObj) {
  for (let i = 0; i < ipList.length; i++) {
    const startTime = new Date().format("yyyy/MM/dd hh:mm:ss")
    global.setProp("timeObj", ipList[i], {})
    global.setProp("timeObj", ipList[i], false, "isclick")
    global.setProp("timeObj", ipList[i], startTime, "start_time")
    console.log("设备ip:" + ipList[i])
    // console.log("开始时间:" + timeObj[ipList[i]].start_time)
  }
}

// 每10分钟检测一次连接状态
function timeObj_Examine(ipList, timeObj, phone_Init) {
  setInterval(() => {
    const startTime = new Date().format("yyyy/MM/dd hh:mm:ss")
    for (let i = 0; i < ipList.length; i++) {
      let ele_time = new Date().format("yyyy/MM/dd hh:mm:ss");
      // 每10分钟检测一次
      if(timeObj[ipList[i]]) {
        if (times(timeObj[ipList[i]].start_time, ele_time, "分") >= 10) {
          // 如果还未点击过
          if (!timeObj[ipList[i]].isclick) {
            phone_Init(ipList[i])
            console.log(ipList[i] + '正在重连......')
            global.setProp("timeObj", ipList[i], false, "isclick")
            global.setProp("timeObj", ipList[i], startTime, "start_time")
          }
        }
      }
    }
  }, 5000);
}

module.exports = {
  times,
  delay,
  doSend,
  isJSON,
  timeObj_DataInit,
  timeObj_Examine,
}

/modules/globalData.js文件代码如下:

const GLOABL_DATA = {}

function get(key) {
  return GLOABL_DATA[key]
}

function set(key, value) {
  GLOABL_DATA[key] = value
}

function setProp(key, prop, value, childProp) {
  try {
    if (!childProp) {
      GLOABL_DATA[key][prop] = value
    } else {
      GLOABL_DATA[key][prop][childProp] = value
    }
  } catch (error) { }
}

module.exports = {
  get,
  set,
  setProp
}

五、后续补充

现在展示的只是最初始的网页版测试代码,小程序版本的代码太多这里就不展示了。实际后续上线至少需要实现获取屏幕的接口,koa代码如下:

/**
 * 获取设备屏幕
 * @param ctx
 * @returns 设备屏幕数据
 */
static async screen(ctx) {
    let { ip, type = '', text = '' } = ctx.query 

    async function getImg(url) {
        return new Promise((reslove, reject) => {
            http.get(url, function (res) {
                reslove(res)
            })
        })
    }

    try {
        let imgUrl = ""
        // type: 11:发送文字 20:回到首页 21:切换进程
        if (type) {
            imgUrl = "http://" + ip + ":50005/event?type=" + type + "&state=0&text=" + text;
        } else {
            imgUrl = "http://" + ip + ":50005/snapshot1?ext=jpg&orient=0&compress=0.00001&scale=1&t-" + new Date().getTime();
        }
        let data = await getImg(imgUrl)
        ctx.body = data
    } catch (err) {
        ctx.response.status = 500;
        ctx.body = {
            code: 500,
            message: err
        }
   }
}