65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WebSockets 控制 Win32 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (9投票s)

2022 年 5 月 1 日

CPOL

3分钟阅读

viewsIcon

10852

Win32 和浏览器之间使用 WebSockets 进行互操作的快捷方式。

引言

现在 Web 应用程序非常流行,如果能够通过 HTML 应用程序远程控制您的 Win32 应用程序,那将非常有用。

背景

当我设想我现在功能齐全的视频和音频音序器 Turbo Play 的时候(早在 2010 年),很少有人考虑通过移动设备远程控制 Windows 应用程序。现在,这已成为许多音频和视频相关应用程序中的标准功能。

然而,对于 HTTP 服务器连续的 GETPOST 请求,开销会太大,特别是对于实时应用程序。 我在这里展示的是如何使用(文档不完善的) WebSocket Win32 API 来演示一种更快的控制方法。

存储库中包含一个简单的 SOCKET 包装器和我的 MIME 库,能够构建一个小型、快速的 Web 服务器。

创建 Web 服务器

您需要两个套接字,一个用于 HTTP 请求,一个用于 WebSocketwebinterface.cpp 中有四个变量。

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

有一个 PickIP() 函数会扫描您的所有接口 (使用 GetAdaptersAddresses()),并将 host4host6 设置为第一个 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, WebSocketGetActionWebSocketCompleteAction 来解码 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 日:首次发布
© . All rights reserved.