寒假第二次作业,暴力做法别骂我()
这个作业属于哪个课程 | <福州大学2022面向对象程序设计> |
---|---|
这个作业要求在哪里 | <2022面向对象程序设计寒假作业2> |
这个作业的目标 | 实现一个路由程序 |
作业正文 | 如下 |
其他参考文献 | <用C++进行简单的文件I/O操作ofstream fout,fin><菜鸟教程-C++文件和流><Google开源项目风格指南-C++><Windows命令行编译C,C++程序><Vsiual Studio Code对比两个文件差异><关于CIDR地址的计算方法><C++中常用的两种记录程序运行时间的总结> |
前言
完成第二次寒假作业后写点总结,奇奇怪怪一道题目,本质只是个算法/编程题,外表却包装是工程项目的样子。顺便说点内心真实想法。我的Github仓库在这里:https://github.com/Gorsonpy/MatchHelper.git
开发环境
语言的话我选择的是C++ 11.
coding: Visual Studio Community 2022.
check : Visual Studio Code.
使用步骤
这个地方我没有能够实现题目的要求(命令行操作)。因为我想不通C++的ifstream本身就要程序内指定要打开的文件,如何能够通过命令行再打开一次文件进行输入(除非我把ifstream重新换成标准输入cin)?或许C风格的文件输入输出能够实现这个,但是我能力有限,只能先写成了交互式界面。
- 打开X64\Debug文件夹。
- 点击main.exe运行(或者命令行进入同级目录状态下输入main.exe)
- 根据提示输入文件名.
需要学习内容
文件输入输出流
之前很少用过,先学习了一波。下面是测试:
首先测试文件输出。代码如下
#include<iostream>
#include<fstream> //文件读取写入流所用标准库
#include<string>
using std::cin;
using std::cout;
using std::string;
using std::ofstream;
using std::ifstream;
using std::endl;
int main()
{
ofstream fout("textOut.dat");
fout << "test: writing sth" << endl;
fout << "second line" << endl;
fout.close();
return 0;
}
运行代码前先创建了一个textOut的空文本文档:
运行后,验证结果:
值得注意的一点是C++寻找文件名的范围,查阅资料+自己试验后发现:
可以指定文件名路径或者不指定,如果像测试代码中不指定路径:就是在生成的cpp源文件的同级目录( 也有资料说是exe可执行文件目录,但是我测试是在cpp的目录 )下搜索,如果已经存在同名textOut的就写入,如果没有,就在该路径下生成一个textOut后再写入。 如果自己指定路径也是一样的,找到则写入,没找到就先创建再写入。
另外,ofstream对象open文件的时候可以同时传入不同写的模式(可参考STL文档),不传入默认就是打开文件后 删除原内容 再写入。
再测试文件输入,文件输出,文件输入和文件输出的逻辑类似,不赘述了:
#include<iostream>
#include<fstream> //文件读取写入流所用标准库
#include<string>
using std::cin;
using std::cout;
using std::string;
using std::ofstream;
using std::ifstream;
using std::endl;
int main()
{
ifstream fin("textIn.txt");
ofstream fout("textOut.txt");
string data = "test data";
int a, b;
fin >> a >> b;
fout << a + b << endl;
fin.close();
fout.close();
}
观察文本输出:
补充:完整测试后发现,IDE"编译并运行"时文件流默认寻找目录为cpp文件所在目录,单独运行exe文件时寻找目录为exe所在文件目录。
CIDR地址和点分十进制
体验感最差的一个地方。整篇作业只字不提CIDR,只提了ip地址和点分十进制。如果直接去网上搜ip地址换算大概率会翻车,因为那不是一套东西。比如你应该会搜出来这个东西:
然而这次作业的地址是有前缀如“/32”这样的,这是CIDR地址。
得把这俩都学完才能做这次作业,遗憾的是作业不提你甚至有可能都不知道CIDR这个名词。包括我也是,问了学长才知道有这回事:
作为一个做任务的人的视角确实体验很差。如果在作业里能提供一个地址换算的样例会不会好理解的多呢?这并不会很花时间,再不济告知这个专业术语叫CIDR地址也便于查询了解。这个东西本身是不难的,却花费了大量(其实是主要的时间)在这上面,因为你光搜“ip地址”一定掉坑,除非是“带位数的ip地址”之类的?只需要给一个换算样例/专业名词就能省去大量时间,何乐而不为呢?
多文件编程
之前学过,遗忘的比较厉害,复习了一下,《C++Primer》的原则是:类定义在头文件,函数应该在头文件(.h)声明,源文件(.cpp)定义。
命名规范
顺道学习了以下Google对于C++项目的命名规范,我觉得很巧妙,其中有一点是:C++类中的成员变量应在最后加'_'. 遵照后我发现真的很妙,因为这样避免了很多函数里形参和类成员变量名称冲突的问题,不用花费很多心思在设计名称上。
分析思路
其实也没有什么思路。
- 每条数据抽象为Data类,每条规则抽象为Rule类.其实这里用结构体一样能够实现,而且简单很多,但是毕竟课程名叫“面向对象程序设计”,并且第三轮会在第二轮基础进行,为了增强可复用性还是选择了class。
Data.h文件:
#pragma once
#ifndef DATA_H
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
using std::vector;
using std::cout;
using std::endl;
using std::string;
using std::ifstream;
using std::istream;
using std::ostream;
using ll = long long;
class Data
{
ll origin_ip_; //十进制
ll origin_port_;
ll receiver_ip_; //十进制存储
ll receiver_port_;
ll tcp_;
public:
friend vector<Data> ReadData(string &file_name);
friend ostream& operator<<(ostream& os, Data& data);
Data() = default;
Data(ll ip1, ll port1, ll ip2, ll port2,
ll tcp) : origin_ip_(ip1), origin_port_(port1), receiver_ip_(ip2),
receiver_port_(port2), tcp_(tcp) {};
ll origin_ip()const { return origin_ip_; }
ll origin_port()const { return origin_port_; }
ll receiver_ip()const { return receiver_ip_; }
ll receiver_port()const { return receiver_port_; }
ll tcp()const { return tcp_; }
};
#endif // DATA_H
ostream& operator<<(ostream& os, Data& data);
vector<Data> ReadData(string &file_name);
Rule.h文件:
#pragma once
#ifndef RULE_H
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
using std::vector;
using std::cout;
using std::endl;
using std::string;
using std::ifstream;
using std::istream;
using std::ostream;
using std::string;
using ll = long long;
class Rule
{
ll origin_ip_beg_;
ll origin_ip_end_;
ll receiver_ip_beg_;
ll receiver_ip_end_;
ll origin_port_beg_;
ll origin_port_end_;
ll receiver_port_beg_;
ll receiver_port_end_;
ll tcp_;
public:
friend vector<Rule> ReadRule(string& file_name);
friend ostream& operator<<(ostream& out, Rule rule);
Rule() = default;
Rule(ll ip1_beg, ll ip1_end, ll ip2_beg, ll ip2_end,
ll port1_beg, ll port1_end,
ll port2_beg, ll port2_end, ll tcp)
: origin_ip_beg_(ip1_beg), origin_ip_end_(ip1_end), receiver_ip_beg_(ip2_beg),
receiver_ip_end_(ip2_end), origin_port_beg_(port1_beg),
origin_port_end_(port1_end), receiver_port_beg_(port2_beg),
receiver_port_end_(port2_end), tcp_(tcp) {}
ll origin_ip_beg()const { return origin_ip_beg_; }
ll origin_ip_end()const { return origin_ip_end_; }
ll receiver_ip_beg()const { return receiver_ip_beg_; }
ll receiver_ip_end()const{ return receiver_ip_end_; }
ll origin_port_beg()const { return origin_port_beg_; }
ll origin_port_end()const { return origin_port_end_; }
ll receiver_port_beg()const { return receiver_port_beg_; }
ll receiver_port_end()const { return receiver_port_end_; }
ll tcp()const { return tcp_; }
};
#endif // !RULE_H
vector<Rule> ReadRule(string& file_name);
核心功能头文件match_util.h:
#pragma once
#ifndef MATCH_UTIL_H
#include<vector>
#include "Data.h"
#include "Rule.h"
using std::vector;
vector<int32_t> DoMatch(vector<Data> &datalist, vector<Rule> &rulelist);
bool check(Data& data, Rule& rule);
void Result_In_File(string &file_name, vector<Data> &datalist,
vector<Rule> &rulelist, string &packet_name);
void EnquireUser();
#endif // !MATCH_UTIL_H
类功能上是有赘余的,重载"<<"运算符和任务无关,单纯方便自己调试输出信息用。
-
文件输入输出,我用的是fstream库,但是体验很差,可能C风格的会好一点,这个我下文会具体说。
-
换算ip地址功能的实现(ReadRule.cpp)。
#include<iostream>
#include<algorithm>
#include<fstream>
#include<string>
#include<algorithm>
#include"Rule.h"
using std::string;
using std::vector;
using std::fstream;
using std::pair;
using ll = long long;
using PII = pair<ll, ll>;
long long qpower(ll a, ll b)
{
ll basic = a, ans = 1;
while (b > 0)
{
if (b & 1)
{
ans *= basic;
}
basic *= basic;
b >>= 1;
}
return ans;
}
PII StrBin_To_Dec(string &str_bin, ll bit) //把二进制的字符串转化为十进制
//返回一个最小地址ip和最大地址ip的二元组
{
PII min_max;
ll num = 0;
int base = 31; //基准,即2的次方数
for (int i = 0; i < bit; ++i) //第bit位之前的正常按照二进制计算
{
ll curr = 0;
if (str_bin[i] == '1')
{
curr = qpower(2, base);
}
num += curr;
--base;
}
min_max.first = num; //最小值就是后面全置为0,无需计算
for (int i = bit; i < str_bin.size(); ++i)
{
//最大值就是后面全为1
num += qpower(2, base);
--base;
}
min_max.second = num;
return min_max;
}
string Dec_To_Bin(ll num) //接受一个数字,并把它转化为八位二进制数字形式的字符串
{
string str_bin = "00000000"; //初始八位都置0
for (int i = 7; i >= 0; --i)
{
str_bin[i] = num % 2 + 48;
num >>= 1;
}
return str_bin;
}
void TransCidr(string& cidr, int cnt[]) //把cidr地址分为五个部分十进制数字
// 存放在传入的数组
{
auto iter = cidr.begin();
ll i = 0; //数组的下标
while (iter != cidr.end()) //遍历字符串
{
while (iter != cidr.end() && *iter != '.' && *iter != '/')
{
cnt[i] = 10 * cnt[i] + (*iter) - 48; //计算每个个部分的十进制数字大小
++iter;
}
//跳出内层循环就代表读到了'.' 或 '/' 或末尾
//如果读到末尾要把最后一个位置数字加上
if (*iter == '/')
{
++iter;
while (iter != cidr.end())
{
cnt[4] = 10 * cnt[4] + (*iter) - 48;
++iter;
}
return;
}
else
++iter, ++i;
}
}
vector<Rule> ReadRule(string &file_name)
{
ifstream fin(file_name);
vector<Rule> rulelist;
ll port1_beg = 0, port1_end = 0, port2_beg = 0, port2_end = 0,
tcp = 0;
//以下分离两ip地址
while (!fin.eof() && fin.peek() != EOF)
{
char other = '\0';
fin >> other; //除去开头的'@'字符
if (other == '\0') //监测是否是最后一个空行
break;
string cidr1, cidr2; //cidr 地址
fin >> cidr1 >> cidr2;
int cnt1[5] = { 0, 0, 0, 0, 0 }; //分别存放五个部分的数字
int cnt2[5] = { 0, 0, 0, 0, 0};
TransCidr(cidr1, cnt1), TransCidr(cidr2, cnt2); // 分离ip地址的五个部分在cnt1和cnt2
string str_bin1, str_bin2;
for (int i = 0; i < 4; ++i) //前四个位置是ip信息
{
str_bin1 += Dec_To_Bin(cnt1[i]);
}
PII ip1 = StrBin_To_Dec(str_bin1, cnt1[4]);
for (int i = 0; i < 4; ++i)
{
str_bin2 += Dec_To_Bin(cnt2[i]);
}
PII ip2 = StrBin_To_Dec(str_bin2, cnt2[4]);
.... //以下略去端口和tcp读入代码
}
总体思路就是文件读入---CIDR地址转化为四个八位的二进制字符串并分离出位数信息---这四个字符串拼接成一个字符串---根据地址位数信息补0(得到最小地址),补1(得到最大地址)---二进制字符串换算为十进制数字存储,便可直接和数据集ip十进制数字比较.
- 匹配功能,这个地方我用的是很朴素的做法:
bool check(Data& data, Rule& rule)
{
if (data.origin_ip() < rule.origin_ip_beg() ||
data.origin_ip() > rule.origin_ip_end())
return false;
if (data.receiver_ip() < rule.receiver_ip_beg() ||
data.receiver_ip() > rule.receiver_ip_end())
return false;
if (data.origin_port() < rule.origin_port_beg()
|| data.origin_port() > rule.origin_port_end())
return false;
if (data.receiver_port() < rule.receiver_port_beg()
|| data.receiver_port() > rule.receiver_port_end())
return false;
if (data.tcp() != rule.tcp() && rule.tcp() <= 255)
return false;
return true;
}
vector<int32_t> DoMatch(vector<Data> &datalist, vector<Rule> &rulelist)
{
vector<int32_t> ans;
for (Data& data:datalist)
{
bool tag = true; //标记是否有匹配的
for (size_t i = 0; i < rulelist.size(); ++i)
{
Rule rule = rulelist.at(i);
if (!check(data, rule))
continue;
else
{
int idx = static_cast<int>(i);
ans.push_back(idx);
tag = false;
break;
}
}
if (tag)
ans.push_back(-1);
}
return ans;
}
- 其余函数功能具体代码可移步Github。
答案检查与性能分析
时间复杂度
主要的可控开销应该是匹配阶段,这里我的做法太暴力了,内外层遍历两个vector,时间复杂度为\(O(n^2).\)
答案检查
我使用的是VS Code里自带的文件内容比较。
但是作业要求的输出格式竟然跟他自己出的an1.txt不一样!
作业要求:
ans1.txt:
这个真的也给人体验很不好!于是只能先按照ans1.txt里那样只输出匹配的位置,确认没错后再补上输出"数据包信息".
监测界面如下,没有报错:
运行时间统计
这里我选用的是time.h标准库,检查的是packet1.txt的匹配时间(事先要先把交互式注释掉,硬编码文件名在程序里)代码如下:
#include<iostream>
#include<fstream> //文件读取写入流所用标准库
#include<string>
#include<vector>
#include<time.h>
#include"Data.h"
#include"Rule.h"
#include"match_util.h"
using std::cin;
using std::cout;
using std::string;
using std::endl;
using std::vector;
int main()
{
clock_t beg, end;
beg = clock();
EnquireUser();
beg = clock();
cout << "Total:" << (double)(end - beg) / CLOCKS_PER_SEC << "s" << endl;
system("pause");
return 0;
}
时间结果如下:
这是一个数据集匹配的时间,那么可以计算完成全部五份数据集的时间约为4.605s,即4605ms。
优化思考(未实现)
可以看出匹配效率还是很低的。我想到的一种优化方式是采用记忆化的方式另外设空间储存规则集匹配的答案,因为观察样本数据集文件可以发现有很多条都是重复(完全一样)的,如果能成功实现这样的记忆化,那么碰到之前已经计算过答案的就无需再做遍历规则集可以O(1)直接给出答案,设数据集规模M,非重复的有R条,规则集规模N,那么时间复杂度可以从O(MN)变为O(RN),考虑实际情况重复的情况会很多,所以这样的优化效果应该是较明显的。
最终我没有能够实现这个优化方案,因为如何标记以高效判定数据集中的某两条数据是否完全相同超出我的能力范畴。如果简单的给数据集添加一个成员变量储存答案,回溯已经计算过的规则集比对ip,端口等信息是否完全相同,那么这样的检查过程实际上也做出了一次遍历,这样的时间复杂度实际上约为O(RRN/2)而非理想的O(R*N).当数据集不重复的数据规模大规则集规模小的时候很可能会做出反向优化。
遇到的困难
-
上文提到过的,CIDR地址概念在题目中的缺失,让我走了很多弯路。
-
文件输入,我在使用C++ STL的fstream中遇到了很多问题,ifstream有时候会多读最后一个空行,有时候又不会,毫无规律可循。上网搜索办法五花八门,均无明显作用,我只能手动标记来减少读入错误的情况。以前别人说C++ STL残疾我不信,现在我信了。很难不让人怀疑这个标准库本身的实现是否就存在问题。
-
Git操作对于VS不是很友好,一开始一直无法成功add.后来参考了一篇文章才顺利解决。<【笔记】vs2015 使用GIT的时候 “Could not open '***.VC.opendb'”>
-
上文提到过,最后的命令行操作设置没能查到用fstream该如何实现,或许真的要用cin代替ifstream。
结语
我能力有限,只能实现到这个程度,欢迎批评指正。