Web 自定义协议实践
自定义协议可用于在网页打开桌面应用程序。自定义协议有别于常见的 http、https、ftp 等协议,它是由开发者自己定义的。
协议注册
在 Windows 中,要注册一个自定义协议,需要在注册表中添加一些项目:
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Classes\myprotocol]
@="URL:myprotocol"
"URL Protocol"=""
[HKEY_CURRENT_USER\Software\Classes\myprotocol\shell]
[HKEY_CURRENT_USER\Software\Classes\myprotocol\shell\open]
[HKEY_CURRENT_USER\Software\Classes\myprotocol\shell\open\command]
@="\"你的程序绝对路径\" \"%1\""
在上面的代码中,myprotocol
是自定义的协议名,你可以自己定义。你的程序绝对路径
是你的程序的绝对路径,%1
是网页传递过来的参数。比如网页的链接是 myprotocol://hello
,那么 你的程序绝对路径
就会接收到 myprotocol://hello
这个参数。
这样,当网页中有一个链接是 myprotocol://hello
时,点击这个链接就会打开你的程序,并传递 myprotocol://hello
这个参数。
协议检测
在网页中,可以使用 a
标签来定义一个自定义协议的链接,点击就会打开你的程序并传递参数:
<a href="myprotocol://hello">打开我的程序</a>
但是,如果你还没有定义这个协议,点击这个链接是没有任何效果的。那么我们就可以使用 custom-protocol-check 这个库来检测是否已经定义了这个协议。
import customProtocolCheck from "custom-protocol-check";
customProtocolCheck(
"myprotocol://params",
() => {
if(!window.confirm("XXXXXX is not installed. Do you want to install it?")) return;
window.open('https://example.com/XXXXXX.exe', '_blank');
},
() => {
console.log("Custom protocol found and opened the file successfully.");
}, 5000
);
这样,当用户点击这个链接时,如果没有定义这个协议,就会弹出一个提示框,询问用户是否要下载你的程序。
至此,我们就完成了一个自定义协议的定义和使用。
那么,如果我们想要更多高级的工程化的功能,启动的应用程序应该如何和 Web 页面进行配合呢?
下面,我们将通过一个 C# WinForm 实例,介绍自定义协议的 Web 与桌面应用程序配合的最佳实践。
Web 与 App 通信
启动应用程序后,应用程序可以通过 Web 的协议链接,获取到 Web 传递过来的参数,但是这样的通信是单向的,如果我们希望 Web 页面和应用程序之间进行双向通信,就需要使用 WebSocket。
我们可以在应用程序内启动一个 WebSocket 服务,然后在 Web 页面中通过 WebSocket 连接到这个服务,这样就可以实现双向通信。
我们可以使用 Fleck 来实现 Websocket Server,这里是 示例代码 。
server = new WebSocketServer("ws://127.0.0.1:" + Port);
server.Start(socket =>
{
socket.OnOpen = () => Logger.Log.Info("WebSocket Open!");
socket.OnClose = () => Logger.Log.Info("WebSocket Close!");
socket.OnError = (err) => Logger.Log.Info("WebSocket Error: " + err.Message);
socket.OnMessage = (message => {
object msg;
try
{
msg = new JavaScriptSerializer().DeserializeObject(message);
}
catch (Exception)
{
msg = message;
}
action(new MessageCall()
{
type = WebSocketMessageType.Text,
message = msg,
client = socket,
});
});
socket.OnBinary = (message => {
action(new MessageCall()
{
type = WebSocketMessageType.Binary,
binary = message,
client = socket,
});
});
});
这里监听了 WebSocket 的字符串消息和二进制消息,然后通过 action
回调函数将消息数据向外传递。其中 client 就是连接的客户端,可以通过 client 发送消息。
而 Web 那边,我们可以使用 WebSocket 连接到这个服务,示例代码:
function connectSocket(onMessage, port) {
return new Promise((resolve) => {
const ws = new WebSocket(`ws://localhost:${port}`);
ws.onopen = () => {
console.log("Socket is connected.");
ws.onmessage = (message) => {
onMessage(message.data);
};
ws.onclose = () => {
console.log("Socket is closed.");
}
resolve(ws);
};
ws.onclose = () => {
// retry after 1s
setTimeout(() => connectSocket(onMessage, port), 1000);
};
});
}
因为启动程序是需要时间的,我们无法确定合适程序完成启动,所以这里使用了 setTimeout 来重试连接,如果连接失败,就会在 1s 后重试。
这样,就建立了一个 Web 页面和应用程序之间的双向通信信道。
通信协议
有了通信信道,我们就可以定义一套通信协议,用于 Web 页面和应用程序之间的通信。比如使用 JSON,定义信息结构如下:
{
"command": "download",
"data": {
"url": "https://file.fishpi.cn/2023/02/blob-e6f2be62.png",
"local": "C:\\Protocol"
}
}
用 command
字段来表示命令,data
字段来表示数据。这样,我们就可以在 Web 页面中发送一个下载文件的命令,应用程序就可以接收到这个命令,并执行下载文件到指定的目录。(没错,就是 @xiaoaonan 上次收到的奇葩用户需求。
然后,应用程序那边,就可以通过 WebSocket 来接收这个命令,然后执行下载文件的操作,示例代码:
// 启动 WebSocket 服务
private void ServiceStart(int Port)
{
Logger.Log.Info("Start WebSocket in Port " + Port);
WebSocketService.Start(call =>
{
try
{
wsClient = call.client;
if (call.type == WebSocketMessageType.Text)
{
MessageExecute((Dictionary<string, object>)call.message);
}
}
catch (Exception ex)
{
Logger.Log.Fatal("Websocket Message Error: " + ex.Message);
throw ex;
}
}, Port.ToString());
}
// 执行命令
private void MessageExecute(Dictionary<string, object> message)
{
string command = message["command"] as string;
object data = message.Keys.Contains("data") ? message["data"] : null;
switch (command)
{
case "Ping":
SendData("Ping", "Pong");
break;
case "Download":
Download((data as Dictionary<string, object>)["url"] as string, (data as Dictionary<string, object>)["local"] as string);
break;
case "Close":
Application.Exit();
break;
}
}
这样,我们就完成了一个自定义协议的 Web 与桌面应用程序的配合。
程序路由
如果我们要更进一步,我们可以在应用程序中定义一套路由系统,用于处理不同的协议地址,比如 myprotocol://form1
就打开一个窗口,myprotocol://form2
就打开另一个窗口。还可以接收 QueryString 参数,比如 myprotocol://form2?port=12345
。
那么,我们就可以在程序中定义一个路由解析器,用于解析协议地址,然后执行对应的操作。这里有一个 示例代码 。具体的实现下回再说,感兴趣的可以先看看代码。
打包发布
最后,我们可以使用 Inno Setup 来打包发布我们的应用程序,这是一个免费的 Windows 安装程序制作工具,可以用于打包发布我们的应用程序。
这里是一个 示例脚本
然后,你可以在这里:https://protocol.git.hancel.org/ 查看到 Web 实例。
GitHub 仓库:https://github.com/imlinhanchao/custom-protocol-app
文章首发于:https://my.hancel.org/2024/02/04/web-with-custom-protocol.html
不愧是jump jump big old
不愧是jump jump big old