前言
想从网口持续接收大量数据并存文件,做上位机界面。因为还有其他功能要占用界面资源,需要新开存文件线程。
至于缓存队列,当接收数据线程收到数据后,不希望因为存文件操作占用资源影响接收数据,将接收到的数据先缓存到队列中,存文件线程再从队列中取数据,一般来说存磁盘的速度是比网口速度快的,所以只要缓存队列稍微大些便不会溢出。当然类中也设计了等待机制,如果存文件缓存队列满了,会先存文件,等待队列空闲时再接收数据。
做一个存文件类,流出来选择路径和Write接口,新建对象、选择路径,直接调用Write(BYTE* data,int len)成员便可将数据保存到指定路径下。
保存过程:新建一个dat文件,当文件大小超过m_MaxFileSize(单位为Byte)后,再新建一个dat文件继续存文件。
使用说明
头文件以及原文件已经上传到我的GitHub。
首先在想存文件的地方新建一个对象。
FileSave file_test;
只需控制四个接口便可以完成数据存储。其中文件最大容量默认为1GB,可以不设置。
int SetFilePath(CString str); // 根据传入的字符串设置文件路径
int SelectFilePath(); // 从windows资源管理器选择文件路径
int Write(BYTE* data,int len); // 写数据到文件
int SetMaxFileSize(int size); // 指定文件最大容量
Demo:
新建一个MFC程序,添加两个按钮,
编辑代码
#include "FileSave.h"
/*省略MFC自动生成的代码*/
FileSave file_test;
// 选择路径
void CfiletestDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
file_test.SelectFilePath();
}
// 存数据
void CfiletestDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
int len = 1024;
BYTE* data = new BYTE[1024];
for (int i = 0; i < len; i++)
{
data[i] = i;
}
for (int i = 0; i < len; i++)
file_test.Write(data, len);
delete data;
}
运行程序,选择路径后,点击造数存文件,每次点击会写入1MB数据,存得数据如下:
关键代码
缓存机制
整体思路是,在类中定义一个私有自定义结构体数组,每个结构体是队列的基本单元,结构体只存储每次要存储数据的长度(单位为Byte)以及数据首指针。入队列时动态申请内存,出队列时会释放动态申请的内存,在析构函数中也会检查有没有未释放的内存并进行释放。
自定义缓存队列,首先自定义结构体。
typedef struct _DataPak
{
int dataLen; // 数据长度,单位为Byte
BYTE* data; // 数据内容
}DataPak;
在类中定义私有队列数组,出入队列指针,以及出入队列锁。
DataPak* m_SaveDataTeamIn;
DataPak* m_SaveDataTeamOut;
DataPak m_SaveDataTeam[MAX_FILE_TEAM];
CCriticalSection m_InsSaveDataCriSec;
CCriticalSection m_PopSaveDataCriSec;
队列初始化。令入指针和出指针都指向数组头。数据长度初始化为0,同时代表着数据指针并未申请动态内存。注意数据指针也需要赋初值,否则会出现野指针。
// 数据存文件队列初始化
void FileSave::InitSaveDataTeam()
{
m_SaveDataTeamIn = m_SaveDataTeamOut = m_SaveDataTeam;
for (int i = 0; i < MAX_FILE_TEAM; i++)
{
m_SaveDataTeam[i].dataLen = 0;
m_SaveDataTeam[i].data = NULL;
}
}
入队列。其中SaveDataTeamFull是自定义函数,判断队列是否为空。
这里有一个关键的概念:临界区加锁。这是因为收到数据时数据入队列,而存文件线程会一直从队列里取数进行存文件,如果同时对同一地址读写便会出现问题。通过临界区加锁使得对同一地址空间不被同时访问。
另外,有可能数据接收速度大于存文件速度,导致存文件缓存队列满。通过bWait控制对剩余包的处理策略,包括直接丢弃、等待两种选择。
// 插入数据存文件队列
// 队列满时 bWait为True 等待,bWait为false 不等待返回错误
int FileSave::InsertSaveDataToTeam(BYTE* data,int len, BOOL bWait)
{
m_InsSaveDataCriSec.Lock(); // 使用临界区加锁
if(bWait)// 队列满时等待
{
m_PopSaveDataCriSec.Lock();
while(SaveDataTeamFull() && m_FileSaveThreadRun) // 当T线程需要停止时,此处应放弃继续写数据;
{
if(m_SaveDataTeamOut == (m_SaveDataTeam + MAX_FILE_TEAM - 1))
m_SaveDataTeamOut = m_SaveDataTeam;
else
m_SaveDataTeamOut++;
}
Sleep(10);
m_PopSaveDataCriSec.Unlock();
}
else// 队列满时返回错误
{
if(SaveDataTeamFull())
{
m_InsSaveDataCriSec.Unlock(); // 解锁
return -1;
}
}
// 入队列
m_SaveDataTeamIn->dataLen = len;
m_SaveDataTeamIn->data = new BYTE[len];
memcpy(m_SaveDataTeamIn->data, data, len);
if(m_SaveDataTeamIn == (m_SaveDataTeam + MAX_FILE_TEAM - 1))
m_SaveDataTeamIn = m_SaveDataTeam;
else
m_SaveDataTeamIn++;
m_InsSaveDataCriSec.Unlock(); // 解锁
return 0;
}
关于队列的其他函数这里不再一一介绍,有不明白或者发现我代码有漏洞的,欢迎在评论区指出。
线程
本来是想着将线程也变成类的成员,这样可以少给出很多接口,将这个类封装的更漂亮,然而翻阅很多资料之后并不知如何操作,这里暂时使用类外线程。
设计到线程的函数包括线程的初始化、开始、结束。
void InitDataSaveToFileThread(); // 初始化存文件线程
void StartDataSaveToFileThread(); // 启动存文件线程
void StopDataSaveToFileThread(); // 关闭存文件线程
其中线程启动函数如下,AfxBeginThread的第二个参数可以将类创建的对象的指针传入外部线程,进行后续的操作。
void FileSave::StartDataSaveToFileThread()
{
m_FileSaveThreadRun = TRUE;
// 第二个参数为传入线程的参数,类型为LPVOID
AfxBeginThread(FileSaveThread, (LPVOID)this, THREAD_PRIORITY_NORMAL, 0, 0, NULL);
}
以下便是存文件线程。将传进来的参数强制转换为自定义类FileSave的对象指针,进一步访问对象的公有成员。
// 存文件线程
UINT FileSaveThread(LPVOID pParam)
{
FileSave *filesave = (FileSave*)pParam;
filesave->SetThreadExit(false); // 线程退出标志置FALSE
// g_WriteFileLength = 0;
while(filesave->GetThreadRunStatus())
{
DataPak pDataPak;
int SaveData = filesave->GetDataFromTeam(&pDataPak); // 有数据时返回1
if(SaveData>0)
{
filesave->TreadSave(&pDataPak);
}
else
{
// 队列为空时等待
Sleep(10);
}
}
filesave->SetThreadExit(true); // 线程退出标志置TRUE
return 0;
}