WebSocket 服务

因软件更新较快,部分文档内容与软件最新版本有所出入,请知悉。

以下内容仅针对于编程爱好者,普通用户可忽略。

概述

Websocket是一种基于HTTP的实时通信协议。参考阮一峰的Websocket教程

Quicker 的 Websocket 服务提供了一个实时通信接口,方便使用者通过自己编写的HTML5网页或者小程序、APP等与Quicker通信,实现特定需求。

功能演示:TODO

Websocket客户端通过局域网直接连接的方式与Quicker通信,相对于推送服务连接方式,不需要经过服务器中转,网络延迟小(可实现类似于触摸板的鼠标控制),可用于传送较大量的数据(如传送文件),也可主动向客户端发送数据。只是websocket接口不能直接通过公网地址访问,使用这个协议也需要一定的专业知识。

设置

在设置窗口 -> 手机APP/WebSocket设置页面中,开启WebSocket服务,并根据需要修改默认端口和验证码。

参数:

启用Websocket服务: 是否开启服务。

端口: 服务端口号,为1-65535之间的数字。

  • 端口号不能其他网络服务冲突。
  • 您需要将Quicker加入到Windows防火墙白名单或者在防火墙设置中开启此端口的访问权限。

验证码: 建立Websocket连接后,经过验证码比对以后才可以进一步通信。可以避免在同一个网络中有多位Quicker用户时连错电脑。

启用安全连接(wss): 是否启用wss连接。类似于https相对于httpwss相对于ws是一种加密的安全连接方式。

如何建立安全连接

浏览器中,以http方式访问的网页中只能建立非加密websocket连接(ws),以https方式访问的网页中只能建立加密的weboskcet连接(wss)。如果以 http 方式访问网页,一些 Javascript 脚本的权限会受到限制(如读取或写入剪贴板、访问摄像头等),为避免这些问题的出现,建议您使用 https 方式访问网页,同时以安全连接 wss 方式建立websocket连接。

使用安全连接方式时,您可以使用wss://转换后的本机ip地址.lan.quicker.cc:端口号/ws访问Quicker的Websocket服务(末尾的ws表示服务网址路径)。其中ip地址部分为您电脑的局域网ip地址中的点.替换为英文短横线-后的字符串。如电脑的局域网ip地址为192.168.1.56时,对应的websocket服务URI为:wss://192-168-1-56.lan.quicker.cc:668/ws

注:这里的域名只是ip地址的别名。就像不同的局域网会有相同的192.168.*.*的内网地址,您并不能通过这个域名访问到别人局域网里的ip,别人也不能通过这个域名访问到您的电脑。(域名解析是需要联网的,除非您在内网架设域名解析服务)

当不使用安全连接时,可以使用ws://ip地址:端口/ws的方式访问Quicker的Websocket服务,如ws://192.168.1.56:668/ws

内置的web服务

如果您编写了客户端网页,可以放置在我的文档\Quicker\_websocket\文件夹下,即可通过与websocket相同端口的网址访问(放置后需重启Quicker或websocket服务),如https://192-168-1-56.lan.quicker.cc:668/index.html

通信协议


通过文本格式向Quicker发送请求并获取响应。请求和响应都使用Json格式,消息参数与推送服务接近。

服务端和客户端:

  • 常开服务等待连接的是服务端,这里就是Quicker软件本体了。
  • 主动发起连接请求的网页、APP、小程序等。

通信过程:

  • 客户端发起Websocket连接。
  • 如果设置了验证码,客户端首先发送验证码消息。
    • 返回验证结果。
  • 如果未设置验证码,服务端(Quicker软件)直接发送验证成功消息。
  • 连接建立。
  • 双方根据需要发送消息和返回结果。

请求地址

非安全连接时:ws://电脑ip地址:设置的端口号/ws

安全连接时:wss://192-168-1-156.lan.quicker.cc/ws

客户 js 示例代码:

let ip = txtIp.value;
let port = txtPort.value;
let _password = txtPassword.value;

let uri = '';
let protocol = '';

// 根据网页是否是https连接,构造连接地址的组成部分
if (isHttps) {
    protocol = 'wss';
    var ipstr = ip.replaceAll('.', '-');		// 将ip地址中的.替换为-
    uri = `${ipstr}.lan.quicker.cc:${port}`;
} else {
    protocol = 'ws';
    uri = `${ip}:${port}`;
}

// 构造完整连接地址
uri = `${protocol}://${uri}/ws`;

// 建立连接
try {
    socket = new WebSocket(uri);
} catch (e) {
    console.log('connect to websocket failed.', e);
    showError(e.message);
    return;
}

setStateContent('连接中...');

同时,客户端开始监听websocket的各种事件:

  • open:连接建立
  • error:发生错误
  • close:连接断开
  • message:收到消息


消息结构


常规请求消息

{
	messageType: 消息类型常量,
  serial: 消息编号,
  operation: '操作类型,如copy将data参数内容写入剪贴板',
  data: '数据,为文本,也可能为对象',
  action: '操作类型为action时指定执行的动作名称或id',
  extData: '可选的额外数据,文本或对象',
  wait: 是否等待操作返回结果
}

参数在不必要的时候可以省略。

常规响应消息

{
	messageType: 消息类型常量,
  serial: 消息编号,
  replyTo: 响应的请求消息编号,
  isSuccess: 操作是否成功,
  message: 操作失败时的提示消息,
  data: 可选的返回数据(文本或对象),
  extData: 可选的额外返回数据(文本或对象)
}

消息类型常量

对应于消息中messageType参数的取值。

  • 2:命令请求消息,用于发送操作指令和内容。
  • 4:命令响应消息,返回指令操作结果。
  • 5:身份验证请求,客户端发送验证码。
  • 6:身份验证响应,返回密码是否正确。


身份验证

连接建立后,开始身份验证流程。

如果Quicker中未设置连接验证码(仅在家庭等安全环境中使用),则直接下发验证通过消息。

// Quicker 发送给客户端的验证通过消息
{
	messageType: 6,
  isSuccess: true, // isSuccess表示是否验证通过
  replyTo: 0
}

如果设置了连接验证码,则需要客户端首先发送身份验证消息,服务端(Quicker软件)返回验证结果。

客户端示例代码(连接建立后主动发送验证请求消息):

// 监听websocket连接事件,连接后如果设置了密码则发送请求消息
socket.addEventListener('open', function(event) {    
    setStateContent('<font color=green>已连接,待认证</font>');

    // 设置了密码,要发送密码消息
    if (_password) {
        sendMsg({
            messageType: 5,			// 消息类型为5
            data: _password,			// data中放置验证密码
            serial: getSerial()
        });
    }
});

Quicker收到消息后对比验证码,返回结果。

{
	messageType: 6,	 // 验证响应消息类型
  isSuccess: false, // isSuccess表示是否验证通过
  replyTo: 1,
  message: '验证码不正确'
}

上行消息

这里指客户端发起请求,Quicker进行执行和响应的消息。

大部分与推送服务中支持的请求类型一致,只额外增加了传送文件支持。

请求消息

{
  "messageType":2,
  "serial": 1000,
  "operation":"paste",
  "data":"Hello Quicker!Quicker真好玩!哈哈😄",
  "action":"动作名或ID",
  "wait":false
}

参数说明

  • messageType:消息类型标识,请求消息为2.
  • serial:请求编号(不强制编号,可以直接写0)。
  • operation: 操作类型,请参考推送服务文档。除推送中的操作类型,另外支持sendfile(传送文件)、pasteimage(粘贴图片到当前窗口)操作,详见后续章节说明。
  • data:操作参数数据。
  • action:操作类型为action时,指定动作的id或名称。请参考推送请求消息格式。
  • wait:是否等待动作响应。


向Quicker传输文件

相对于推送服务,websocket直连不受带宽限制,可以用于传送文件(其他设备传送到Quicker)。具体协议如下:

  • 通过两条消息传送文件。
  • 第一条:文本消息,告知下一步要传送文件,以及文件的名称。
  • 第二条:二进制消息,发送文件内容。


文本消息格式:

{
    "messageType":2,
  	"serial": 1000,
		"operation":"sendfile",		// sendfile 表示要传送文件
    "data":"文件名.png"				// 文件名
}

二进制消息:为文件内容字节数组。js参考代码:

// 发送文件
function sendFile() {
    var file = document.getElementById('theFile').files[0];

    if (!file) {
        console.log('没有文件。');
        return
    }
    if (file.size > 200000000) {
        alert('File should be smaller than 200MB')
        return
    }

    var filename = file.name;
    console.log('file name:', filename);

    // 发送第一条消息,传送文件名
    var msg = {
        messageType: 2,
        operation: 'sendfile',
        serial: msgSerial++,
        data: filename
    };

    socket.send(JSON.stringify(msg));

    // 发送第二条:二进制消息,传输文件数据
    var reader = new FileReader();
    var rawData = new ArrayBuffer();
    reader.loadend = function () {
    }
    reader.onload = function (e) {
        rawData = e.target.result;
        socket.send(rawData);

        console.log("文件已发送");
    }
    reader.readAsArrayBuffer(file);
}

响应消息格式

{
  "MessageType":4,
  "ReplyTo":0,
  "IsSuccess":true,
  "Data":"D:\\Docs\\Quicker\\_recv\\20220113_021127521_quicker.bin",
  "Message":"ok"
}

参数:

  • MessageType,固定为4.
  • ReplyTo:响应的哪一条消息的Serial值。
  • IsSuccess:是否成功响应。
  • Data:返回的数据。
  • Message:错误消息。

下行消息

Quicker组合动作增加了 Websocket 模块,可用于向连接的客户端发送消息。

  • 向客户端发送文本消息:按规定的消息格式发送json文本内容。这里也可以通过表达式创建匿名对象,Quicker会自动序列化为文本。
  • 向客户端发送文件(二进制方式):使用与“上行消息”中说明的两个消息一致的方式发送文件。(第一个消息发送文件名,第二个消息为二进制方式发送文件内容)。这种方式在iOS的浏览器中不支持。
  • 向客户端发送文件(Base64方式):将文件内容序列化为base64字符串后放入extData参数中一起发送。可支持iOS,只是传送的数据略大一些。

客户端消息处理参考代码:

var msg = JSON.parse(data);

// 密码验证结果消息,MSG_AUTH_RESP = 6
if (msg.messageType == MSG_AUTH_RESP) {
    if (msg.isSuccess) {
        setStateContent('<font color=green>已连接</font>');
        document.getElementById('tests').classList.remove('d-none');
    } else {
        setStateContent('<font color=red>认证失败</font>');
    }
} else if (msg.messageType === MSG_PUSH) {  // MSG_PUSH = 2,表示常规命令消息
    if (msg.operation === 'sendfile') {
        if (!msg.extData) {
            // 消息中没有extData,表示是通过二进制方式发送的文件内容,
            // 需要等下一个消息接收二进制数据
            _nextFileName = msg.data;
            console.log('next file name:', _nextFileName);
        } else {
            // 有extData,它是文件内容的base64转换。
            let blob = base64toBlob(msg.extData);
            SaveBlobAs(blob, msg.data); // msg.data 为文件名。
        }

    }
    else if (msg.operation == 'copy') {
        // copy 命令,将data内容写入剪贴板。
        document.getElementById('txtData').value = msg.data;
        writeClipboard(msg.data);
    }
}


反馈与讨论

语雀在语雀上查看