使用 WebSockets 控制 Win32 应用程序






4.71/5 (9投票s)
Win32 和浏览器之间使用 WebSockets 进行互操作的快捷方式。
引言
现在 Web 应用程序非常流行,如果能够通过 HTML 应用程序远程控制您的 Win32 应用程序,那将非常有用。
背景
当我设想我现在功能齐全的视频和音频音序器 Turbo Play 的时候(早在 2010 年),很少有人考虑通过移动设备远程控制 Windows 应用程序。现在,这已成为许多音频和视频相关应用程序中的标准功能。
然而,对于 HTTP 服务器连续的 GET
和 POST
请求,开销会太大,特别是对于实时应用程序。 我在这里展示的是如何使用(文档不完善的) WebSocket Win32 API 来演示一种更快的控制方法。
存储库中包含一个简单的 SOCKET 包装器和我的 MIME 库,能够构建一个小型、快速的 Web 服务器。
创建 Web 服务器
您需要两个套接字,一个用于 HTTP 请求,一个用于 WebSocket
。webinterface.cpp 中有四个变量。
// ------- Variables
int Port = 12345;
int WebSocketPort = 12347;
std::string host4 = "";
std::string host6 = "";
// -----------------
有一个 PickIP()
函数会扫描您的所有接口 (使用 GetAdaptersAddresses()),并将 host4
和 host6
设置为第一个 IP (稍后用于 mDNS 发现),您也可以硬编码它们。
监听 Web 服务器的第一个端口是一个标准的 WinSock
Bind 和 Listen。 当您建立连接时,您会回复浏览器,传递一个包含 websocket 连接代码的 HTML 文档 (存储库中的1.html 作为一个例子)。
void WebServerThread(XSOCKET y)
{
std::vector<char> b(10000);
std::vector<char> b3;
for (;;)
{
b.clear();
b.resize(10000);
int rval = y.receive(b.data(), 10000);
if (rval == 0 || rval == -1)
break;
MIME2::CONTENT c;
c.Parse(b.data(), 1);
// Get /, display 1.html
std::string host;
bool v6 = 0;
for (auto& h : c.GetHeaders())
{
if (h.Left() == "Host")
{
host = h.Right();
std::vector<char> h2(1000);
strcpy_s(h2.data(), 1000, host.c_str());
auto p2 = strstr(h2.data(), "]:");
if (p2)
{
*p2 = 0;
host = h2.data() + 1;
v6 = 1;
break;
}
auto p = strchr(h2.data(), ':');
if (p)
{
*p = 0;
host = h2.data();
}
break;
}
}
const char* m1 = "HTTP/1.1 200 OK\r\nContent-Type:
text/html\r\nConnection: Close\r\n\r\n";
b.clear();
ExtractResource(GetModuleHandle(0), L"D1", L"DATA", b);
b.resize(b.size() + 1);
char* pb = (char*)b.data();
char b2[200] = {};
if (v6)
sprintf_s(b2,200,"ws://[%s]:%i", host.c_str(), WebSocketPort);
else
sprintf_s(b2, 200 , "ws://%s:%i", host.c_str(), WebSocketPort);
b3.resize(b.size() + 1000);
strcat_s(b3.data(), b3.size(), m1);
sprintf_s(b3.data() + strlen(b3.data()), b3.size() -
strlen(b3.data()), pb, b2);
char* pb2 = (char*)b3.data();
y.transmit((char*)pb2, (int)strlen(pb2), true);
y.Close();
}
}
我们必须扫描标头中的 "Host
" 以获取浏览器用于连接到我们的实际主机,然后传递 1.html,其中有一个空格 ("%s
") 来填充 ws://IP:Port
。请记住,在 IPv6 中,您必须使用大括号 []
包裹 IP 地址。
我们传递的 HTML 文档包含,除了我在这里使用的标准 jQuery 和 Bulma 内容之外,还有简单的 websocket
代码。
<script>
var socket = new WebSocket("%s");
socket.onopen = function (e) {
$("#live").html("Connected");
$("#messagex").show();
};
socket.onerror = function (e) {
$("#messagex").hide();
$("#live").html("Disonnected");
}
socket.onclose = function (e) {
$("#live").html("Disconnected");
$("#messagex").hide();
}
socket.onmessage = function (event) {
var e = event.data;
$("#received").html(e);
}
function message() {
msg = prompt("Please say something...", "Hello");
if (msg != null)
socket.send(msg);
}
</script>
这将为连接和错误创建回调,只要消息接收和发送,我们就会从/向 Win32 应用程序发送/接收数据。
创建 WebSocket 服务器
WebSocket
服务器是一个 HTTP 服务器,当 WebSocket
请求启动时切换协议。 Win32 WebSocket
API 的优点在于它是连接独立的。 这意味着您以某种方式提供从浏览器收到的数据,它会返回您必须回复的数据,而无需知道您将如何回复(TCP、TLS 等)。
代码中的 WS
类包含简单的 WebSocket
函数。
// Create a server side websocket handle.
HRESULT Init()
{
return WebSocketCreateServerHandle(NULL, 0, &h);
}
一旦我们获得了一个句柄,我们就可以像之前一样让我们的 websocket
服务器接受一个连接。
void WebSocketThread(XSOCKET s)
{
std::vector<char> r1(10000);
for (;;)
{
int rv = s.receive(r1.data(), 10000);
if (rv == 0 || rv == -1)
break;
std::vector<WEB_SOCKET_HTTP_HEADER> h1;
MIME2::CONTENT c;
c.Parse(r1.data(), 1);
std::string host;
for (auto& h : c.GetHeaders())
{
if (h.IsHTTP())
continue;
WEB_SOCKET_HTTP_HEADER j1;
auto& cleft = h.LeftC();
j1.pcName = (PCHAR)cleft.c_str();
j1.ulNameLength = (ULONG)cleft.length();
auto& cright = h.rights().rawright;
j1.pcValue = (PCHAR)cright.c_str();
j1.ulValueLength = (ULONG)cright.length();
h1.push_back(j1);
}
auto& ws2 = Maps[&s];
if (FAILED(ws2.Init()))
break;
std::vector<char> tosend;
if (FAILED(ws2.PerformHandshake(h1.data(), (ULONG)h1.size(), tosend)))
break;
这个 PerformHandshake
调用 WebSocketBeginServerHandshake, 包含我们从浏览器收到的所有标头,并返回我们必须发送给浏览器的标头以启动 WebSocket
协议。 这还必须以 "HTTP/1.1 101 Switching Protocols\r\n"
消息开始,以通知浏览器我们将成功切换。
完成之后,我们现在可以发送和接收消息。 我们循环接收消息。
std::vector<char> msg;
for (;;)
{
int rv = s.receive(r1.data(), 10000);
if (rv == 0 || rv == -1)
break;
msg.clear();
auto hr = ws2.ReceiveRequest(r1.data(), rv, msg);
if (FAILED(hr))
break;
if (msg.size() == 0)
continue;
msg.resize(msg.size() + 1);
MessageBoxA(hMainWindow, msg.data(), "Message", MB_SYSTEMMODAL | MB_APPLMODAL);
}
一旦我们得到一些字节,我们就将它们传递给 ReceiveRequest()
,然后它调用 WebSocketReceive, WebSocketGetAction 和 WebSocketCompleteAction 来解码 websocket
消息,并返回给我们一个包含实际发送数据的缓冲区。
要发送数据,我们以类似的方式调用 SendRequest()
。
for (auto& m : Maps)
{
std::vector<char> out;
m.second.SendRequest("Hello", 5,out);
m.first->transmit((char*)out.data(),(int)out.size(),1);
}
请注意,我正在保存所有 websocket
服务器的映射以及 WS 结构,以便处理多个连接 - 您必须同步它们。
使用这项技术,我能够为 Turbo Play 创建一个小型(但)Web 控制器。
发现服务
Windows 10+ 还具有 ZeroConf/mDNS 发现 API,因此您可以在 dns-sb
中发布该服务。 主要函数是 DNSServiceRegister,它将发布我们的服务。
rd = {};
rd.pServiceInstance = &di;
rd.unicastEnabled = 0;
di.pszInstanceName = (LPWSTR)L"app._http._tcp.local";
di.pszHostName = (LPWSTR)L"myservice.local";
InetPtonA(AF_INET6, host6.c_str(), (void*)&i6);
di.ip6Address = &i6;
InetPtonA(AF_INET, host4.c_str(), (void*)&i4);
DWORD dword = i4;
// Hey, this IP4_ADDRESS is different than in_addr
DWORD new_dword = (dword & 0x000000ff) << 24 | (dword & 0x0000ff00) << 8 |
(dword & 0x00ff0000) >> 8 | (dword & 0xff000000) >> 24;
i4 = new_dword;
di.ip4Address = &i4;
di.wPort = (WORD)Port;
rd.Version = DNS_QUERY_REQUEST_VERSION1;
rd.pRegisterCompletionCallback = [](DWORD Status,
PVOID pQueryContext,
PDNS_SERVICE_INSTANCE pInstance)
{
DNSRegistration* r = (DNSRegistration*)pQueryContext;
if (pInstance)
DnsServiceFreeInstance(pInstance);
};
rd.pQueryContext = this;
auto err = DnsServiceRegister(&rd, 0);
if (err != DNS_REQUEST_PENDING)
MessageBeep(0);
要终止注册,我们将调用 DnsServiceDeRegister()
。
代码
该代码包含一个小型可执行文件,该文件创建一个 Web 服务器和一个 WebSocket 服务器,供任何浏览器打开,然后允许应用程序发送消息,浏览器接收该消息并回复。 玩得开心!
历史
- 2022 年 5 月 1 日:首次发布