C语言目录结构的JSON序列化
一、背景
最近接了一个新的项目,需要在嵌入式平台上搭建一个服务器交互用户配置界面,于是接触了一些嵌入式平台的websever框架类型。收获良多,也是第一次接触到用C语言写的mongoose webserver,也体会到了交叉编译带来的极致体验与痛苦。
CGI(Common Gateway Interface)通用网关接口
至于为什么要写C语言遍历目录结构呢?一来,为了更加熟悉C语言的体系;二来,arm平台的限制原因,嵌入式环境,内存等环境相对恶劣,我们会采用相对较老的CGI模式来进行后端的服务调用,webserver只是扮演简单的用户认证以及URI逻辑控制的角色。而CGI是一个黑盒,只要它输出符合http协议,不管用什么语言都可以进行编写,甚至shell都可以,其实arm板上用shell来写cgi也是不少的。例如C语言的printf,C++的cout<<,php的echo都可以作为接口的输出。
显然这一次我使用的C语言来编写的CGI文件用来输出(响应)请求
二、C语言遍历目标目录的方式
查阅C语言的资料的时候踩过不少坑,开始调用#include <io.h>这个头文件,但是万万没想到,这个文件貌似已经弃用了,历经千辛万苦,抽丝剥茧,最终还是找到了有用的头文件#include <dirent.h>这个头文件还是系统库函数,直接引用就可以了,无需额外编译及指定链接。我们接下来看一下,这个库函数是如何来遍历目录的。
1、重要的结构体
在dirent中有两个非常重要的结构体struct DIR和struct dirent:
struct __dirstream
{
void *__fd;
char *__data;
int __entry_data;
char *__ptr;
int __entry_ptr;
size_t __allocation;
size_t __size;
__libc_lock_define (, __lock)
};
typedef struct __dirstream DIR;
这个结构体就是存储目录的基本信息用的。对我们来说用处不大,但是必不可少。再来看一下dirent:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file */
char d_name[256]; /* filename */
};
这个结构体就是我们后面会着重操作的结构体,看到里面有很多有用信息,包括文件名称、文件类别等等。
2、基本用法
先上代码,在逐步分析:
#include<dirent.h>
/*-----------int main---------*/
struct dirent* ent = NULL;
DIR *pDir;
if( (pDir=opendir("/home/test")) == NULL)
{
printf("open dir %s failed\n", pszBaseDir);
return false;
}
while( (ent=readdir(pDir)) != NULL )
{
printf("the ent->d_type is%d the ent->d_name is%s\n", ent->d_type, ent->d_name);
}
closedir(pDir);
首先关注三个重点的函数opendir()、readdir()和closedir():
opendir()入参就是我们的目标路径了,一般是以可执行文件的(编译后的C文件)运行的目录为起点的相对路径。该函数返回一个DIR的结构体指针,而readdir()和closedir()入参就都是这个DIR的指针了。可以看到,readdir(pDir)返回的是dirent结构体,而通过这个结构体,我们就可以拿到我们想要的字段,为序列化JSON做准备了。
三、定义一个文件目录链表结构
开始在想着如何承载这个目录结构的时候,想的是一个文件作为一个结构体,然后内部存放一个文件夹结构体数组和一个文件数组,但是在嵌入式平台,珍贵的内存资源不允许我们这么做,因为如果定义了这样的结构体类型,而我们的结构体数组又存放不满的话(没人能保证我们的文件夹(子目录)的数量是刚好匹配你的结构体数组长度)就会有非常大的资源空间的浪费。于是我就想到了链表结构,每当遍历到一个文件夹,就将其挂在链表上,那么,就可以极大程度的减少内存资源的消耗。接下来我们来看一下这个链表的结构:
#define MAX_FILE_NUM 1000 //1000个文件极限
struct file_dir_struct
{
char dir_name[100];
char file_name[FILES_NUM_Max][100];
struct file_dir_struct *next;
struct file_dir_struct *firstchild;
};
第一个变量依然是当前的文件目录的名称也就是文件夹名称;
第二个变量是一个文件数组,这是一个最大数量为1000,字符串限制大小100字节的字符串指针数组。用来存放普通文件名;
第三个变量是一个文件夹结构体,这个结构体指向它的同级目录的下一个文件夹结构体;
第四个变量是一个文件夹结构体,指向它的第一个子目录。
这样看上去可能不太直观,我们来画一个图,大致上就能懂了:我是图
四、目录的遍历以及链表的生成
遍历生成链表结构的话,我们做了如下几步操作:
1、首先我们需要传入我们的目标路径以及该路径的文件夹结构体,没有错,第一个结构体需要我们自己去生成,其实也可以放入函数的内部去实现。
2、通过opendir()打开当前目录,进行循环遍历。
3、在遍历之前,我们需要创建一个文件夹结构体缓存指针,这样我们可以在每次循环结束的时候记住这个结构体,以便下一次循环的时候,链接两个结构体,这个也是链表生成的关键。
4、遍历的时候我们通过dirent结构体的d_type来判断这个对象是不是文件夹(目录)
5、判断是文件夹(目录)且不是.,..目录的情况下,我们再为这个文件夹(目录)创建结构体并申请内存空间(嵌入式消耗不起,先判断再申请)。
6、我们要把遍历到的第一个文件夹(目录)挂在我们传入的文件夹(目录)结构体的firstchild上,然后把标志flagfirst置为false,然后再把我们事先声明的缓存指针struct file_dir_struct* cacheDir指向它,留作下一轮使用。
7、接下来下一轮进入文件夹(目录)逻辑的循环,我们就把cacheDir->next指向本轮的刚创建的结构体就完成链表连接了。
8、文件直接放到文件数组,没啥好说的。
9、接下来就是拼接传入的文件目录路径,然后进入递归遍历的逻辑啦,具体看代码:
#include <string.h>
#include <stddef.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#define FILES_NUM_Max 100
typedef enum{
true=1,false=0
}bool;
struct file_dir_struct
{
char dir_name[100];
struct file_dir_struct *next;
struct file_dir_struct *firstchild;
char file_name[FILES_NUM_Max][100];
};
/*
* file_search函数,用户递归遍历目录文件夹并生成链表结构。
* 入参:
* 1、char *dir 需要进行遍历的目标路径,相对路径以该程序运行的环境为根目录。
* 2、struct file_dir_struct* file_dir 第一个结构体需要我们自己定义并传入。
*/
int file_search(const char *dir, struct file_dir_struct *file_dirs)
{
int i = 0;
//标志位,判断是否是第一个子目录,链接firstchild后置为false
bool firstflag = true;
DIR *pCurrentDir;
struct dirent *stFileData;
if((pCurrentDir = opendir(dir)) == NULL)
{
printf("Failed to open dir:%s\n",dir);
return -1;
}
//缓存上一轮循环的目录结构体,用于链接本次的结构体使用。
struct file_dir_struct *cacheDir = NULL;
while ((stFileData = readdir(pCurrentDir)) != NULL)
{
//判断类型是否为文件夹(目录)
if(stFileData->d_type == DT_DIR)
{
//跳过'.','..'目录
if(0 == strcmp(stFileData->d_name, ".") || 0 == strcmp(stFileData->d_name, ".."))
{
continue;
}
struct file_dir_struct *tmpDir = (struct file_dir_struct *)malloc(sizeof(struct file_dir_struct));
if(tmpDir == NULL)
{
printf("malloc failed! dir_name:%s\n",stFileData->d_name);
return -1;
}
//printf("%s\n",stFileData->d_name);
memset(tmpDir, 0, sizeof(tmpDir));
strncpy(tmpDir->dir_name, stFileData->d_name, strlen(stFileData->d_name));
/*
* 这里一共做了两件事:
* 1、将遍历到的第一个文件夹置为其firstchild,并把flagfirst置为false,下次循环不进该逻辑
* 2、将该子文件夹赋给缓存,下一个循环进入else逻辑,用于next指针的赋值。
*/
if(firstflag)
{
file_dirs->firstchild = tmpDir;
printf("firstchild:%s\n",tmpDir->dir_name);
cacheDir = tmpDir;
firstflag = false;
}
else
{
tmpDir->firstchild = NULL;
cacheDir->next = tmpDir;
cacheDir = tmpDir;
}
//这里要开始准备递归遍历子目录文件夹了,首先把dirpath进行拼接,生成新的路径
char dirCache[1024] = {'\0'};
strncpy(dirCache, dir, strlen(dir));
strncat(dirCache, "/", strlen("/")+1);
strncat(dirCache, stFileData->d_name, strlen(stFileData->d_name)+1);
//递归遍历
file_search(dirCache, tmpDir);
}
else//文件的话直接存入文件数组内
{
if(i < FILES_NUM_Max)
{
strncpy(file_dirs->file_name[i], stFileData->d_name, strlen(stFileData->d_name)+1);
i++;
}
}
}
//不要忘了关闭目录;
closedir(pCurrentDir);
return 0;
}
五、结构体的JSON序列化
我们将文件夹结构体化工作才做了一半,接下来就是如何进行JSON序列化了
JSON序列化不是一件很轻松的事情,幸好我只是针对这个结构体进行序列化,那样会省去不少的条件分支处理,减少80%的工作,但就这也把我折腾的很头疼。
思路大致上是和我们创建结构体的过程是反过来的(其实可以在创建过程就可以完成的,但是处理逻辑可能没有单独来做那么清晰),也是通过递归的思想来进行的,首先把重复工作写成函数,一个add_json_value来处理键值对的添加,一个add_json_array用来生成数组。我们先来看一下生成的JSON字符串长啥样,这样对接下来的代码我们大致有个数,以linux的usr目录为例(不完整):
{
"directroyName": "user",
"files": [],
"directroies": [
{
"directroyName":"local",
"files": ["111.c","lib","lib64","include"],
"directroies":[
{
"directroyName": "nginx",
"files":[],
"directroies":[
{
"directroyName":"conf",
"files": ["nginx.conf"],
"directroies":[]
}
]
}
]
},
{
"directroyName":"lib",
"files": ["111.c","lib","lib64","include"],
"directroies":[
{
"directroyName": "linux-x86_64",
"files":["xxx.so","xxx.a"],
"directroies":[]
}
]
}
]
}
当然我们生成的json字符串是没有那么多换行和缩进的,可能一行就完事了(也是符合JSON要求的)。 看代码吧,我会在适当的地方添加注释。
//添加键值对,在这里就是就是用来添加结构体的文件夹名称的,复杂键值对我们在递归逻辑处理。
void add_json_value(char* json, char *key, char* value)
{
char tmpc[100] = {'\0'};
sprintf(tmpc, "\"%s\":\"%s\",", key, value);
strncat(json, tmpc, strlen(tmpc)+1);
}
//添加数组,实际是用来添加文件数组的,涉及文件夹对象的复杂数组我们也都在递归逻辑里面处理。
void add_json_array(char* json, char *key, char (*arr)[100])
{
int i = 0;
char tmpc[100] = {'\0'};
sprintf(tmpc, "\"%s\":[", key);
strncat(json, tmpc, strlen(tmpc)+1);
while (**arr != '\0' && i < FILES_NUM_Max)
{
char tmp[100] = {'\0'};
sprintf(tmp, "\"%s\",", *arr);
strncat(json, tmp, strlen(tmp)+1);
arr++;
i++;
}
if(i>0) //这里可能不太好理解i>0及时说明走了上述循环,在数组末尾会多一个','这是不符合JSON要求的
{
//进入该逻辑我们就把末尾的','去掉
json[(strlen(json)-1)] = '\0';
}
//添加数组尾部
strcat(json,"],");//这个尾部是可以加','的因为我们是顺序增加添加,文件数组后面跟的必是文件夹数组。
}
char* dir2json(struct file_dir_struct *file_dirs, char* json)
{
struct file_dir_struct *cacheDir = file_dirs;
strcat(json,"{");
printf("here\n");
add_json_value(json,"directoryName", file_dirs->dir_name); //添加文件夹名称
add_json_array(json, "files", file_dirs->file_name); //添加文件数组
printf("%s\n\r\n\r",json);
//这里开始要生成文件夹数组了。
strcat(json,"\"directories\":[");
//判断是否有子目录,有的话递归生成子目录的字符串,注意入参,需要把json传进去拼接。
if(cacheDir->firstchild != NULL)
{
dir2json(cacheDir->firstchild, json);
}
/* 到此为止,我们的单个文件夹逻辑处理完了,回顾一下:
* 1、完成了结构体的文件名称JSON序列化。
* 2、完成了文件数组的JSON序列化
* 3、完成了firstchild的序列化,实际上是已经递归了一遍子目录了
* 接下来就是完成链表部分的递归了。
*/
if(cacheDir->next != NULL)//判断是否到了文件的结尾
{
strcat(json,"]},"); //关闭递归完成的子目录,加上封闭字符串。注意这个','是本层级的并列文件目录
dir2json(cacheDir->next, json);
}
else //和add_json_array一样,如果是结尾了,就不需要','了
{
strcat(json,"]}");
}
}
六、结尾
写在后面的话
我从来不是一个聪明的人,但是我热爱思考,热爱钻研,热爱学习,所以在这里把这次经过记录下来,这样对自己也是一个很好的巩固反馈的过程,再次思路非常的重要,其实除了这个结构体,我觉得序列化的部分是没有任何的普适性的,但是依然非常的难做,所以说思路很重要,这样,下一次在遇到困难问题的时候,我们可以不会是无从下手,至少还能做一些挣扎~
到这里我们就差不多结束了,当然里面还有很多的不足
比如写完时隔两天,在写文章的过程,突然发现,我只写了申请文件结构体的函数,没有写释放结构体的函数,评论里面有木有小伙伴帮忙补上的~