This article shows how to remote-control your win32 application via a web browser.
Introduction
Now that web applications are prevalent, it would seem very useful to be able to remotely control your Win32 application via an HTML application.
Background
When I visualized my now full-featured video and audio sequencer, Turbo Play, back in 2010, little was going over the remote control via a mobile device of a Windows app. Now this is a standard feature found in many audio and video related applications.
The problem however with contiguous GET
S and POST
S to a HTTP server is that the overhead would be too big, especially for a realtime application. What I present here is how to use the (poorly documented) WebSocket Win32 API to demonstrate a faster method of control.
Included in the repository is a simple SOCKET wrapper and my MIME library, capable of building a small, quick web server.
Creating the Web Server
You need two sockets, one for the HTTP request and one for the WebSocket
. There are four variables in webinterface.cpp:
int Port = 12345;
int WebSocketPort = 12347;
std::string host4 = "";
std::string host6 = "";
There is a PickIP()
function that would scan all your interfaces (using GetAdaptersAddresses()) and set the host4
and host6
to the first IPs (to be later used in mDNS discovery) but you can hardcode them as well.
Listening to the first port for the web server is a standard WinSock
Bind and Listen. When you have the connection, you reply to the browser passing a HTML document (1.html in the repository as an example) that would contain websocket-connection code:
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);
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();
}
}
We must scan the headers for the "Host
" to get the actual host the browser used to connect to us, then pass the 1.html which has an empty space ("%s
") to fill in the ws://IP:Port.
Remember that, in IPv6, you must use braces []
to the IP address.
The HTML document we would pass contains, among the standard jQuery and Bulma stuff I used here, the simple websocket
code:
<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>
This creates callbacks for connection and errors, as long as message reception and sending we will send/receive to/from the Win32 application.
Creating the WebSocket Server
A WebSocket
server is a HTTP server that switches protocols when a WebSocket
request initiates. The good thing with the Win32 WebSocket
API is that it's connection-independent. That means that you provide the data you received somehow from the browser and it returns the data you must reply to it, without knowing how you are going to reply (TCP, TLS, etc.).
The class WS
in the code contains simple WebSocket
functions:
HRESULT Init()
{
return WebSocketCreateServerHandle(NULL, 0, &h);
}
Once we got a handle, we can have our websocket
server accept a connection like before:
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;
This PerformHandshare
calls WebSocketBeginServerHandshake, with all the headers we receive from the browser and returns the headers that we must send to the browser to initiate the WebSocket
protocol. This must also begin with the "HTTP/1.1 101 Switching Protocols\r\n"
message to notify the browser that we will successfully switch.
Once that is done, we can now send and receive messages. We loop in order to receive messages:
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);
}
Once we get some bytes, we pass them to ReceiveRequest()
and this calls WebSocketReceive, WebSocketGetAction and WebSocketCompleteAction to decode the websocket
message and return us a buffer with the actual data sent.
To send data, we call SendRequest()
in a similar fashion.
for (auto& m : Maps)
{
std::vector<char> out;
m.second.SendRequest("Hello", 5,out);
m.first->transmit((char*)out.data(),(int)out.size(),1);
}
Note that I'm saving a map of all the websocket
servers along with a WS structure in order to handle multiple connections - you have to synchronize them.
Using this technology, I'm able to create a small (yet) web control for Turbo Play:
Discovering the Service
Windows 10+ has also a ZeroConf/mDNS discovery API so you can publish the service in dns-sb
. The main function is DNSServiceRegister that would publish our service:
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);
To terminate the registration, we would call DnsServiceDeRegister()
.
The Code
The code contains a small executable that creates a web server and a websocket server to be opened by any browser, then allows the app to send a message and the browser to receive it and reply. Have fun with it!
History
- 1st May, 2022: First release