环形数据队列
在日常收发数据过程中,尤其多线程操作,数据的收发需要用到数据队列去处理,那么为什么要使用数据队列?什么是数据队列呢?
常规存储机制弊端
在接收或者发送数据的时候,你的数据的存储机制是什么样的呢 ?
- 是否是采用下述方式?
#define MAX_BUF_SIZE 2048
unsigned short recv_len;
unsigned char recv_buf[MAX_BUF_SIZE];
int main()
{
......
while(1)
{
recv_len = receive(recv_buf);
......
}
}
- 此种方法如果在单线程中问题不大,但是不适合多线程,多线程中,通常接收和发送函数是和主线程分离开来的,例如:
#define MAX_BUF_SIZE 2048
unsigned short recv_len;
unsigned char recv_buf[MAX_BUF_SIZE];
void *recv_thread(void *arg)
{
int ret;
while(1)
{
ret = receive(recv_buf); /* 如果接收到数据,则返回值为接收字节数 */
if(ret > 0)
{
recv_len = ret;
}
}
}
int main()
{
pthread_t thread_other;
......
/* 创建接收线程,主要作用是负责接收数据 */
pthread_create (&thread_other, NULL, recv_thread, NULL);
while(1)
{
if(recv_len > 0)
{
/* 处理接收 buf */
......
}
}
return 0;
}
代码弊端:上述代码模仿单线程的接收处理,但是显然问题很大,因为处理和接收在不同线程,处理线程正在处理数据过程中,有可能接收线程又接收到新数据,导致处理线程处理的数据被覆盖掉,会导致意想不到的后果。
解决方法:多线程操作中,我们希望:如果前一次接收到的数据还没有处理结束,此时又有新数据到来,我们希望新数据不会将未处理的数据覆盖掉,而是存储到未处理数据后面,等上一次数据处理结束后再处理新数据,这就是数据队列的存储机制。
数据队列的实现
数据队列的核心就是数据的入栈和出栈。下述代码中,Data_Queue
就是数据队列,PutData
和 DropData
分别是数据的入栈和出栈:
#define MAX_DATA_SIZE 2048
typedef struct
{
unsigned short start; //目前处理到的数据索引
unsigned short next; //最新入栈的数据索引
unsigned short count; //待处理数据数量
unsigned short size; //缓冲区大小
unsigned char buf[MAX_DATA_SIZE]; //数据缓冲区
}Data_Queue;
/*******************************************************************
* funcname: QueueInit(Data_Queue *data_queue)
* para: data_queue-数据队列
* function: 数据队列初始化
* return:
********************************************************************/
void QueueInit(Data_Queue *data_queue)
{
(*data_queue).next = 0;
(*data_queue).start = 0;
(*data_queue).count = 0;
(*data_queue).size = MAX_DATA_SIZE;
}
/*******************************************************************
* funcname: PutData
* para: data_queue-数据队列,data-入栈数据
* function: 数据入栈
* return:
********************************************************************/
void PutData(Data_Queue *data_queue, unsigned char data)
{
(*data_queue).buf[(*data_queue).next++] = data; //数据入栈
if((*data_queue).next >= (*data_queue).size) //入栈数据索引超过到缓冲区最大索引后,归零
{
(*data_queue).next = 0;
}
(*data_queue).count++;
if((*data_queue).count >= (*data_queue).size) //未处理的数据超多缓冲区容量后,舍弃旧数据,保留新数据
{
(*data_queue).count = (*data_queue).size;
(*data_queue).start++;
if((*data_queue).start >= (*data_queue).size) //已处理数据索引超过到缓冲区最大索引后,归零
{
(*data_queue).start = 0;
}
}
}
/*******************************************************************
* funcname: DropData
* para: data_queue-数据队列
* function: 数据出栈
* return:
********************************************************************/
void DropData(Data_Queue *data_queue)
{
if(((*data_queue).next == (*data_queue).start) &&
((*data_queue).count != (*data_queue).size)) //数据处理完毕直接返回
{
(*data_queue).count = 0;
return;
}
(*data_queue).start++; //已处理数据索引 + 1
if((*data_queue).start >= (*data_queue).size)
{
(*data_queue).start = 0;
}
if((*data_queue).count > 0) //未处理数据 -1
{
(*data_queue).count--;
}
return;
}
数据队列实战
1.数据队存储机制
此示例可以解决常规存储机制的弊端:
#define MAX_BUF_SIZE 2048
typedef struct
{
unsigned short start; //目前处理到的数据索引
unsigned short next; //最新入栈的数据索引
unsigned short count; //待处理数据数量
unsigned short size; //缓冲区大小
unsigned char buf[MAX_BUF_SIZE]; //数据缓冲区
}Data_Queue;
Data_Queue data_queue;
void *recv_thread(void *arg)
{
int ret,i;
unsigned short recv_len;
unsigned char recv_buf[MAX_BUF_SIZE];
while(1)
{
recv_len = receive(recv_buf); /* 如果接收到数据,则返回值为接收字节数 */
if(recv_len > 0)
{
for(i=0;i<recv_len;i++)
{
PutData(&data_queue,recv_buf[i]);
}
}
}
}
int read(unsigned char *read_buf)
{
int i = 0;
while(data_queue.count)
{
read_buf[i++] = data_queue.buf[start];
DropData(&data_queue);
}
return i;
}
int main()
{
unsigned char read_buf[MAX_BUF_SIZE];
pthread_t thread_other;
/* 初始化数据队列 */
QueueInit(&data_queue);
......
/* 创建接收线程,主要作用是负责接收数据 */
pthread_create (&thread_other, NULL, recv_thread, NULL);
while(1)
{
if(read(read_buf) > 0)
{
/* 处理接收 buf */
......
}
}
return 0;
}
2.利用数据队列处理接收到的特定字段
我们期望的是接收到的每个消息帧都是我们所需要的完整报文,但是现实往往不会和我们期望的一样。即:一帧消息里面往往包含多组我们需要的报文,我们需要将多组报文都处理;或者说多帧消息才能组成一组我们需要的报文段,我们需要将多个消息帧组成完整的报文段后再处理。这两个难题我们依然可以使用数据队列来解决。
例如:我们需要的消息帧以 0x68 开头,0x16 结尾,帧头后两个字节表示帧长度,此长度不包括帧头帧尾,我们把这个消息帧称为 698 报文。
-
举例1 :一帧消息帧中包含多个 698 报文,依次处理
示例报文1:68 30 00 01 05 01 00 00 00 00 00 00 52 D9 81 00 00 07 E4 03 0F 00 09 01 23 00 00 07 E4 08 03 01 0D 28 34 CC 01 07 E4 08 03 01 0D 28 34 CC 01 50 80 16
示例报文2:68 1E 00 81 05 01 00 00 00 00 00 00 D2 B6 01 00 00 00 3C 07 E4 03 0F 00 09 01 23 00 00 3F ED 16
接收到的消息帧中包含这两组示例报文,即:示例报文1+示例报文2:
68 30 00 01 05 01 00 00 00 00 00 00 52 D9 81 00 00 07 E4 03 0F 00 09 01 23 00 00 07 E4 08 03 01 0D 28 34 CC 01 07 E4 08 03 01 0D 28 34 CC 01 50 80 16 68 1E 00 81 05 01 00 00 00 00 00 00 D2 B6 01 00 00 00 3C 07 E4 03 0F 00 09 01 23 00 00 3F ED 16
但是这包含两个 698 报文 的消息帧中,两组报文我们需要按照先后关系做对应处理。 -
举例2:多个消息帧,组成一个 698报文。
示例报文:68 30 00 01 05 01 00 00 00 00 00 00 52 D9 81 00 00 07 E4 03 0F 00 09 01 23 00 00 07 E4 08 03 01 0D 28 34 CC 01 07 E4 08 03 01 0D 28 34 CC 01 50 80 16
,这是完整的一组 698 报文。
接收到3段消息,组合而成才是完整的 698 报文:
示例消息1:68 30 00 01 05 01 00 00 00 00 00 00 52 D9 81 00 00 07 E4 03 0F 00 09 01
示例消息2:23 00 00 07 E4 08 03 01 0D 28 34 CC 01 07 E4 08 03 01 0D 28 34 CC
示例消息3:01 50 80 16
我们需要将 3 个消息帧组合成我们需要的 698 报文然后处理:
#define MAX_BUF_SIZE 2048
typedef struct
{
unsigned short start; //目前处理到的数据索引
unsigned short next; //最新入栈的数据索引
unsigned short count; //待处理数据数量
unsigned short size; //缓冲区大小
unsigned char buf[MAX_BUF_SIZE]; //数据缓冲区
}Data_Queue;
Data_Queue data_queue;
void *recv_thread(void *arg)
{
int ret,i;
unsigned short recv_len;
unsigned char recv_buf[MAX_BUF_SIZE];
while(1)
{
recv_len = receive(recv_buf); /* 如果接收到数据,则返回值为接收字节数 */
if(recv_len > 0)
{
for(i=0;i<recv_len;i++)
{
PutData(&data_queue,recv_buf[i]);
}
}
}
}
/* 此函数可以解决 2 个例子中的问题
* 1. 一帧消息帧中包含多个 698 报文,依次处理
* 2. 多个消息帧,组成一个 698报文,然后再行处理
* 例如我们需要的消息帧以0x68开头,0x16结尾,帧头后两个字节表示帧长度,此长度不包括帧头帧尾
*/
void read_buf(unsigned char *read_buf)
{
int ret;
int datalen;
if(data_queue.count > 0)
{
for(int i=0;i<data_queue.count;i++)
{
if(data_queue.buf[mod(data_queue.start+i,data_queue.size)] == 0x68)
{
datalen = data_queue.buf[mod(data_queue.start+i+1,data_queue.size)];
datalen += data_queue.buf[mod(data_queue.start+i+2,data_queue.size)]*256;
if(data_queue.buf[mod(data_queue.start+i+1+datalen,data_queue.size)]==0x16)
{
while(i--)
dropdata(&data_queue);
printf("rec xxxframe,byte = %d\r\n ",datalen+2);
for(int j=0;j<datalen+2;j++)
{
read_buf[j] = data_queue.buf[mod(data_queue.start,data_queue.size)];
dropdata(data_queue);
printf("%02X ",read_buf[j]);
}
printf("\r\n");
retrun j;
}
}
}
}
return 0;
}
int main()
{
unsigned char read_buf[MAX_BUF_SIZE];
pthread_t thread_other;
/* 初始化数据队列 */
QueueInit(&data_queue);
......
/* 创建接收线程,主要作用是负责接收数据 */
pthread_create (&thread_other, NULL, recv_thread, NULL);
while(1)
{
if(read_buf(read_buf) > 0)
{
/* 处理接收 当前buf中的消息 */
......
}
}
return 0;
}
总结
上述主要以接收为例,说明了使用数据队列的原因以及用法,发送数据其实也一样,只需要在发送前 PutData,然后在发送结束后,DropData 即可。