C语言目录结构的JSON序列化方案

1,118 阅读10分钟

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 DIRstruct 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,"]}");
  }
}

六、结尾

写在后面的话

我从来不是一个聪明的人,但是我热爱思考,热爱钻研,热爱学习,所以在这里把这次经过记录下来,这样对自己也是一个很好的巩固反馈的过程,再次思路非常的重要,其实除了这个结构体,我觉得序列化的部分是没有任何的普适性的,但是依然非常的难做,所以说思路很重要,这样,下一次在遇到困难问题的时候,我们可以不会是无从下手,至少还能做一些挣扎~

到这里我们就差不多结束了,当然里面还有很多的不足

比如写完时隔两天,在写文章的过程,突然发现,我只写了申请文件结构体的函数,没有写释放结构体的函数,评论里面有木有小伙伴帮忙补上的~