C语言实训-网页服务器

C语言实训-网页服务器

注意

此项目仅作为教学实训作品上交,并不适用于正式项目上纲上线。因为项目中的搭建的 http 服务并不完善,不支持会话中持续的上下文传递。此外,对于报文的处理也比较简陋,解析器可能会存在内存泄漏的情况,具体查看 “已知问题” 部分。

介绍

实训内容 效果实现
实训内容 展示

此项目是对学校C语言实训课选题一的实现与进阶,旨在学习C语言与挑战使用C语言写前后端。该项目实现了一个学生数据管理功能,包括学生数据的增删改查与数据统计。

该项目设计使用的技术栈如下:

  • C语言(那是肯定的啦
  • HTTP 报文的解析与生成 (后端)
  • sqlite3 的数据库读写 (后端)
  • Layui 框架(HTML、JavaScript、CSS) (前端)

此外,编写代码过程中使用到的主要工具有:

  • CLion
  • DB Browser (SQLite)

已知问题

  • 可能存在内存泄漏问题,由于习惯了 Python 的自动回收机制,在 C 中可能会存在部分对象未及时释放,导致内存泄漏。
  • 虽然代码中对部分实现功能不同的函数放在了不同源代码文件中,当仍然缺乏统一规范的注释以及优化。即使对于程序的实现是面向对象,但是仍有部分面向程序。
  • 代码对 http 报文解析不完美,不支持解析 post 表单数据的解析,所以程序中均使用 get 方式传递数据。
  • 为了减少工作量,默认的路由函数理论上支持所有请求方式,因为没有对请求方式进行进一步的判断与验证。
  • 项目中的 include 文件夹和 lib 文件夹中尽可能包含了所有运行时需要的库文件,理论上这一步应当在 CMakeLits.txt 中标注所需要的库,但是原谅我,因为安装库这种东西实在太麻烦了,我可以不想在不同电脑中演式的时候出现缺少库的情况。
  • 不稳定,不保证频繁请求下不会出什么问题(C语言太麻烦啦o(╥﹏╥)o

编译说明

推荐使用 CLion IDEA 环境进行编译生成,项目已经将所有的运行时DLL打包,理论上在任意电脑上编译没有问题。由于项目依赖于多个支持库,而编写代码均是在 CLion 上完成的,同时在 Visual Stduio 上可以正确编译该项目。

该项目至少依赖于下面的几个支持库:

  • uv (用于 TCP 通信)
  • glib (提供类列表、类字典的对象)
  • json-c (用于 json 转化)
  • libsqlite3 (用于读写 sqlite3 数据库)

项目使用 CMake 作为构建工具,推荐使用 MinGW 工具链,项目中的 include 文件夹中,除了 webhttpd 和 srclink 之外,均为提取的支持库部分源代码,这些代码配合 lib 中的动态链接库使用,基本可以在 MinGW 上构建并生成可执行程序。

通过我的不懈努力,现在可以使用 Visual Studio 和 CLion 构建了,而且 lib 中均为我已经编译好的静态库,不会产生任何多余的支持库文件,发布版本时,建议使用 Visual Stduio 进行构建, MinGW 构建出来的 JSON-C 依赖其他文件。

  • 手动构建

首先确保拥有 cmake 的环境,然后再终端中输入如下内容生成构建文件:

cmake -G "CodeBlocks - MinGW Makefiles" -S . -B .\cmake-build-debug

进入并构建

cd cmake-build-debug
cmake --build .

当然,你可以直接在 Release 中下载我编译好的程序。

编程说明

注意

由于此项目运用了多个支持库,并且将每个支持库的库源文件和静态链接库均独立出来打包,所以在进行编程时理论上无需安装任何运行库。在 Windows 平台下,我将这些支持库在 MSVC 和 MinGW 的环境下静态编译,并在 MinGW 环境和 Visual Stduio 下测试通过。

使用 CLion 进行编程

安装 CLion 之后(并配置好系统环境),克隆该项目,并在 CLion 中打开。理论上会进行自动配置,弹出下方的构建选项:

构建配置

选择默认配置即可,而后可以在 CLion 页面进行编程。

使用 Visual Studio 进行编程

克隆该项目,打开该项目根目录,进入 Visual Studio ,依次打开:

确保有 CMake 环境(没有去安装):

切换到项目根目录,输入:cmake -G "Visual Studio 17 2022" .(需要确认自己安装的版本)

随后会生成 Visual Studio 的工程文件:

在 Visual Stduio 中,右侧解决方法,右键 “StudentManager” , 设为启动项目。

然后可以进行编程了。

提示
由于该项目将尽可能所有的支持库打包,所以在 Visual Studio 的外部依赖项会有点多,如果你想自己安装支持库,可以使用 vcpkg 来管理包,并保留下面红框框住的 include 文件(其余删除),并在 CMakefiles 中添加 vcpkg 包的查找路径(或者是 CLion 中)。
需要保留的文件 配置 vcpkg

目录架构

本项目
├─ CMakeLists.txt  # CMakeLists 构建文件
├─ database.db  # 预设学生数据的 sqlite3 数据库
├─ README.md  # 说明文档
├─ templates  # 前端 HTML 页面存放文件夹
│  ├─ add.html  # 添加学生
│  ├─ edit.html  # 编辑学生
│  ├─ index.html  # 主页
│  └─ stat.html  # 数据分析(年龄统计)
├─ static  # 静态前端资源
│  ├─ echarts.min.js  # 图表 JS
│  └─ layui  # layui 界面库
├─ src  # 项目源文件
│  ├─ backend.c  # 后端
│  ├─ frontend.c  # 前端
│  ├─ main.c  # 入口点
│  ├─ static_response.c  # 静态资源响应(前端)
│  └─ utils.c  # 使用功能
├─ lib-MSVC  # MSVC 的静态库
├─ lib  # MinGW 的静态库
└─ include  # 支持库文件
│  ├─ srclink  # 与源文件链接的头文件
│  ├─ webhttpd  # 自己写的头文件(实现 HTTP 服务器)
│  │  ├─ Basic.h  # 套接字、进行响应
│  │  ├─ HttpRequest.h  # 请求解析
│  │  └─ HttpResponse.h  # 响应生成
│  └─ ......
└─......

探索步骤

起因

下学期课多,我先把实训的作业完成(或者说先把框架搭好),是不是轻松一点?😄我熟悉前后端的开发,为什么我不把这个“管理系统”升级为前后端呢?这不正是实训课程中的“举一反三,加深理解,提高学生综合运用所学知识的能力”吗?

搭建 HTTP 服务器并以此区别每个路由的相应,可以参考 Flask 的处理方式,所以我们的设计的大体思路如下:

解析 HTTP 请求报文

说干就干,想要搭建网页服务器,我们要先理解网页服务器的基本原理。实际上,HTTP 协议是基于 TCP 协议的,HTTP 协议中规定了通讯的报文格式,从而可以正确传递网页的数据和内容。所以我们只需要让 TCP 服务器可以按照正确的方式响应浏览器发送过来的 HTTP 报文即可。(如下图)

一个基本的 HTTP 请求和响应如下:

提示
由于重点在于 HTTP 服务器的搭建,对于相较于比较底层的 TCP 服务器搭建这里就略过了,简单来说 TCP 搭建就是创建套接字、绑定套接字、开始监听,写出最基本的客户端通信即可,TCP 服务器的搭建可以~~抄袭~~参考网上的教程。另外,由于 C 语言没有异常捕获,必须在每一步都尽可能处理可能的异常行为。

我们先从解析 HTTP 请求报文开始,HTTP 请求报文是一行一行的(除了正文内容),第一行是请求行,标明了 请求方式请求地址 以及 请求协议 版本。紧接着每一行均为 请求头 ,直到最后空行结束,附带上 请求正文 (可以没有)。这里科普一下, 请求正文 一般是用来携带 POST 的表单数据 或者是 文件数据 的。

因为都是明文传输,所以解析起来也相对比较容易,难点在于如何使用 C 代码去解析,我的程序的设计逻辑大体是遵循 Python 的 Flask 进行设计的,我的设想是建立一个结构体,其中包含了这个请求的基本数据,例如:

typedef struct HttpRequestBody {
    char *content;
    size_t size;
} HttpRequestBody;

typedef struct HttpRequest {
    GHashTable *headers;
    char method[10];
    char path[10240];
    char protocol[20];
    char remote_addr[INET_ADDRSTRLEN];
    int remote_port;
    HttpRequestBody body;
} HttpRequest;

在使用其他编程语言的时候,响应正文的类型都是 “字节型(bytes)” ,但是C语言中鲜有类似的对象出现,而是用 char * (字符串)来表示,原因是字节型本质上就是内存中的二进制,char类型虽然是字符,实际上是出于人们对其的解释为“字符”,对于非字符数据,char是可以保存的。另外,C语言中没有类似 len() 或者 length 之类的方法或者对象来获取字节长度,所以需要自行标明。此外 size_t 被宏定义为 unsigned long long

我将请求头放在哈希表中进行对应,这样可以方便的取出对应的值,对于正文内容,在另外设置一个结构体来存储其正文内容和长度。(虽然在我设计的程序中这些没有用到,因为根本没有用到 POST 来传递数据。)

此外,你会发现这里的 HttpRequest 缺少了 args 和 form 两个在 Python 中分别表示 GET 请求表单和 POST 请求表单对象的对象。这里是为了方便我并未设置,不然就要大费周章去做解析了。实际上,GET表单请求的内容会被 URL 编码而后放在 path 中(如下),因为程序会使用到 get 提交的表单数据,所以我专门设立了一个函数 parse_querys 用于解析地址中 “?” 之后的表单信息,该函数会返回一个对应的哈希表。(尚未测试重复键的行为会怎么样,可能会导致程序崩溃吧,笑)

/add?uid=114514&name=姓名...

解析的代码在 include/webhttpd/HttpRequest.h ,这里提供一下解析的流程图:

分割文本用到的 strtok 函数有点像易语言中的 分割文本 函数,以前思考不明白为什么易语言偏偏要使用这样“循环式”分割文本(为什么不像 Python 一样直接变成列表),现在明白原来是参考C语言的设计,这样分割文本避免了没有“列表”的问题(主要是如果用数组存,需要额外知道每个子串的长度来确定分配内存大小,不用数组)会更快一些。

将这个流程定义为函数 parse_request,而后在TCP服务器获取客户端数据的地方,将获取的数据传入该函数,并获得 HttpRequest 对象。

生成 HTTP 响应报文

解析之后就要生成响应了,同样生成响应至少需要一下内容: 响应协议版本响应状态码响应状态响应正文长度响应正文

typedef struct HttpResponseBody {
    char *content;
    size_t size;
} HttpResponseBody;

typedef struct HttpResponse {
    int status_code;
    GHashTable *headers;
    HttpResponseBody body;
} HttpResponse;

typedef struct HttpResponseRaw {
    char *content;
    size_t size;
} HttpResponseRaw;

随后我们写一个函数用于将 HttpResponse 对象拼接成为响应报文,流程图就不画了,主要是进行字符串的拼接,这里说一下新认识的几个函数:

  • int snprintf ( char * str, size_t size, const char * format, … ) 将指定的内容写入缓存区,参考 printf ,可以用于将输出的内容直接输出到 char * 字符串中。
  • int sscanf(const char str, const char format, …) 这个函数实际上并没有在这里用到,主要为了和上面的函数匹配,这个用于从 char * 中按照指定格式提取内容。

匹配视图函数

接下来的事情就是匹配视图函数了,就是决定浏览器的请求需要被那个函数处理并生成响应。我们可以使用字符串匹配的方式来匹配请求的路由,简单的例子就是,假设浏览器请求 “/apple” 我们在程序中写一个 switch 语句,如果匹配到 “/apple” 就执行 apple_response() 函数。就是简单的一对一嘛。

不过问题在于,简单的字符串匹配会出问题,简单的例子就是,假设浏览器请求 /static/test.js ,但是偏偏带上了查询参数,例如 /static/test.js?v=1.0.2 ,来查询指定版本(这种方式主要是为了避免 cdn 缓存和热更新用的)。显而易见,如果加上查询参数,就不能简单使用字符串比较了,所以为了方便与简单,直接使用正则表达式匹配好了,这样灵活性更高。

设想如下:我们定义一个哈希表,并把正则匹配的字符串最为键,把视图函数的指针作为值,随后在TCP服务器响应客户端数据的地方变量哈希表,如果正则匹配则执行对应的视图函数。

static void add_route(char *route_pattern, HttpResponse (*handler)(HttpRequest)) {
    g_hash_table_insert(RouteMap, g_strdup(route_pattern), (gpointer) handler);
}

处理浏览器请求

资源文件

经过上面的设计之后,我们只需要关注当个路由函数如何实现就行了。我们先来解决前端问题,即传输资源文件如 js、css、html 什么的。我们可以参考 Flask 设计方法,即将所有静态文件均放在文件夹 static 中,请求路由 /static/* 即可访问到 static 文件夹下的资源文件(定义一个 static_response 函数)。

提示
如果请求恶意传入诸如 ../ 等路径,可能会导致安全文件,访问到非 static 目录下的文件,所以我在处理请求报文时,如果检查到诸如 ../ 的字符串将会直接返回 406 错误。

传输资源文件需要注意的是其的 mime 类型,为了方便这里使用直接后缀名对应的方法。除此之外就是传输文件内容了,即用 fopen(FILE, "rb") 打开文件并读取传输。在读取之前还要先得知其文件大小,这个可以使用“移光标”的方法:

fseek(file, 0, SEEK_END);
long file_size = ftell(file);
fseek(file, 0, SEEK_SET);

html 网页

本质上 html 就是一个静态文件,但是如果放在 static 文件夹中并请求太不 city 了,主要是上述我们强制将资源文件全部设定在了 /static/ 路由下,如果不这样操作(资源文件直接挂在 / 下)的话还要另外判断,而且不太符合程序的目录架构(因为要遵守 Flask 嘛,笑)。所以,我创建了一个文件夹叫做 templates 专门用于存放 html 网页文件,接着定义一个函数 send_templates (类似于 Flask 的 render_template 就是没有变量传递的功能) 用于传递网页。本质上这个函数就是指定了路径的 static_response 函数。

HttpResponse send_template(HttpRequest request, char *filepath_) {
    char filepath[BUFFER_SIZE] = "./templates/";

    strcat_s(filepath, BUFFER_SIZE, filepath_);

    FILE *file = fopen(filepath, "rb"); // 以二进制模式打开文件
    if (!file) {
        return not_found_response(request);
    }

    // 获取文件大小
    fseek(file, 0, SEEK_END);
    long file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    // 读取文件内容到缓冲区
    char *file_content = malloc(file_size);
    fread(file_content, 1, file_size, file);
    fclose(file);

    // 构造响应
    HttpResponse response;
    response.status_code = 200;
    response.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

    // 设置 MIME 类型
    const char *mime_type = get_mime_type(filepath);
    g_hash_table_insert(response.headers, g_strdup("Content-Type"), g_strdup((char *) mime_type));

    // 设置响应体内容和大小
    response.body.content = file_content;
    response.body.size = file_size;

    return response;
}

所以,我们可以方便的指定前端页面的地址:

HttpResponse index_route(HttpRequest request) {
    return send_template(request, "index.html");
}

HttpResponse add_student_route(HttpRequest request) {
    return send_template(request, "add.html");
}

HttpResponse edit_student_route(HttpRequest request) {
    return send_template(request, "edit.html");
}

HttpResponse stat_route(HttpRequest request) {
    return send_template(request, "stat.html");
}

是不是很赏心悦目啊(

后端请求

后端路以 /api 开头,大体与上述类似,不过后端要大量用到 json 转化,所以使用了 json-c 来生成 json 数据。

构建前端页面

终于到我拿手的时候了,C语言写HTTP服务器简直就是折磨,但是写前端的html是一种享受,因为你不需要去讨论函数传递、指针传递的问题。

简单来说,前端就是 页面 + 执行脚本 ,我们采用 layui Web UI 组件库,这样可以大大减少我们的代码量以及写出更好看的页面,具体的说明文档查看 (Layui)[https://layui.dev/] 。 界面设计主要使用:

  • table 动态表格
  • 表单组件(按钮、输入框)

对于统计数据页面采用 echart 绘图。

构建后端页面

由于后端页面的设计思路基本相同,这里就挑几个比较有特点的说一下,对于学生数据的获取、排序均由 sqlite3 数据库执行而来的。

学生数据获取

一般来说,对于数据库数据的获取与修改经过以下几个步骤:

  • 解析参数:使用 parse_querys 函数,将查询参数解析为哈希表的键值对
  • 生成数据库查询语句:sqlite3_prepare_v2 + 参数绑定(确保不会被 SQL 注入)
  • 解析为GList:stmt_to_dict_list 函数
  • 转化为JSON:g_list_to_json_array 函数

注意:这里大部分的辅助函数并非是我写的,而是直接由GPT生成的,我并不想把时间浪费在写这个东西上面。

HttpResponse api_get_route(HttpRequest request) {
    int page = 1, limit = 5;

    // 解析 url 参数
    GHashTable *dict = parse_querys(request.path);

    if (dict) {
        char *value = (char *) g_hash_table_lookup(dict, "page");

        if (value) {
            char *endptr;
            int num = (int) strtol(value, &endptr, 10);
            if (*endptr == '
HttpResponse api_get_route(HttpRequest request) {
    int page = 1, limit = 5;

    // 解析 url 参数
    GHashTable *dict = parse_querys(request.path);

    if (dict) {
        char *value = (char *) g_hash_table_lookup(dict, "page");

        if (value) {
            char *endptr;
            int num = (int) strtol(value, &endptr, 10);
            if (*endptr == '\0' && num > 0) {
                // 检查合法性
                page = num;
            }
        }

        value = (char *) g_hash_table_lookup(dict, "limit");
        if (value) {
            char *endptr;
            int num = (int) strtol(value, &endptr, 10);
            if (*endptr == '\0' && num > 0) {
                // 检查合法性
                limit = num;
            }
        }

        g_hash_table_destroy(dict);
    }

    // 这里开始生成响应数据了
    HttpResponse response;
    response.status_code = 200;
    response.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    g_hash_table_insert(response.headers, g_strdup("Content-Type"), g_strdup("application/json; charset=utf-8"));

    // 打开数据库连接
    sqlite3 *db;
    int rc = sqlite3_open(DATEBASE_NAME, &db);

    if (rc != SQLITE_OK) {
        return error_server_response(request);
    }

    // 获取表中全部数量
    const char *count_query = "SELECT COUNT(*) FROM students;";
    sqlite3_stmt *count_stmt;

    rc = sqlite3_prepare_v2(db, count_query, -1, &count_stmt, NULL);
    if (rc != SQLITE_OK) {
        sqlite3_close(db);
        return error_server_response(request);
    }

    int total_count = 0;
    if (sqlite3_step(count_stmt) == SQLITE_ROW) {
        total_count = sqlite3_column_int(count_stmt, 0);
    }
    sqlite3_finalize(count_stmt);

    // 构建分页查询 SQL
    const char *sql_template = "SELECT * FROM students LIMIT ? OFFSET ?;";
    sqlite3_stmt *stmt;

    rc = sqlite3_prepare_v2(db, sql_template, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        sqlite3_close(db);
        return error_server_response(request);
    }

    // 设置分页参数
    sqlite3_bind_int(stmt, 1, limit); // 第 1 个参数是 LIMIT
    sqlite3_bind_int(stmt, 2, (page - 1) * limit); // 第 2 个参数是 OFFSET

    // 查询数据并转为 GList
    GList *result_list = stmt_to_dict_list(stmt);
    sqlite3_finalize(stmt);
    sqlite3_close(db);

    json_object *data_array = g_list_to_json_array(result_list);

    const char *json_string = json_object_to_json_string_ext(
        data_api_json(0, "success", total_count, data_array),
        JSON_C_TO_STRING_PRETTY);

    // 设置响应体
    response.body.content = malloc(strlen(json_string) + 1);
    response.body.size = strlen(json_string);
    strcpy(response.body.content, json_string);

    // 释放资源
    free_dict_list(result_list);

    return response;
}
' && num > 0) { // 检查合法性 page = num; } } value = (char *) g_hash_table_lookup(dict, "limit"); if (value) { char *endptr; int num = (int) strtol(value, &endptr, 10); if (*endptr == '
HttpResponse api_get_route(HttpRequest request) {
    int page = 1, limit = 5;

    // 解析 url 参数
    GHashTable *dict = parse_querys(request.path);

    if (dict) {
        char *value = (char *) g_hash_table_lookup(dict, "page");

        if (value) {
            char *endptr;
            int num = (int) strtol(value, &endptr, 10);
            if (*endptr == '\0' && num > 0) {
                // 检查合法性
                page = num;
            }
        }

        value = (char *) g_hash_table_lookup(dict, "limit");
        if (value) {
            char *endptr;
            int num = (int) strtol(value, &endptr, 10);
            if (*endptr == '\0' && num > 0) {
                // 检查合法性
                limit = num;
            }
        }

        g_hash_table_destroy(dict);
    }

    // 这里开始生成响应数据了
    HttpResponse response;
    response.status_code = 200;
    response.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    g_hash_table_insert(response.headers, g_strdup("Content-Type"), g_strdup("application/json; charset=utf-8"));

    // 打开数据库连接
    sqlite3 *db;
    int rc = sqlite3_open(DATEBASE_NAME, &db);

    if (rc != SQLITE_OK) {
        return error_server_response(request);
    }

    // 获取表中全部数量
    const char *count_query = "SELECT COUNT(*) FROM students;";
    sqlite3_stmt *count_stmt;

    rc = sqlite3_prepare_v2(db, count_query, -1, &count_stmt, NULL);
    if (rc != SQLITE_OK) {
        sqlite3_close(db);
        return error_server_response(request);
    }

    int total_count = 0;
    if (sqlite3_step(count_stmt) == SQLITE_ROW) {
        total_count = sqlite3_column_int(count_stmt, 0);
    }
    sqlite3_finalize(count_stmt);

    // 构建分页查询 SQL
    const char *sql_template = "SELECT * FROM students LIMIT ? OFFSET ?;";
    sqlite3_stmt *stmt;

    rc = sqlite3_prepare_v2(db, sql_template, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        sqlite3_close(db);
        return error_server_response(request);
    }

    // 设置分页参数
    sqlite3_bind_int(stmt, 1, limit); // 第 1 个参数是 LIMIT
    sqlite3_bind_int(stmt, 2, (page - 1) * limit); // 第 2 个参数是 OFFSET

    // 查询数据并转为 GList
    GList *result_list = stmt_to_dict_list(stmt);
    sqlite3_finalize(stmt);
    sqlite3_close(db);

    json_object *data_array = g_list_to_json_array(result_list);

    const char *json_string = json_object_to_json_string_ext(
        data_api_json(0, "success", total_count, data_array),
        JSON_C_TO_STRING_PRETTY);

    // 设置响应体
    response.body.content = malloc(strlen(json_string) + 1);
    response.body.size = strlen(json_string);
    strcpy(response.body.content, json_string);

    // 释放资源
    free_dict_list(result_list);

    return response;
}
' && num > 0) { // 检查合法性 limit = num; } } g_hash_table_destroy(dict); } // 这里开始生成响应数据了 HttpResponse response; response.status_code = 200; response.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_hash_table_insert(response.headers, g_strdup("Content-Type"), g_strdup("application/json; charset=utf-8")); // 打开数据库连接 sqlite3 *db; int rc = sqlite3_open(DATEBASE_NAME, &db); if (rc != SQLITE_OK) { return error_server_response(request); } // 获取表中全部数量 const char *count_query = "SELECT COUNT(*) FROM students;"; sqlite3_stmt *count_stmt; rc = sqlite3_prepare_v2(db, count_query, -1, &count_stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return error_server_response(request); } int total_count = 0; if (sqlite3_step(count_stmt) == SQLITE_ROW) { total_count = sqlite3_column_int(count_stmt, 0); } sqlite3_finalize(count_stmt); // 构建分页查询 SQL const char *sql_template = "SELECT * FROM students LIMIT ? OFFSET ?;"; sqlite3_stmt *stmt; rc = sqlite3_prepare_v2(db, sql_template, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return error_server_response(request); } // 设置分页参数 sqlite3_bind_int(stmt, 1, limit); // 第 1 个参数是 LIMIT sqlite3_bind_int(stmt, 2, (page - 1) * limit); // 第 2 个参数是 OFFSET // 查询数据并转为 GList GList *result_list = stmt_to_dict_list(stmt); sqlite3_finalize(stmt); sqlite3_close(db); json_object *data_array = g_list_to_json_array(result_list); const char *json_string = json_object_to_json_string_ext( data_api_json(0, "success", total_count, data_array), JSON_C_TO_STRING_PRETTY); // 设置响应体 response.body.content = malloc(strlen(json_string) + 1); response.body.size = strlen(json_string); strcpy(response.body.content, json_string); // 释放资源 free_dict_list(result_list); return response; }

学生数据统计

实训大纲中要求统计学生的年龄数据,所以这里借助逐行读取数行并添加数据。

HttpResponse api_stat_route(HttpRequest request) {
    // 打开数据库连接
    sqlite3 *db;
    int rc = sqlite3_open(DATEBASE_NAME, &db);

    if (rc != SQLITE_OK) {
        return normal_response(500, -1, "服务器错误");
    }

    // 构建查询 SQL
    const char *sql_query = "SELECT birth FROM students;";
    sqlite3_stmt *stmt;

    rc = sqlite3_prepare_v2(db, sql_query, -1, &stmt, NULL);
    if (rc != SQLITE_OK) {
        sqlite3_close(db);
        return normal_response(500, -1, "查询准备失败");
    }

    char tags[4][15] = {"17岁", "18岁", "19岁", "19岁以上"};
    int counts[4] = {0, 0, 0, 0};

    // 今年日期
    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);
    int now_year = tm_info->tm_year + 1900;

    while (sqlite3_step(stmt) == SQLITE_ROW) {
        const char *birth = (const char *) sqlite3_column_text(stmt, 0);
        int year, month, day;
        sscanf(birth, "%4d-%d-%d", &year, &month, &day);
        if (now_year - year == 17) {
            counts[0]++;
        } else if (now_year - year == 18) {
            counts[1]++;
        } else if (now_year - year == 19) {
            counts[2]++;
        } else if (now_year - year > 19) {
            counts[3]++;
        }
    }

    sqlite3_finalize(stmt);
    sqlite3_close(db);

    // 创建 JSON 对象
    struct json_object *json_obj = json_object_new_object();

    // 创建 JSON 数组并填充 tags
    struct json_object *tags_array = json_object_new_array();
    for (int i = 0; i < 4; i++) {
        json_object_array_add(tags_array, json_object_new_string(tags[i]));
    }

    // 创建 JSON 数组并填充 counts
    struct json_object *counts_array = json_object_new_array();
    for (int i = 0; i < 4; i++) {
        json_object_array_add(counts_array, json_object_new_int(counts[i]));
    }

    // 将 tags 和 counts 添加到 JSON 对象中
    json_object_object_add(json_obj, "tags", tags_array);
    json_object_object_add(json_obj, "counts", counts_array);

    const char *json_string = json_object_to_json_string_ext(
        data_api_json(0, "success", 0, json_obj),
        JSON_C_TO_STRING_PRETTY);

    HttpResponse response;
    response.status_code = 200;
    response.headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    g_hash_table_insert(response.headers, g_strdup("Content-Type"), g_strdup("application/json; charset=utf-8"));

    // 设置响应体
    response.body.content = malloc(strlen(json_string) + 1);
    response.body.size = strlen(json_string);
    strcpy(response.body.content, json_string);

    return response;
}

打包说明

将项目进行构建之后,除了上述选择的文件之外,其余均可删除,你会发现这个可执行文件没有生成额外 dll ,这就是静态库静态链接的能力!但是如果使用 MinGW 打包,可能在一些电脑上无法正常运行,所以这里建议使用 Visual Studio (MSVC)环境生成最终的项目。

此文章同步至开源仓库(https://gitee.com/wojiaoyishang/c-language-training-web-server)。
您可以点击链接查看更多详细信息,十分欢迎给我们提交 Issue 或者 PR ,一同为开源社区做贡献。
最近一次文章同步于 2025-01-18 17:00:27 。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇