RemoteControl: 基于VS2022环境下利用C/C++和MFC实现的远程控制项目 (gitee.com)
数据包的封装设计和后台运行功能
数据包的封装设计
为什么要设计数据包?
从服务端(被控端)角度出发,和客户端(控制端)建立连接后,客户端通过传输命令来使得服务端接收到命令在执行对应的操作。这一个传输命令的过程中,是从应用层的报文下来的,而我建立连接是TCP连接,TCP面向字节流,容易出现粘包的问题。所以通过对上层报文设计数据包的封装在按照字节流的报文段去传输。我采用的这个就很好的解决的粘包的问题。
最后的和校验是保证数据的完整性。同时考虑到信道上的各种不安全情况,可以采用对数据明文到密文的转换规则来实现安全性。
怎么封装设计?
自定义通信协议(protocol)如下
数据包头sHead【2B】 | 长度nLength【4B】 | 命令sCmd【2B】 | 主体部分strData | 和校验sSum【2B】 |
---|
数据包头:根据经验支撑可以设定不常见的。eg:FEFF。包头标记
长度:从控制命令开始到和校验结束的长度
数据包按照如上协议封装设计可以解决粘包问题。
Packet.h
#pragma once
#include<string>
class CPacket
{
public:
WORD PacketHead;//包头,包开始的标志,我这里设置为0x FE FF,经验之谈
DWORD PacketLength;//包长度,即从控制命令开始到和校验结束长度
WORD PacketCommand;//包内控制命令...unsigned short为2B = 16位,即最多支持2的16次方种命令
std::string PacketData;//包内具体数据
WORD SumCheck;//和校验
public:
CPacket():PacketHead(0), PacketLength(0), PacketCommand(0), SumCheck(0){}
CPacket(const BYTE* pBufferData, size_t& bufSize); //对传入的整块缓冲区解析包。BYTE = unsigned char
CPacket(const CPacket& p);//复制构造
CPacket& operator=(const CPacket& p);//赋值
~CPacket(){}
};
Packet.cpp
#include "pch.h"
#include "Packet.h"
CPacket::CPacket(const BYTE* pBufferData, size_t& bufSize)
{//对传入的整块缓冲区解析包。BYTE = unsigned char
size_t index = 0;
for (; index < bufSize; index++) {
if (*(WORD*)(pBufferData + index) == 0xFEFF) {//强转WORD类型指针后按照WORD类型规则对指针解引用获取值
PacketHead = 0xFEFF;
index += 2;
break;
}
}
if ((index + 4 + 2 + 2) > bufSize) {//判断剩下缓冲区是否满足一个数据包的最小要求
bufSize = 0;
return;
}
PacketLength = (*(DWORD*)(pBufferData + index)); index += 4;//获得长度信息后把index指针移动到指向控制命令的位置
if (PacketLength + index > bufSize) {//该包接收不完整
bufSize = 0;
return;
}
PacketCommand = (*(WORD*)(pBufferData + index)); index += 2;//获得控制命令信息后把index移到具体报数据开始位置
if ((PacketLength - 2 - 2) > 0) {
PacketData.resize(PacketLength - 4);
memcpy((void*)PacketData.c_str(), pBufferData + index, PacketLength - 4);
index += (PacketLength - 4);//记得对齐和校验位置
}
SumCheck = *(WORD*)(pBufferData + index);
index += 2;//指向下一个包的包头
//和校验过程
WORD tempSum = 0;
for (size_t i = 0; i < PacketData.size(); i++) {
tempSum += BYTE(PacketData[i]) & 0xFF;
}
if (tempSum == SumCheck) {
bufSize = index;
}
bufSize = 0;
}
CPacket::CPacket(const CPacket& p)
{
this->PacketHead = p.PacketHead;
this->PacketLength = p.PacketLength;
this->PacketCommand = p.PacketCommand;
this->PacketData = p.PacketData;
this->SumCheck = p.SumCheck;
}
CPacket& CPacket::operator=(const CPacket& p)
{
if (this != &p) {
this->PacketHead = p.PacketHead;
this->PacketLength = p.PacketLength;
this->PacketCommand = p.PacketCommand;
this->PacketData = p.PacketData;
this->SumCheck = p.SumCheck;
}
return *this;
}
Nagle算法拓展了解
TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。
即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于重负载的网络来是无法接受的。称之为**“糊涂窗口综合征”**。
为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
Nagle算法规则
- 如果发送缓冲区的数据长度达到MSS,则允许发送
- 如果发送缓冲区含有FIN,则先将剩余数据发送,再关闭
- 设置了TCP_NODELAY=true选项,则允许发送。TCP_NODELAY是取消TCP的确认延迟机制。正常情况下,当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假一般是40ms),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。当然,TCP确认延迟40ms并不是一直不变的,TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。相当于禁用了Negale 算法。TCP_QUICKACK选项来取消确认延迟。
- 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
服务端后台运行
后台运行即服务器启动时取消窗口弹出
1属性栏修改方式
第一步:属性页-连接器-入口点–输入【mainCRTStartup】,意思是:告诉它从哪里执行呢,从显示的CRT的这个地方执行。
第二步:属性页-系统-子系统,将原来的控制台改为【窗口(/SUBSYSTEM:WINDOWS)】,选项告诉系统如何运行exe,从窗口开始运行,控制台就不会出来了。
2代码修改方式
即根据需求选择如下四个中一个写在remotecontrol.cpp的全局区即可
这四行代码是用于在编译链接阶段设置一些链接器(linker)选项的预处理指令。在C和C++编程中,编译器和链接器是用于将源代码转换为可执行文件的工具。
#pragma comment(linker,"/subsystem:windows /entry:WinMainCRTStartup" )
//指定Windows子系统,入口点是Winmain
#pragma comment(linker,"/subsystem:windows /entry:mainCRTStartup" )
//指定Windows子系统,入口点是main
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup" )
//指定了控制台子系统,入口点是Winmain
#pragma comment(linker,"/subsystem:console /entry:WinMainCRTStartup" )
//指定了控制台子系统,入口点是main
- Windows 子系统:
- 适用于图形用户界面(GUI)应用程序,如窗口化的应用程序。
- 程序入口点通常是
WinMain
函数。 - 提供窗口、消息循环等 GUI 相关的功能,允许创建窗口、按钮、菜单等可视化界面元素。
- 控制台子系统:
- 适用于控制台(命令行)应用程序,例如命令行工具或文本界面应用程序。
- 程序入口点通常是
main
函数。 - 提供控制台窗口,允许在其中执行命令、接收输入和输出文本信息。