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