1 后台运行设置
为什么要设置后台运行?
因为这是个服务器程序,运行起来不设置后台运行的话会弹出一个控制台,会严重影响体验
后台运行怎么设置
方式一 控制项:
-
项目属性--》链接器--》入口点:mainCRTStartup
-
项目属性--》链接器--》所有选项--》子系统--》选中windows
方式二源码:
#pragma comment(linker, "subsystem:windows /entry:WinMainCRTStartup") // 纯窗口界面 #pragma comment(linker, "subsystem:windows /entry:mainCRTStartup") // 命令行里面可以开启窗口 #pragma comment(linker, "subsystem:console /entry:WinMainCRTStartup")// 窗口界面可以创建console #pragma comment(linker, "subsystem:console /entry:mainCRTStartup")// 纯后台
2 接收数据包的设计
在上一节以及完成了基本的网络通信架构,现在就要接收数据了,接收数据会有一些问题
2.1 为什么要设计包
2.1.1tcp传输过程中容易发生粘包
tcp通信协议工作方式:
-
客户端发送一个数据包,服务器应答一个数据包
-
有时候tcp为了提高通信效率,client可以不用等待对方回应可以反反复复的发送数据包,server就会一个一个回应,client有一个没有收到就会报错
-
当客户端发送命令很频繁的时候,有时候server‘端就会收到多个数据包,这时如何区分这些数
什么是粘包?粘包就是连续向对端发送两个或者两个以上的数据包,对端在一次收取中收到的数据包数量可能大于1个,当大于1个时,可能是几个(包括一个)包加上某个包的部分,或者干脆几个完整的包在一起。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫作半包。
客户端发送命令频繁,server一次接收多次发送数据包,如何区分这些数据包,来了一些数据需要准确识别这些数据。
2.2 解决方案:包头加包体格式
包的结构:
public: WORD sHead; // 包头 固定位0xFEFF DWORD nLength; // 包长度 从命令号到和校验 WORD sCmd;// 命令号 -- 根据业务选择 std::string strData; // 包数据 WORD sSum;// 校验值
00 0000 00 .... 00
包头+包长度+命令号+包数据+校验
包头常用值:FFFE、FEFE(这些值不易再数据里面出现--经验值)
因为recv到的是一个缓冲区数据所以,需要传入这个缓冲区地址,以及缓冲区长度,再根据包结构,先找到包头两个字节,指针加2(因为包头占两个字节),在把后四个字节当作lenght(命令号的长度+正式数据长度+和校验长度)解析出来值,指针加4(length占四个字节),然后把后面两个字节当作命令号解析,指针再加两个字节,然后就是包数据了利用(前面解析出来lenght的值 - 4就可以算出缓冲区的长度了)然后memcpy 长度就是length-4, 然后指针再加length-4,然后把后面两个字节当作sum,最后对数据进行和校验,与接收到的sum值进行对比,正确就返回index的值(解析了多少缓冲区长度)
解包接口
// 数据解析 len:输入作为缓冲区长度,输出:本次解析的数据长度 CPacket(const BYTE* pData, size_t& nSize) { size_t index = 0; for (; index < nSize; index++) { if (*(WORD*)(pData + index) == 0xFEFF) { index += 2; break; } } // 包数据可能不全 if (index + 8 > nSize) { nSize = 0; return; } nLength = *(DWORD*)(pData + index); index += 4; //cmd + data + sum + head + length // 包数据不全 if (nLength + index > nSize) { nSize = 0; return; } sCmd = *(WORD*)(pData + index); index += 2; if (nLength > 4) { strData.resize(nLength - 4); memcpy((void*)strData.c_str(), pData + index, nLength - 4); index += nLength - 4; } sSum = *(WORD*)(pData + index); index += 2; DWORD sum = 0; for (int i = 0; i < strData.size(); i++) { sum += BYTE(strData[i]) & 0xFF; } if (sSum == sum) { nSize = index; return; } nSize = 0; }
封包接口
在下一节发送数据时介绍
3 服务器缓冲区的处理
可复用缓冲区处理逻辑如下:
假设有一个buffer有4096字节,在一次recv后,往buffer里面填数据,然后开始解析数据,解析完了后,需要把buffer减去用掉的长度,还有把buffer往前移,把用掉的buffer覆盖,第二次接收数据时,buffer的首地址需要加上上次没用完的数据长度,buffer的总长度也得减掉上次没用完的长度
参考代码:
-
定义一个静态变量index来累加每次的recv的数据长度
-
recv时buffer缓冲区的地址应该是buffer+index,buffer缓冲区长度为BUFFER_SIZE - index
-
让解析数据包的长度为index,但是又不能传index进去,因为会改变index的值,所以需要让len = index
-
解析完数据后,把缓冲区,往前移覆盖用掉的缓冲区
-
让index减去用掉的长度
// 循环接收处理数据,存放在m_packet里面 int DealCommand() { if (m_socket_client == INVALID_SOCKET) { return false; } char* buffer = new char[BUFFER_SIZE]; // 这里有个bug,可能上传有没有处理完的数据,再次DealCommand时候就会丢失数据 // 解决方案初始化放在构造函数里面,因为这是给可重复利用的缓冲区 memset(buffer, 0, BUFFER_SIZE); // 缓冲区当前所有数据长度 static int index = 0; while (true) { size_t len = recv(m_socket_client, buffer + index, BUFFER_SIZE - index, 0); if (len <= 0) { return -1; } // 每次接收数据后,index加上这个长度 index += len; // 让每次解析都是全部的数据长度,如果直接传index进去解析,会导致index值被修改 len = index; // len是个引用 本次解析的数据长度 m_packet = CPacket((BYTE*)buffer, len); if (len > 0) { // 解析完了后,把buffer往前移 memmove(buffer, buffer + len, BUFFER_SIZE - len); // index 减去用掉的长度 index -= len; return m_packet.sCmd; } } }