概述
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
相对于http
,wss
相对于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',
extraData: '可选的额外数据,文本或对象',
wait: 是否等待操作返回结果
}
参数在不必要的时候可以省略。
常规响应消息
{
messageType: 消息类型常量,
serial: 消息编号,
replyTo: 响应的请求消息编号,
isSuccess: 操作是否成功,
message: 操作失败时的提示消息,
data: 可选的返回数据(文本或对象),
extraData: 可选的额外返回数据(文本或对象)
}
消息类型常量
对应于消息中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);
}
}