通过HTML应用远程控制Win32程序

在2010年,当构想出功能齐全的视频和音频序列器Turbo Play时,通过移动设备远程控制Windows应用程序的想法还不是很普遍。现在,这已成为许多音频和视频相关应用程序的标准功能。然而,连续的GET和POST请求对HTTP服务器的开销太大,尤其是对于实时应用程序来说。本文将展示如何使用文档不足的WebSocketWin32API来实现更快的控制方法。

创建Web服务器

需要两个套接字,一个用于HTTP请求,一个用于WebSocket。在webinterface.cpp中有四个变量:

int Port = 12345; int WebSocketPort = 12347; std::string host4 = ""; std::string host6 = "";

有一个PickIP()函数会扫描所有接口(使用GetAdaptersAddresses())并将host4和host6设置为第一个IP(稍后用于mDNS发现),但也可以硬编码它们。监听第一个端口用于Web服务器是一个标准的WinSock Bind和Listen。当建立连接后,回复浏览器传递一个HTML文档(仓库中的1.html作为示例),其中包含WebSocket连接代码:

void WebServerThread(XSOCKET y) { std::vector b(10000); std::vector 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); std::string host; bool v6 = 0; for (auto& h : c.GetHeaders()) { if (h.Left() == "Host") { host = h.Right(); std::vector 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文档包含WebSocket代码:

<script> var socket = new WebSocket("%s"); socket.onopen = function(e) { $("#live").html("Connected"); $("#messagex").show(); }; socket.onerror = function(e) { $("#messagex").hide(); $("#live").html("Disconnected"); } 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请求发起时切换协议。Win32WebSocket API的好处是它与连接无关。这意味着提供了浏览器发送给数据,它返回必须回复的数据,而不需要知道将如何回复(TCP、TLS等)。代码中的WS类包含简单的WebSocket函数:

HRESULT Init() { return WebSocketCreateServerHandle(NULL, 0, &h); }

一旦得到一个句柄,就可以像以前一样接受WebSocket连接:

void WebSocketThread(XSOCKET s) { std::vector r1(10000); for (;;) { int rv = s.receive(r1.data(), 10000); if (rv == 0 || rv == -1) break; std::vector 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 tosend; if (FAILED(ws2.PerformHandshake(h1.data(), (ULONG)h1.size(), tosend))) break; }

这个PerformHandshare调用WebSocketBeginServerHandshake,带有从浏览器接收的所有头部,并返回必须发送到浏览器以启动WebSocket协议的头部。这也必须以"HTTP/1.1 101 Switching Protocols\r\n"消息开始,通知浏览器将成功切换。一旦完成,现在可以发送和接收消息。循环以接收消息:

std::vector 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 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; 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()。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485