2024年10月

前言

推荐一款基于.NET 8.0 免费开源跨平台在线考试系统,系统不仅支持桌面端,还特别优化了移动端的用户体验。

通过本系统可以轻松搭建自己的在线考试平台,实现随时随地的测试与评估。

本文将详细介绍系统的功能特点、技术架构以及如何部署和使用。

系统介绍

支持跨平台、国产化部署。

支持国产人大金仓、达梦、OceanBase数据库 及 MySql、SqlServer、PostgreSql、SQLite 等常见数据库,极大地提高了项目的可移植性和适用范围。

系统不仅支持主流的操作系统,还特别针对国产化环境进行了优化,确保在多种国产操作系统上稳定运行。

系统功能

1、管理端首页

基本信息:展示当前账号的信息及欢迎语。

预览信息:查看管理员的基本资料。

修改信息:更新个人信息。

修改密码:更改登录密码。

退出登录:安全退出系统。

2、发布考试

支持多种考试模式(正式考试、模拟测试)。

提供灵活的试题生成方式(随机出题、手动选题、开考随机)。

支持自动与手动阅卷。

考试合格者可直接获得证书。

3、试卷管理

试卷分类:多级分类管理,支持批量操作。

发布考试:包括保存草稿、正式发布等功能。

复制:一键复制已有的试卷。

预览:查看试卷内容。

修改:编辑试卷信息。

启停用:控制试卷的状态。

删除:移除不再需要的试卷。

4、阅卷

阅卷:对主观题进行评分。

预览:查看阅卷情况。

考试管理:涵盖考生、成绩、阅卷进度、数据分析等方面。

5、考试管理

题型管理:提供单选、多选、判断、填空、简答题型,并支持扩展。

题库管理:批量导入/导出试题,支持预览。

证书管理:创建证书模板,支持拖拽定位及预览。

题目组:组织题目便于练习和组合成卷。

6、问卷调查

支持内部与外部问卷,后者可通过二维码填写。

发布:创建新的问卷。

复制:快速复制现有问卷。

预览:查看问卷设计。

修改:编辑问卷内容。

启停用:控制问卷的有效性。

删除:移除问卷。

问卷统计:生成统计图表。

7、其他功能

企业管理:包括组织架构调整、人员权限设定等。

系统管理:如管理员配置、用户设置、数据库维护、安全规则制定等。

日志管理:记录管理员操作、用户活动、系统错误,以及相关设置。

统计图表:展示用户登录频率、安全拦截次数等关键指标。

支持环境

支持的操作系统

1、Windows

2、Linux

支持的数据库

系统源码

系统代码组织框架结构,如下所示:

├── src (源代码)
│ ├── Datory (数据库基础类)
│ ├── XBLMS (接口基础类)
│ ├── XBLMS.Core (核心代码)
│ ├── XBLMS.Web (UI)
│ │ ├── wwwroot (对外访问目录)
│ │ ├── Controllers (WebApi)
│ │ ├── log (运行日志)
│ │ ├── Pages (页面)
│ │ ├── appsettings.json (配置文件)
│ │ ├── web.config (配置文件,非IIS部署可以删除)
│ │ ├── xblms.json (配置文件)
├── appsettings.json (配置文件)
├── build.sln (解决方案,用于发布)
├── gulpfile.js (配置文件,用于发布)
├── xblms.sln (解决方案,用于开发)

项目部署

发布跨平台版本,为了构建和发布适用于不同操作系统的XBLMS,请遵循以下步骤:

Windows (x64)

1、执行以下命令来安装依赖项、构建前端资源、编译.NET解决方案并发布.NET Core应用程序:

npm install
npm run build
-win-x64
dotnet build .
/build-win-x64/build.sln -c Release
dotnet publish .
/build-win-x64/src/XBLMS.Web/XBLMS.Web.csproj -r win-x64 -c Release -o ./publish/xblms-win-x64

2、然后进入发布目录获取部署文件

cd ./publish/xblms-win-x64

Linux (x64)

1、执行以下命令来安装依赖项、构建前端资源、编译.NET解决方案并发布.NET Core应用程序:

npm install
npm run build
-linux-x64
dotnet build .
/build-linux-x64/build.sln -c Release
dotnet publish .
/build-linux-x64/src/XBLMS.Web/XBLMS.Web.csproj -r linux-x64 -c Release -o ./publish/xblms-linux-x64

2、然后进入发布目录获取部署文件

cd ./publish/xblms-linux-x64

注意事项

  • 确保所有依赖项都已正确安装。
  • 在执行构建命令之前,请检查
    .csproj
    文件中的路径是否正确。
  • 如果需要针对不同的Linux发行版进行交叉编译,可能还需要额外的步骤来设置正确的交叉编译工具链。
  • 确认
    .NET SDK
    版本与项目要求相符。
  • 对于Linux环境,建议在一个与目标系统相同架构的机器上进行构建,以避免潜在的二进制兼容性问题。
  • 发布和部署手册:https://gitee.com/xblms/xblms/tree/master/部署手册

项目效果

演示环境为单机构版本,多机构版本麻烦自己在本地运行代码体验。

主要区别在于按公司和部门进行权限划分,各自管理和组织考试。

管理端

用户端

移动端

注意事项

  • 由于演示环境限制,同一个账号不能同时在多个设备上登录。

  • 如果突然被强制离线,可能是其他用户登录了同一账号,请重新登录尝试。

项目总结

本文展示了部分功能和内容,如有需求访问项目地址获取详细信息。希望本文能在考试系统开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

项目地址

Gitee:
https://gitee.com/xblms/xblmes

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

之前为了准备CTF比赛,搞了一个云服务器,顺便申请了一个
公网IP

看着申请到的IP地址,我突然想到了一个问题:
不是早就说IP地址已经耗尽了吗,为什么我随便就能申请到,是谁在负责IP地址的管理分配,咱们中国总共又有多少IP地址呢?

答案:大概是3.9亿多个。而美国有十多亿,是我们的好几倍。这个数字怎么来的呢,别着急,继续往下看。

大家知道,我们现在的互联网主要还是采用的IPv4的地址,虽然我上大学那会儿就在吼IPv6了,但这么多年过去了,IPv4的地位依旧没有动摇。

IPv4的IP地址,是采用4个字节的整数来表示,学过编程的都知道,4个字节能够表示的范围是
0-4294967295
(无符号数)。

也就是说,全世界最多只有42.9亿个IPv4的地址。

当然,为了书写和记忆方便,我们一般不会用整数来表示,而是采用
点分十进制
的形式。

从 0.0.0.0 到255.255.255.255,分四个字节,每个字节范围是0-255。

然而,IP地址中不是所有都能拿来直接用,这42.9亿中有不少特殊用途的IP地址。

比如很多人都知道这三个范围的私有IP:

10.0.0.0--10.255.255.255
172.16.0.0--172.31.255.255
192.168.0.0--192.168.255.255

实际上,除了这三个范围的IP地址,还有许多特殊用途的IP地址。网络上很多文章列举的七七八八,都不完善,完整的特殊IP地址列表,还得要去管理IP地址的
IANA
官网查看。

IANA

IANA,全称
Internet Assigned Numbers Authority
,互联网号码分配机构,它管理着全球互联网中使用的IP地址、域名和许多其它参数。

在它的官网中,有一个文档,包含了迄今为止,所有特殊用途的IP地址:

很多人都知道
127.0.0.1
代表着本地回环地址,但其实回环地址不只是一个,从
127.0.0.0-127.255.255.255
整个16777216个地址都是回环地址,所有发往这些地址的数据包都会被loop back。

IP地址本就珍贵,这么多都作为回环地址,是不是有点浪费啊?

中国的IPv4地址数量

全球的IP地址由五大区域性机构进行管理,咱们国家的IP地址由亚太地区的APNIC分配管理。

在APNIC的官网上,也有一个公开的文档,记录了APNIC管理分配的所有IP地址信息,每日更新,可以访问下面这个地址拿到:

该文件每行的格式如下:

注册机构|国家/地区代码|类型|起始地址|IP数量|分配日期|状态

咱们大陆地区的代码是CN,所以,可以通过简单的命令将大陆地区的IPv4地址分配情况筛选出来:

cat delegated-apnic-latest | grep "CN|ipv4" > cn_ipv4.txt

可以看到,截止2024-8-17日,中国大陆地区一共分配了
8655
个网段:

写个简单的脚本程序,将所有网段的IP地址数量相加:

import sys
total = 0
print('target file: %s' % sys.argv[1])
with open(sys.argv[1]) as fp:
    while True:
      line = fp.readline()
      if line:
          fields = line.split('|')
          ip = fields[3]
          num = int(fields[4])
          print('ip: %s, num: %d' % (ip, num))
          total += num
      else:
          break
print('total: %d' % total)

得到总共的IP地址数量:
3 4314 4448
,约占整个IP地址空间的
8%

用同样的方法,分别统计台湾、香港、澳门三个地区的数据:

  • 台湾:
    35711232
  • 香港:
    12571136
  • 澳门:
    337664

那整个加起来的数据就是:
3 9176 4480
,三亿九千多万,约占整个IP地址空间的
9.14%

通过同样的方法,还可以找到美国的数据,大概是
15 8647 9872,接近16亿左右。按照人均来算,美国人均可以分到4个公网IP地址。真是太阔绰了,没办法,谁让互联网是人家发明的呢。

那这个数据到底对不对呢?

我查阅了国内专门负责IP地址分配管理的机构:
CNNIC
(中国互联网络信息中心)在今年3月份发布的一份《第 53 次中国互联网络发展状况 统计报告》,其中关于IP地址这里,有一个数据:

这份文件给出去年12月份的数据,全国的IPv4地址大概在3.92亿,与前面的统计基本上吻合,可能由于数据时间和其他方面的因素,存在小部分的出入,但总体而言,国内的IPv4地址数量大概在这个规模是肯定的。

阿里云的IPv4地址数量

弄清了国内的IP地址数量,那么问题来了,阿里云到底有多少IPv4地址呢?

在淘宝下的一个IP地址库中找到了一个数据:

当然,从表格中大陆地区的IP地址数量可以看到,这个表格的数据是有些过时了,但数量级应该差不了太多。

从这份数据来看,阿里巴巴拥有差不多
370W+
的IP地址。

而这,仅仅是活跃的IP地址数,不代表阿里所屯的IP地址总数。

那阿里究竟屯了多少IP,在公开的信息中,我只找到了一份2019年发布的数据:

2019上半年,阿里云的活跃IP数量在330万,比上面的淘宝IP统计少了40万,也就是说2年之后活跃IP增长了40万。

而光是2019年的这份数据,阿里云屯的IP总数已经超过
1500W+
,而现在,这个数据只会更多,就国内几家云服务厂商而言,可谓是一骑绝尘了。

从上面图中还可以看出,亚马逊不愧是全球最强大的云计算厂商,足足屯了
7500W+
的IP地址,就这还是几年前数据。

至此,我们的问题算是弄清楚了。当然了,购买云服务器得来的IP地址也是有租用期限的,云服务器到期之后,里面的公网IP地址也就回收了。

虽然IP地址紧张,但各云服务器厂商都屯了不少,公网IP在池子里不断流转,所以我们才能花个百来十块就搞来一个。

最后,给大家留一个思考题:
0.0.0.0和127.0.0.1这两个IP地址,有什么不同?


很多小伙伴有查询IP地址归属地的需求,这里推荐一个纯真数据库:

纯真(CZ88.NET)自2005年起一直为广大社区用户提供社区版IP地址库,只要获得纯真的授权就能免费使用,并不断获取后续更新的版本。如果有需要免费版IP库的朋友可以前往纯真的官网进行申请。
纯真除了免费的社区版IP库外,还提供数据更加准确、服务更加周全的商业版IP地址查询数据。纯真围绕IP地址,基于 网络空间拓扑测绘 + 移动位置大数据 方案,对IP地址定位、IP网络风险、IP使用场景、IP网络类型、秒拨侦测、VPN侦测、代理侦测、爬虫侦测、真人度等均有近20年丰富的数据沉淀。

一、介绍
今天是这个系列《C++之 Opencv 入门到提高》得第二篇文章。今天这个篇文章很简单,只是简单介绍如何使用 Opencv 加载图像、显示图像、修改图像和保存图像,先给大家一个最直观的感受。但是,不能认为很简单,只是让学习的过程没那么平滑一点,以后的路就好走了。OpenCV 具体的简介内容,我就不多说了,网上很多,大家可以自行脑补。
OpenCV 的官网地址:
https://opencv.org/
,组件下载地址:
https://opencv.org/releases/

OpenCV 官网学习网站:
https://docs.opencv.ac.cn/4.10.0/index.html

我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10(64位)
开发组件:OpenCV – 4.10.0
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
开发语言:C++(VC16)

二、知识讲解
由于今天的内容很简单,就不过多的写文字了,直接上代码,而且在代码中都进行了详细的注释,开始今天旅程吧。
源代码中有需要图像的,把地址换成自己的。

1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 
4 using namespacecv;5 using namespacestd;6 
7 /// <summary>
8 ///图像处理9 ///1、加载图像        imread( const String& filename, int flags = IMREAD_COLOR )10 ///2、创建窗口        void namedWindow(const String& winname, int flags = WINDOW_AUTOSIZE)11 ///3、显示图像        void imshow(const String& winname, InputArray mat)12 ///4、修改图像        void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 )13 ///5、保存图像        imwrite( const String& filename, InputArray img,const std::vector<int>& params = std::vector<int>())14 /// </summary>
15 /// <returns></returns>
16 intmain()17 {18     //1、加载图像19     //Mat imread( const String& filename, int flags = IMREAD_COLOR ):第一个参数是图像的地址,第二个参数加载图像的方式。20     //IMREAD_UNCHANGED(<0)表示加载原图,不做任何修改。21     //IMREAD_GRAYSCALE(0)表示把原图作为灰度图像加载进来。22     //IMREAD_COLOR(>0)表示把原图作为 RGB 图像加载进来。23     //opencv 支持 jpg、png、tiff 等常见的图像格式。
24     Mat src = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\guanyu.jpg", IMREAD_UNCHANGED);25 
26     //以灰度图像的方式加载原图
27     Mat src2 = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\guanyu.jpg", IMREAD_GRAYSCALE);28 
29     //2、创建窗口30     //void namedWindow(const String& winname, int flags = WINDOW_AUTOSIZE):winname 表示窗口的名称。flags 表示窗口的特性。31     //namedWindow:表示创建一个指定名称和特性的窗口,这个窗口用于显示图像。它是由 OpenCV 自动创建和销毁的,无需手动释放。32     //flags:窗口特性列表33     //WINDOW_AUTOSIZE:自适应图片大小,用户无法修改窗口大小。34     //WINDOW_FREERATIO:窗口大小自适应比例35     //WINDOW_FULLSCREEN:全屏显示36     //WINDOW_GUI_NORMAL:是绘制窗口的旧方法没有状态栏和工具栏,而WINDOW_GUI_EXPANDED是一个新的增强GUI。37     //WINDOW_KEEPRATIO:保持图像的比例。38     //WINDOW_NORMAL:跟 QT 集成的时候会使用,允许修改窗口大小。39     //40 
41     namedWindow("OrigianImage", WINDOW_AUTOSIZE);42     namedWindow("GrayImage", WINDOW_AUTOSIZE);43 
44     //3、显示图像45     //void imshow(const String& winname, InputArray mat):winname 使用 namedWindow 创建的窗口名称;mat 要显示的图像对象。46     //imshow:使用 namedWindow 创建的窗口,显示指定的图像。
47 
48     imshow("OrigianImage", src);49     imshow("GrayImage", src2);50 
51     //4、修改图像52     //void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 ):src 源图像,需要转换的图像;dst 色彩空间转换后的图像;code 源色彩空间和目标色彩空间的转换方式;dstCn53     //cvtColor:把图像从一个色彩空间转化为另一个色彩空间。54     //code 列表:55     //COLOR_BGR2GRAY:从 RGB 转化到 Gray,从彩色转换为灰色。56     //57 
58 Mat des;59 cvtColor(src, des, COLOR_BGR2HSV);60 
61     namedWindow("cvtColorImage", WINDOW_AUTOSIZE);62     imshow("cvtColorImage", des);63 
64 
65     //5、保存图像66     //imwrite( const String& filename, InputArray img,const std::vector<int>& params = std::vector<int>()):filename 要保存图片的文件名称,包含完整路径和文件名和扩展名,扩展名不同,生成的图像也不同;img 要保存的图像。67     //imwrite:用于保存为指定名称的图像。68     //只有8位、16位的 PNG、JPG、TIFF文件格式而且是单通道或者是三通道的 BGR 的图像才可以通过这种方式保存。69     //保存 PNG 格式的时候可以保存透明通道的图片。70     //可以指定压缩参数。71 
72     //保存三种格式,通过后缀名实现。
73     imwrite("D:\\ScanImages\\mypic.png",des);74     imwrite("D:\\ScanImages\\mypic2.jpg", des);75     imwrite("D:\\ScanImages\\mypic3.tif", des);76 
77     waitKey(0);78 
79     return 0;80 }

原图代码:

1 Mat src = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\guanyu.jpg", IMREAD_UNCHANGED);
2 namedWindow("OrigianImage", WINDOW_AUTOSIZE);

3 imshow("OrigianImage", src);

效果如图:

灰度图像的代码:

1 Mat src2 = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\guanyu.jpg", IMREAD_GRAYSCALE);2 namedWindow("GrayImage", WINDOW_AUTOSIZE);3 imshow("GrayImage", src2);

灰度图像如图:

改变色彩空间的代码:

1 Mat des;2 cvtColor(src, des, COLOR_BGR2HSV);3 
4 namedWindow("cvtColorImage", WINDOW_AUTOSIZE);5 imshow("cvtColorImage", des);

效果如图:

其他的就不多说了,内容很简单。



三、总结
这是 C++ 使用 OpenCV 的第二篇文章,其实也没那么难,感觉是不是还是很好入门的,那就继续。初见成效,继续努力。皇天不负有心人,不忘初心,继续努力,做自己喜欢做的,开心就好。

1. 前言

本篇我们讲解
2个月搞定计算机二级C语言
——真题 5

真题5-程序评分

2. 程序填空题

2.1 题目要求

真题5-程序填空

2.2 提供的代码

#include <stdio.h>

double fun(int n) {
    int    i;
    double s, t;
    /**********found**********/
    s = __1__;
    /**********found**********/
    for (i = 1; i <= __2__; i++) {
        t = 2.0 * i;
        /**********found**********/
        s = s + (2.0 * i - 1) * (2.0 * i + 1) / __3__;
    }
    return s;
}

main() {
    int n = -1;
    while (n < 0) {
        printf("Please input(n>0): ");
        scanf("%d", &n);
    }
    printf("\nThe result is: %f\n", fun(n));
    getchar();
}

2.3 解题思路

这道题就是典型的纸老虎题,它作为编程的第一题定会在考场上压力你一下,在题目上做做文章,搞得这道题好像很难的样子,其实主要的代码都写完了,而且也给出了式子的规律,那么我们只需略微出手,便可拿下此题。


(1)
处填空:

此处是给变量
s
赋值,可以看到在
fun()
函数的末尾是
return s;
将其作为函数返回值返回,由此可以得出
s
是保存前 n 项和的变量,所以这里我们给它赋值为 0,做一个变量初始化,防止程序开始运行时
s
存储的是
垃圾值

s = 0;  // 初始化为 0


(2)
处填空:

这里缺一个循环的条件,在题目所给的式子中是从 1 计算到 n 的,循环中
i
做的就是这件事情,所以
i
小于等于形参
n
时符合条件,此处填
n

for (i = 1; i <= n; i++) {  // 1 ~ n


(3)
处填空:

我们单独看这个式子
(2.0 * i - 1) * (2.0 * i + 1) / __3__
,此处可以和题目最后的式子规律一一对应,空缺的第 3 处对应着式子的分母部分
(2*n)²
。变量
t
在循环开始时通过
t = 2.0 * i;
已经完成了分母括号里的计算,我们只需使用
t * t
计算平方即可。

s = s + (2.0 * i - 1) * (2.0 * i + 1) / (t * t);  // 计算 t 乘 t,即 t 的平方

当然计算平方也可以使用 C 语言标准库里的函数——
pow
函数,它可以用于计算幂次,定义在
math.h
头文件中,使用需要包含头文件。

// C 库函数 double pow(double x, double y) 返回 x 的 y 次幂,即 x ^ y
#include <math.h> // 使用 pow 函数时需要包含 math.h 头文件

s = s + (2.0 * i - 1) * (2.0 * i + 1) / pow(t, 2); // 计算 t 的 2 次幂,即 t 的平方

题目要求不要改动其他函数,不得增行或删行,所以这里我继续使用
t * t
计算平方,平时编程时可以直接用
pow
函数。

2.4 代码实现

填写完整的代码:

#include <stdio.h>

double fun(int n) {
    int    i;
    double s, t;
    /**********found**********/
    s = 0;  // 初始化为 0
    /**********found**********/
    for (i = 1; i <= n; i++) {  // 1 ~ n
        t = 2.0 * i;
        /**********found**********/
        s = s + (2.0 * i - 1) * (2.0 * i + 1) / (t * t);  // 计算 t 乘 t,即 t 的平方
    }
    return s;
}

int main() {
    int n = -1;
    while (n < 0) {
        printf("Please input(n>0): ");
        scanf("%d", &n);
    }
    printf("\nThe result is: %f\n", fun(n));
    getchar();
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

3. 程序修改题

3.1 题目要求

真题5-程序修改

3.2 提供的代码

#include <stdio.h>
#pragma warning(disable : 4996)

int my_isalpha(char c) {
    if (c >= 'A' && c <= 'Z')
        return 1;
    else if (c >= 'a' && c <= 'z')
        return -1;
    else
        return 0;
}

void a() {
    char ch;
    int  sort;
    printf("本程序判断你从键盘上键入字符的种类,请输入字符(串):\n");
    do {
        ch = getchar();
        if (ch != '\n') {
            sort = my_isalpha(ch);
            /**********************found***********************/
            switch (-1 <= sort && sort <= 1) {
                case 1:
                    printf("%c", '*');
                    break;
                    /**********************found***********************/
                case -1:
                    printf("%c", '#');
                case 0:
                    printf("%c", '?');
            }
        }
        /**********************found***********************/
    } while (ch == '\n');
    printf("%c", '\n');
}

void main() {
    a();
}

3.3 解题思路

这道题主要考察
switch
语句的用法。


(1)
处修改:

switch
括号中的表达式(即控制表达式)必须是整型或能转换为整型的类型。
case
标签也必须是常量表达式,并且必须与
switch
的控制表达式类型兼容。

所以不需要那一堆的关系、逻辑运算,只需放入
sort
就好,它存储着
my_isalpha
函数的返回值。

switch (sort) {


(2)
处修改:


switch
语句中,
break
语句用于终止当前
case
分支的执行,并跳出
switch
语句,避免程序继续执行后续的
case
分支。

在改好第一处后,运行程序输入一个小写字母,返回的是
#?
,这是因为此处的
case -1:
的分支没有加
break
语句,导致不能跳出
switch
语句,在执行完
case -1:
分支后又执行了
case -1:
分支,显然我们程序没有按照我们的想法运行。

这时我们需要将两个分支分别加上
break
语句即可。

我们在后续的程序开发中可以再加上
default
分支,用来执行不在条件内的操作,从而增强程序的健壮性。

case -1:
    printf("%c", '#');
    break;
case 0:
    printf("%c", '?');
    break;


(3)
处修改:

do while
语句会先执行一遍循环中的程序,再判断循环条件是否为真,真则继续循环,否则执行循环后面的程序。

ch
存储的是键盘输入的单个字符,
'\n'
是一个转义序列,用于表示换行符,对应键盘上的回车键(Enter)。

这里我没找出什么错误,提供的代码是
while (ch == '\n');
,改的话只能改成
while (ch != '\n');
,这两个代码我分别提交后都可以得满分。

分别运行这两个程序,如果按照要求先输入字符再敲回车这两个程序是没有任何区别的。

但是没有先输入字符直接敲回车,那么区别就来了:
while (ch == '\n');
不论你敲多少次回车,这个循环都不停止,因为符合
ch == '\n'
这个条件,它会一直循环到你输入不是回车的字符。

do {
    ch = getchar();
    if (ch != '\n') {
        // ……
    }
    /**********************found***********************/
} while (ch == '\n');


while (ch != '\n');
呢?如果你第一次敲回车此时
ch
存储的是回车的转义字符
'\n'
,循环条件是
ch != '\n'
,由于循环条件不符合,所以不会进行下一次循环,而是执行
while
下面的语句了。

do {
    ch = getchar();
    if (ch != '\n') {
        // ……
    }
    /**********************found***********************/
} while (ch != '\n');

这就是它俩的区别所在,大家以后编程时需要注意循环/判断的条件哦。

3.4 代码实现

修改后的代码:

#include <stdio.h>
#pragma warning(disable : 4996)
int my_isalpha(char c) {
    if (c >= 'A' && c <= 'Z')
        return 1;
    else if (c >= 'a' && c <= 'z')
        return -1;
    else
        return 0;
}
void a() {
    char ch;
    int  sort;
    printf("本程序判断你从键盘上键入字符的种类,请输入字符(串):\n");
    do {
        ch = getchar();
        if (ch != '\n') {
            sort = my_isalpha(ch);
            /**********************found***********************/
            switch (sort) {
                case 1:
                    printf("%c", '*');
                    break;
                    /**********************found***********************/
                case -1:
                    printf("%c", '#');
                    break;
                case 0:
                    printf("%c", '?');
                    break;
            }
        }
        /**********************found***********************/
    } while (ch != '\n');
    printf("%c", '\n');
}
void main() {
    a();
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

4. 程序设计题

4.1 题目要求

真题5-程序设计

4.2 提供的代码

#include <stdio.h>
#pragma warning(disable : 4996)
double fun(int n) {
}
main() {
    int    n;
    double s;
    void   NONO();
    printf("Input n:  ");
    scanf("%d", &n);
    getchar();
    s = fun(n);
    printf("s=%f\n", s);
    NONO();
    getchar();
}
void NONO() { /* 请在此函数内打开文件,输入测试数据,调用 fun 函数,输出数据,关闭文件。 */
    FILE * rf, *wf;
    int    n, i;
    double s;
    rf = fopen("in.dat", "r");
    wf = fopen("out.dat", "w");
    for (i = 0; i < 10; i++) {
        fscanf(rf, "%d", &n);
        s = fun(n);
        fprintf(wf, "%lf\n", s);
    }
    fclose(rf);
    fclose(wf);
}

4.3 解题思路

题目要求计算多项式的值,给定多项式的第一项为 1,第二项到第 n 项是计算阶乘倒数的和。

我们可以用
Sn
作为存储累加和的变量,之后通过循环累乘得到阶乘的值,最后计算每一项阶乘的倒数并累加至
Sn

在提交时出现了一点小问题,程序可以正常运行,但提交时判为 0 分。经过测试只要把 main 函数里的
NONO();
语句注释再提交就好了。

4.4 代码实现

填写完整的代码:

#include <stdio.h>
#pragma warning(disable : 4996)
double fun(int n) {
    int           i         = 0;
    double        Sn        = 1.0;  // 存储累加和,多项式的第一项为 1
    unsigned long factorial = 1;

    for (i = 1; i <= n; i++) {  // 遍历 1~n,注意 i 从 1 开始
        factorial *= i;         // 分别计算从 1~n 的阶乘
        Sn += 1.0 / factorial;  // 计算阶乘的倒数,并加到累加和变量中
    }

    return Sn;
}
main() {
    int    n;
    double s;
    void   NONO();
    printf("Input n:  ");
    scanf("%d", &n);
    getchar();
    s = fun(n);
    printf("s=%f\n", s);
    NONO();	// 如果评分没过,把这句注释了再提交就可以了
    getchar();
}
void NONO() { /* 请在此函数内打开文件,输入测试数据,调用 fun 函数,输出数据,关闭文件。 */
    FILE * rf, *wf;
    int    n, i;
    double s;
    rf = fopen("in.dat", "r");
    wf = fopen("out.dat", "w");
    for (i = 0; i < 10; i++) {
        fscanf(rf, "%d", &n);
        s = fun(n);
        fprintf(wf, "%lf\n", s);
    }
    fclose(rf);
    fclose(wf);
}

提示:为确保代码正常运行,请在题库编程环境的对应题目中进行测试和运行。

5. 后记

本篇博客到这就结束了,如果您有疑问或建议欢迎您在留言区留言。

本文分享自华为云社区
《【华为云MySQL技术专栏】RDS for MySQL 审计日志功能介绍》
,作者:GaussDB数据库。

1. 背景

在生产环境中,当数据库出现故障或问题时,运维人员需要快速定位出异常或者高危的SQL语句。这时,审计日志能够提供详细的记录,帮助追踪每个数据库操作的执行者、执行时间以及受影响的数据对象,从而大大加速故障排查和恢复流程。

MySQL企业版提供了审计日志插件,可以对数据库操作进行细粒度的审计。该插件支持记录用户登录、查询执行、数据修改等重要操作。然而,在MySQL社区版中,只是提供了审计日志的相关插件接口定义和功能描述,并不支持原生的审计日志功能。

为了弥补这一功能的缺失,华为云RDS for MySQL通过集成Percona公司开源的审计日志插件,实现了MySQL审计日志功能。该功能已在RDS for MySQL 5.7和RDS for MySQL 8.0版本中开放,满足了用户对数据库安全审计的需求,同时增强了数据库的合规性和可用性。

本文将以RDS for MySQL为研究对象,对于审计日志进行功能介绍和原理解析。

2. 功能参数介绍

当在RDS for MySQL上开启审计日志功能,用户可以通过SHOW variables LIKE 'audit%';语句查看与审计日志功能相关的变量名和参数值。

mysql> SHOW variables LIKE 'audit%';
+-------------------------------------+---------------+
| Variable_name                       | Value         |
+-------------------------------------+---------------+
| audit_log_anonymized_ip             |               |
| audit_log_buffer_size               | 1048576       |
| audit_log_csv2_escape               | OFF           |
| audit_log_csv2_old_separated_format | OFF           |
| audit_log_csv2_truncation           | ON            |
| audit_log_exclude_accounts          |               |
| audit_log_exclude_commands          |               |
| audit_log_exclude_databases         |               |
| audit_log_file                      | audit.log     |
| audit_log_flush                     | OFF           |
| audit_log_force_rotate              | OFF           |
| audit_log_format                    | CSV2          |
| audit_log_handler                   | FILE          |
| audit_log_include_accounts          |               |
| audit_log_include_commands          |               |
| audit_log_include_databases         |               |
| audit_log_policy                    | ALL           |
| audit_log_rotate_on_size            | 104857600     |
| audit_log_rotations                 | 50            |
| audit_log_strategy                  | ASYNCHRONOUS  |
| audit_log_syslog_facility           | LOG_USER      |
| audit_log_syslog_ident              | percona-audit |
| audit_log_syslog_priority           | LOG_INFO      |
+-------------------------------------+---------------+
23 rows in set (0.01 sec)

这些参数控制着审计日志插件的整体功能,允许用户灵活调节审计日志的各个方面。通过合理设置和调整这些参数,用户可以精确确定日志的记录范围、记录级别、存储方式等。例如,用户可以通过audit_log_policy 决定审计日志的记录级别,也可以通过改变audit_log_strategy 调整审计日志的刷新策略。

在 RDS for MySQL中,部分变量因安全合规考虑未开放修改。下表对审计日志功能中相关变量的作用和默认值进行介绍。

表格 1 审计日志变量介绍

3. 日志内容解析

在RDS for MySQL上,audit_log_policy的默认值为ALL。在该级别下,审计日志会记录包括DML(数据操作语言)、DDL(数据定义语言)、DCL(数据控制语言)操作以及连接或断开连接等数据库活动。需要注意的是,不同类型的活动包含的日志字段有所不同。下面将从常见DDL、DML、DCL操作以及数据库连接与断开连接产生的审计日志进行内容解析。

DDL、DML和DCL

对于DDL、DML和DCL操作,所生成的审计日志格式是相同的。对于日志字段的具体含义详见如下。

  • RECORD ID:审计日志唯一ID,用来标识每条审计日志。

  • STATUS:状态码,非0表示ERROR。

  • NAME:操作命令分类,QUERY、EXECUTE、QUIT、CONNECT等。

  • TIMESTAMP:记录日志发生的时间戳。

  • COMMAND_CLASS:记录DDL、DML和DCL操作的类型,SELECT、INSERT、DELETE等。

  • SQLTEXT:执行SQL语句的内容。

  • USER:连接数据库的用户名。

  • HOST:连接数据库的主机名。

  • IP:连接数据库的客户端IP地址。

  • DATABASE:连接指定的数据库名。

Connect和Disconnect

连接或断开连接事件,在用户登录成功或登录失败时均会有日志记录。与DDL和DDL操作产生的审计日志不同,连接事件产生审计日志增加了PRIV_USER、OS_LOGIN等字段,下面对于连接或断开连接产生的审计日志进行解析。

  • RECORD ID: 审计日志唯一ID,用来标识每条审计日志。

  • STATUS:状态码,非0表示ERROR。

  • NAME: 操作命令分类,QUERY、EXECUTE、QUIT、CONNECT等。

  • TIMESTAMP:记录日志发生的时间戳

  • USER:连接数据库的用户名。

  • PRIV_USER:经过身份验证的用户名。

  • OS_LOGIN:外部用户名。

  • PROXY_USER:代理用户名。

  • HOST:连接数据库的主机名。

  • IP:连接数据库的客户端IP地址。

  • DATABASE:连接指定的数据库名。

通过对审计日志内容的解析,用户不仅可以快速地查看任意时间段数据库的活动状态,还能够准确了解每条SQL语句的详情,包括执行的用户、时间戳、查询类型等关键信息。这样详细的记录为安全审查、问题排查以及性能优化提供了强有力的支撑。

4.RDS for MySQL审计日志原理浅析

RDS for MySQL审计功能的核心是通过不同类型的事件驱动审计日志插件完成对应类型日志的记录。在RDS for MySQL中一共支持两类事件,即一般事件和连接事件。一般事件可以理解为用户执行的DDL、DML和DCL语句。连接事件则是连接数据库(Connect)和断开连接(Disconnect)数据库。审计日志插件支持事件定义的相关代码如下。

static int is_event_class_allowed_by_policy(mysql_event_class_t event_class,num audit_log_policy_t policy) {
  static unsigned int class_mask[] = {
      /* ALL */
      (1 << MYSQL_AUDIT_GENERAL_CLASS) | (1 << MYSQL_AUDIT_CONNECTION_CLASS),
      0,                                   /* NONE */
      (1 << MYSQL_AUDIT_CONNECTION_CLASS), /* LOGINS */
      (1 << MYSQL_AUDIT_GENERAL_CLASS),    /* QUERIES */
  };

  return (class_mask[policy] & (1 << event_class)) != 0;
}

当发生可审计事件时,服务器会调用相关的审计接口,以便向已注册的审计日志插件传递该事件的信息,确保审计插件在必要时能够接收到并处理该事件。

审计日志功能在RDS for MySQL内核的入口函数是mysql_audit_notify。通过对应事件驱动审计日志插件的工作。主要工作流程调用栈如下所示。

  do_command
    ->dispatch_commnad
        ->mysql_audit_notify
            ->event_class_dispatch
                  // 检查当前插件是否需要处理此事件
                ->plugin_dispatch
                    // 按事件类别下发审计任务
                    ->audit_log_notify

数据库内核在收到一条SQL的执行请求后,首先,会通过do_command函数处理该连接。处理完成后,由dispatch_command函数依据不同SQL类型进行命令分发。之后,审计入口函数mysql_audit_notify会完成审计日志记录前的准备和校验工作。如果校验通过,则后续工作会由已注册的审计日志插件中的其他函数完成。

通过函数调用栈可以看出,审计日志功能与数据库内核之间实现了高度解耦。二者通过预先注册的函数接口进行对接,这种设计提高了未来功能扩展的灵活性。

审计日志插件的校验和资源准备工作由mysql_audit_acquire_plugins函数完成,当该函数完成校验后,即审计日志插件已完成注册并且相关资源也已完成绑定,接下来将由audit_log_notify函数按照事件的类型和相关参数设定完成任务分发。audit_log_notify会调用audit_log_write函数完成审计日志的写入,audit_log_write会依据audit_log_handler变量的值来判断是写入审计日志文件还是系统日志。

若是写入日志文件中,此时还会判断日志的写入策略;如果audit_log_strategy是PERFORMANCE或ASYNCHRONOUS,则会调用audit_handle_file_write_buf函数,将日志内容写入审计日志插件的缓冲区中,否则会调用audit_handle_file_write_nobuf函数,将日志内容直接写入操作系统文件缓存。

若audit_log_handler值为SYSLOG,则意味着审计日志会直接写到系统日志中。那么则会通过调用audit_handler_syslog_write完成日志向系统日志的写入。审计日志内部函数调用流程如图所示。

图1 审计日志插件工作流程图

从图中可以看到,审计日志的落盘方式主要有两种,分别是通过文件系统完成日志落盘和利用审计日志刷盘线程不断地将缓冲区的日志写入磁盘中。审计日志的缓冲区是在审计日志插件初始化时完成相关资源的分配,其结构体如下:

struct audit_log_buffer {
  // 缓冲区内容
  char *buf;
  // 缓冲区大小
  size_t size;
  // 写日志位置
  size_t write_pos;
  // 日志落盘位置
  size_t flush_pos;
  // 日志落盘工作线程
  pthread_t flush_worker_thread;
  // 缓冲区是否暂停
  int stop;
  // 缓冲区满是否丢弃该日志
  int drop_if_full;
  void *write_func_data;
  audit_log_write_func write_func;
  mysql_mutex_t mutex;
  mysql_cond_t flushed_cond;
  mysql_cond_t written_cond;
  log_record_state_t state;
};

从审计日志缓冲区结构体可以看到,日志缓冲区主要通过日志写入函数和日志落盘线程完成其核心功能。

当日志需要写入缓冲区时,首先会比较日志的长度和缓冲区的大小。如果审计日志的长度大于日志缓冲区的大小,并且缓冲区满且选择不丢弃该日志时,审计日志的落盘线程暂停工作,并会绕过日志的缓冲区,直接写入文件缓冲区中。当审计日志长度小于日志缓冲区大小时,此时会将日志的内容拷贝到文件缓冲区中,并更新缓冲区write_pos等相关参数,等待日志落盘线程的工作。如果当前写入位置超过整个缓冲区大小的一半,则会立刻通知落盘线程,完成审计日志的落盘。

审计日志的落盘工作主要由日志落盘工作线程完成。如果日志缓冲区没有关闭并且缓冲区中还存在日志尚未落盘,则会循环调用日志落盘函数进行日志写入。

static void *audit_log_flush_worker(void *arg) {
  audit_log_buffer_t *log = (audit_log_buffer_t *)arg;
  // 线程初始化
  my_thread_init();
  // 如果日志缓冲区没有关闭并且当前还有日志未落盘
  while (!(log->stop && log->flush_pos == log->write_pos)) {
    // 进行日志的落盘工作
    audit_log_flush(log);
  }
  // 关闭线程
  my_thread_end();

  return nullptr;
}

对于日志落盘函数,会通过循环判断write_pos和flush_pos是否相等,如果二者相等并且日志缓冲区没有停止工作,此时会等待1秒进入循环;如果二者不相等,说明缓冲区中有新的日志需要落盘。

此时,若write_pos大于缓冲区的大小,日志插件会把flush_pos后的所有日志进行落盘,并将当前日志的状态设为LOG_RECORD_INCOMPLETE。若write_pos在缓冲区大小范围内,则会将该条日志完整写入审计日志文件中并将该日志状态设为LOG_RECORD_COMPLETE。在完成日志落盘操作后,与缓冲区相关变量值会同步更新,并为下次日志落盘做好准备。

5.使用说明

1)数据库实例开启审计日志

登录管理控制台,在云数据库RDS for MySQL的“实例管理”页面,单击目标实例名称,进入基本信息页面。在左侧导航栏,点击“SQL审计”,在“SQL审计”右侧点击开启按钮,在弹框中点击“确定”,打开审计日志开关。

图2 设置审计日志功能

2)审计日志的下载

开启审计日志后,数据库的相关活动都会以日志的形式记录在OBS中,用户可以在控制台界面进行审计日志的下载。

图3 控制台界面下载审计日志

6.总结

RDS for MySQL的审计日志功能在用户活动监控、权限变更追踪和性能优化等方面有着重要的作用。它不仅帮助企业提升数据库的整体安全性,满足日益严格的合规性要求,还在故障排查中提供有价值的信息。这一功能能够精确的记录用户的数据库操作,有助于识别潜在的安全威胁,并为性能瓶颈分析和优化提供详实的数据支持。

华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。

点击链接,免费领取您的专属云主机~