12.4 组播鼠标批量执行

组播模式相比单播模式可以提高网络的效率和带宽利用率,因为组播数据包只需要发送一次,就可以被多个接收者接收,而不需要每个接收者都单独发送一份数据包。这在需要同时向多个接收者发送相同数据的场景下特别有用,如视频会议、在线教育、流媒体等。组播模式可以减少网络拥塞,降低网络延迟,并且可以减少网络中的冗余数据。

通过构建组播服务器端与客户端,并配合键盘鼠标控制接口,当服务器端执行一个操作时客户端同步执行,通过此方法读者可轻易的实现一个简单的镜像服务器,当服务器规模庞大而主机系统版本相同时,该功能可实现服务器端执行一次客户端即可实现批量部署的效果。

先来看服务端是如何实现的功能,首先服务端定义umsg结构体,该结构用于存储鼠标坐标值以及鼠标按键状态,当服务器运行后开启组播模式等待客户端上线,当客户端上线则我们通过动态获取本机鼠标位置并封装成结构体传输给上线的客户端,以此来实现镜像功能。

#include <winsock.h>
#include <iostream>

#pragma comment(lib, "WSOCK32.lib")

using namespace std;

// 鼠标状态结构体
typedef struct umsg
{
int cursor_pos_x; // 鼠标X坐标
int cursor_pos_y; // 鼠标Y坐标
int cursor_key_state; // 鼠标按键状态

umsg() :cursor_pos_x(), cursor_pos_y(), cursor_key_state()
{
cursor_pos_x = 0;
cursor_pos_y = 0;
cursor_key_state = 0;
}
}umsg;

// 获取鼠标按键
int GetKeyState()
{
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
{
Sleep(15);
return 1;
}
if (GetAsyncKeyState(VK_RBUTTON) & 0x8000)
{
Sleep(15);
return 2;
}
return 0;
}

int main(int argc, char *argv[])
{
WSADATA wsaData;
struct sockaddr_in addr;
int fd;
struct ip_mreq mreq;

// 初始化套接字
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
std::cout << "初始化失败" << std::endl;
return 0;
}

// 创建套接字 SOCK_DGRAM 采用UDP
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
std::cout << "套接字创建失败" << std::endl;
return 0;
}

// 设置套接字为组播模式
u_int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&yes, sizeof(yes)) < 0)
{
std::cout << "设置组播模式失败" << std::endl;
return 0;
}

// 0-同一台主机,1-跨主机
UCHAR uLoop = 1;

setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&uLoop, sizeof(uLoop));

memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9999);

// 绑定套接字
if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
{
std::cout << "绑定失败" << std::endl;
return 0;
}

// 设置组播模式中的组信息
mreq.imr_multiaddr.s_addr = inet_addr("228.2.3.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq)) < 0)
{
int err = GetLastError();
std::cout << "设置组失败: " << err << std::endl;
return 0;
}

// 循环
while (1)
{
char recv_buffer[4096] = { 0 };
char send_buffer[4096] = { 0 };
int addrlen = sizeof(addr);
int nbytes;

// 接收组播数据
if ((nbytes = recvfrom(fd, recv_buffer, 4096, 0, (struct sockaddr *) &addr, (int *)&addrlen)) < 0)
{
std::cout << "接收数据包失败" << std::endl;
return 0;
}
recv_buffer[nbytes] = '\0';
std::cout << "接收组播数据包: " << recv_buffer << std::endl;

umsg msg;

// 获取鼠标状态
POINT pt;
BOOL ref = GetCursorPos(&pt);

// 设置鼠标坐标
msg.cursor_pos_x = pt.x;
msg.cursor_pos_y = pt.y;

// 获取鼠标状态值
int key_flag = GetKeyState();
if (key_flag == 0)
{
msg.cursor_key_state = 0;
}
else if (key_flag == 1)
{
msg.cursor_key_state = 1;
}
else if (key_flag == 2)
{
msg.cursor_key_state = 2;
}

std::cout << "鼠标X = " << msg.cursor_pos_x << " 鼠标Y = " << msg.cursor_pos_y << std::endl;
std::cout << "鼠标键位 = " << msg.cursor_key_state << std::endl;

// 发送组播数据包
sendto(fd, (char *)&msg, 4096, 0, (struct sockaddr *) &addr, sizeof(addr));
}
return 0;
}

与服务端功能类似,对于客户端来说,收到数据包以后,将其转换为umsg格式结构体,读取其中坐标信息,并执行指定函数对鼠标的状态进行设置,实现鼠标的同步执行。

#include <winsock.h>
#include <iostream>

#pragma comment(lib, "WSOCK32.lib")

using namespace std;

// 鼠标状态结构体
typedef struct umsg
{
int cursor_pos_x; // 鼠标X坐标
int cursor_pos_y; // 鼠标Y坐标
int cursor_key_state; // 鼠标按键状态

umsg() :cursor_pos_x(), cursor_pos_y(), cursor_key_state()
{
cursor_pos_x = 0;
cursor_pos_y = 0;
cursor_key_state = 0;
}
}umsg;

int main(int argc, char *argv[])
{
WSADATA wsaData;
struct sockaddr_in addr;
int fd;

// 初始化套接字
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
std::cout << "初始化失败" << std::endl;
return 0;
}

// 创建套接字 SOCK_DGRAM 采用UDP
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
std::cout << "套接字创建失败" << std::endl;
return 0;
}

UCHAR uLoop = 1; // 0-同一台主机,1-跨主机

setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&uLoop, sizeof(uLoop));

// 设置组播模式组信息
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("228.2.3.1");
addr.sin_port = htons(9999);

// 循环
while (1)
{
// 发送组播数据包
char send_buffer[4096] = "Hello, World!";
if (sendto(fd, send_buffer, strlen(send_buffer), 0, (struct sockaddr *) &addr, sizeof(addr)) < 0)
{
std::cout << "发送失败" << std::endl;
return 0;
}

// 接收组播数据
int addrlen = sizeof(addr);
char recv_buffer[4096] = { 0 };
recvfrom(fd, recv_buffer, 4096, 0, (struct sockaddr *) &addr, (int *)&addrlen);
std::cout << "接收组播数据包: " << recv_buffer << std::endl;

// 格式化数据包为umsg格式
umsg* recv_message = (umsg*)recv_buffer;
int pos_x = recv_message->cursor_pos_x;
int pos_y = recv_message->cursor_pos_y;
int key_stat = recv_message->cursor_key_state;

// 判断键位并设置
if (key_stat == 0)
{
std::cout << "鼠标X: " << pos_x << " 鼠标Y: " << pos_y << endl;
SetCursorPos(pos_x, pos_y);
}
else if (key_stat == 1)
{
std::cout << "左键按下" << std::endl;
mouse_event(MOUSEEVENTF_LEFTUP | MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
key_stat = 0;
}
else if (key_stat == 2)
{
std::cout << "右键按下" << std::endl;
mouse_event(MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
key_stat = 0;
}
}
return 0;
}

读者可以编译上方两段代码,并首先在物理机内启动服务端,在虚拟机内启动客户端,此时当服务端鼠标发生移动时客户端也会跟随移动,服务端执行的操作客户端也会被执行,如下图所示;