避免造轮子,从造轮子开始。 里面都是一些简短的好用的与具体项目无关的工具函数或者工具类。 不依赖三方库,且跨平台,支持c++14及以上,开箱即用。
软件架构说明
从gitee上拉下来
放入你的工程文件中
添加到工程目录里,如果是用vs,建议使用新建筛选器的方式添加。如果是qt
则建议使用pri方式添加。
对于msvc,需要添加宏
_CRT_SECURE_NO_WARNINGS _WINSOCK_DEPRECATED_NO_WARNINGS _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS _CRT_NONSTDC_NO_DEPRECATE
来避免一些独有的警告或者错误
对于TCP/UDP,如果要在windows下使用,需要添加库-lws2_32 -liphlpapi
对于日志类,如果在LogMod.cpp中定义了宏USE_FILESYSTEM,则需要在
声明文件Utils.h
size_t ArraySize(T)
编译期计算数组大小的函数,
uint32_t MakeUInt32(uint16_t low, uint16_t high)
用两个16位整数合并成一个无符号的32位整数
int32_t MakeInt32(uint16_t low, uint16_t high)
用两个16位整数合并成一个有符号的32位整数
template <typename T> constexpr T ByteSwap(T source)
交换字节,可用于大小端互换,支持8位,16位,32位,64位的无符号及有符号整数,float,double的字节交换
template <typename T> inlineconstexpr T ToBigEndian(T source)
大小端转换函数,共4个,这里只例举了一个
inline constexpr uint32_t ToNetOrderL(uint32_t source)
主机序与网络序转换函数共4个,用于替换ntohs,ntohl,htons,htonl4个函数,这里重写的原因是为了减少跨平台头文件依赖和库依赖。
CHECK_FORMAT(str,arg)
格式化字符串编译期检测宏,仅gcc下有效
DISABLE_COPY(Class)
删除类的默认复制型构造和赋值函数
DEFAULT_MOVE(Class)
定义类的默认移动型构造和赋值函数,仅支持运用了pImpl机制的类
Q_DECLARE_PRIVATE(Class) Q_DECLARE_PUBLIC(Class)
qt中常用的技术,用于隐藏类的细节,减少继承类的大小。具体可自行查阅qt中的资料
DEBUGSOCK(name)
打印当前的网络错误,name为用到的网络相关函数
INIT_WIN_SOCK CLEAN_WIN_SOCK
初始化及清除网络函数。
声明文件ThreadPool.h
提供了一个HeaderOnly的线程池
使用示例
int main(){
tool::ThreadPool pool(8);//创建含8条线程的线程池
提交一个普通任务
auto f1 = pool.AddTask([](){
return 1;
});
f1.get();//获取提交任务的计算结果
return 0;
}
声明文件NetAdress.h
网络地址相关
transAddr()
包含两个重载函数,用于主机序32位整数和ip地址字符串之间的互换
uint16_t HtoNs(uint16_t value)
共4个函数,用于主机序和网络序之间的整数互换,这里之所以要重新定义主要是为了跨平台,且不再需要include跨平台相关的头文件。
class NetAddress
一个用于存储ipv4地址的类,可以使用多种常见的方式构造,包括(0x00000000,10000)或者("127.0.0.1",10000)或者("127.0.0.1:10000")或者(AddrID)或者(const sockaddr *)
这里的AddrID定义位64位无符号整数,其内部结构为ip占据4字节,port占2字节,最后2字节为0。可用来当作网络地址的Hash值
可以使用toKey()
和toString()
来获取当前网络地址的64位Key和字符串表示
声明文件:TcpSocket.h
Tcp相关,包含一个客户端和一个服务端。
这里多说一句,TCP和UDP中的四个类,请尽量不要用到new/delete的来构造,把这些类当作简单类型来使用。
注意notifyForQuit()
这个函数,这个函数用来通知当前的类准备退出,调用后,会在合适的时候安全退出内部的线程,关闭连接。
构造函数中包含一个数据回调函数。
当调用sendData()函数时,会像服务端当前连接中的所有客户端发送此消息。
该tcp中设置的keep_alive间隔为5s。每有一个连接进来都会创建一个新线程来监听数据。
示例
#include "tools/tcpsocket.h"
void connCb(tool::net::NetAddress addr, bool state)
{
if (state)
printf("new client[%s] connected\n",addr.toString().data());
else
printf("client[%s] disconnected\n", addr.toString().data());
}
int main()
{
auto recvCb = [](const char* data, int size) {
printf("server recv data[%d]:%s",size,data);
};
tool::net::TcpServer server;
server = tool::net::TcpServer(recvCb,connCb);
server.connect("127.0.0.1",8888,1);//创建服务端
while (getch() != 'q');
return 0;
}
构造函数中包含两个回调函数,第一个回调函数是数据回调函数,当有客户端发来数据时触发,第二个回调函数是连接状态回调函数,每当有新的客户端连接或者旧的客户端断开就会触发此回调。
当调用sendData()函数时,会像服务端当前连接中的所有客户端发送此消息。
该tcp中设置的keep_alive间隔为5s。
示例,该示例每隔一秒向服务端发送一次消息,10次后停止发送。
#include "tools/tcpsocket.h"
#include <thread>
int main()
{
tool::net::TcpClient client("127.0.0.1",8888,nullptr);
char data[] = "hello server\n";
for(int i = 0;i < 10;++i){
client.sendData(data,sizeof(data));
std::this_thread::sleep_for(std::chrono::seconds(1));
}
while (getch() != 'q');
return 0;
}
声明文件UdpSocket.h
这里的Udp使用的是组播,且不是以客户端和服务端来区分,而是以UdpRecver和UdpSender来区分,一个只用来接收组播地址的数据,一个只用来向组播地址发送数据,使用方式和TcpSocket类似,这里就不再赘述了。
对应头文件StreamParser.h
流数据解析相关
std::string ToHex(const std::string& buffer)
将字符串按ASCII码转换为16进制的字符串,如"hello"->"68656C6C6F"
std::string FromHex(const std::string& buffer)
将16进制格式的字符串转换为ASCII格式的字符串,如"68"->"h"
二进制数据流生成类,这是一个可以用来简单序列化数据的类。将数据以二进制的形式放入流而不是以文本的形式,支持基本类型,和容器类,
示例,示例中展示了如何将一个非POD数据序列化,并通过网络发送出去,
#include "tools/StreamParser.h"
struct Student{
std::string name;
int32_t id;
std::vector<int64_t> phone;
std::string address;
};
int main()
{
tool::BinaryStream bs;
Student stu;
stu.name = "Tom";
stu.id = 123456;
stu.phone = {123456789,987654321};
stu.address = "Null Street";
bs << stu.name << stu.id << stu.phone << stu.address;
tool::net::TcpClient client("127.0.0.1",8888,nullptr);
client.sendData(bs.Data(),bs.Size());
return 0;
}
一个简单易用的数据提取工具,可用于tcp中的粘包或者文件读取,总共只有三个接口。
void RegisterUpdator(std::function<void(const char*, int)> functor)
注册数据回调函数,当我们从数据流中提取出一条完整的数据,通过这个回调通知。
void InstallParser(std::function<int(const char*, int)> functor)
注册数据解析函数,可以自定义如何从数据中提取数据,大多数时候配合第四条中的函数使用就行了
void Update(const char* data, int size)
数据更新函数,每当收到新数据,通过这个接口传递数据。
template<typename HTType, HTType Head, HTType Tail, typename SizeType>int UpdateCommonPackage(const char* data, int size)
这个函数是一个模板函数,由于模板声明过长,这里并没有放完整,简单来说,如果一个数据
满足(Head+Size+...+Tail)这样的形式,我们就可以使用这个函数来解析,其中的模板参数HTType表示帧头帧尾
的类型,后面是帧头帧尾的参数,而SizeType则是紧接着帧头后表示长度的类型,需要注意的是,这里的长度
是指含帧头帧尾的一共的字节数。
输入参数为带解析的数据,输出参数为解析出来的包的长度,为0表示数据长度不够,为-1表示数据错误。
示例中展示了如何创建一个服务器,并解析客户端发送过来的tsmr2期协议的数据。
#include "tools/tcpsocket.h"
void connCb(tool::net::NetAddress addr, bool state)
{
if (state)
printf("new client[%s] connected\n",addr.toString().data());
else
printf("client[%s] disconnected\n", addr.toString().data());
}
#include "tools/StreamParser.h"
int main()
{
using namespace std::placeholders;
tool::StreamParser sp;
sp.InstallParser(tool::UpdateCommonPackage<uint32_t,0x5A5A5A5A,0xA5A5A5A5,uint32_t>);
sp.RegisterUpdator([](const char*,int) {
printf("recv a package\n");
});
tool::net::TcpServer server(std::bind(&tool::StreamParser::Update, &sp, _1, _2),connCb);
server.connect("127.0.0.1", 8888,1);
while (getch() != 'q');
return 0;
}
12
日志类的相关声明可在头文件LogMod.h中查看
int64_t TimeStamp()
返回时间戳,这里的时间戳是UTC时间,毫秒精度。
std::string TimeToString(int64_t timeStamp, const std::string& format = "%Y-%m-%d %H:%M:%S")
将时间戳按指定格式转换为字符串
std::string TimeMsToString(int64_t timeStamp, const std::string& format = "%Y-%m-%d %H:%M:%S.%g")
将时间戳按指定格式转换为字符串,支持毫秒精度,不过这个函数目前不太完善,会忽略时间中最前面的0,无法固定宽度
比如如果是01:01.001则会输出为1.1.1
std::vector<std::string> GetFilesFromDir(const std::string& dirName)
获取指定路径下的文件,在windows下支持使用通配符搜索,linux下暂无此功能。
void CreatePathForFile(const std::string& fileName)
为一个带完整路径名的文件创建路径(如果这个路径不存在),在LogMod.cpp中有一个宏USE_FILESYSTEM,如果启用的话,
会使用c++17中的filesystem来完成4,5函数,在这个模式下,该函数可以创建多个不存在的路径,如果不定义这个宏,则采用跨平台函数创建,仅支持创建一层路径。
class Timer
一个计算耗时的类。支持微秒精度
日志内部中共有两个类,AsyncLog和SyncLog,请不要在工程中直接包含两个对应的头文件。而是只包含LogMod.h
可通过是否定义宏USE_ASYNC_LOG来选择使用哪种日志。
SyncLog属于比较老旧的版本,内部不会创建线程,结构比较简单,也不支持自定义格式输出。
建议使用AsyncLog,异步日志,启用后,内部会创建一个线程,定期同步日志。
日志共分为6个等级,Trace,Debug,Info,Warning,Error,Success,默认全部开启,如果要全部关闭,可定义宏LOG_NO_OUTPUT
如果要取消其中某一级,可通过定义LOG_NO_XXX的方式完成,如LOG_NO_DEBUG,定义这个宏后,则debug打印会失效。
LOG_T,LOG_D,LOG_I,LOG_W,LOG_E,LOG_S(format , ...)
格式话日志输出宏
inline void InstallLogMessageScreenState(bool state)
修改日志的屏幕输出状态,初始时默认开启。
inline void InstallLogMessageFileState(bool state, const std::string& fileName = "")
修改日志的文件输出状态,初始时默认关闭。当开启日志时,第二个参数表示日志的输出文件,如果使用默认参数,则会在当前文件下创建
一个log文件夹,在其中生成一个含时间戳的log文件。
inline void InstallLogMessageHandler(std::function<void(const char* data, int size)> writer,
std::function<void()> flusher = nullptr,
std::function<void()> closer = nullptr)
重定向日志输出,共需要传递三个回调函数,分别是写回调,刷新回调,和关闭回调。比如我们有一个QTextEdit控件,我们需要把日志输出
到这个控件里,则可以这样做,
InstallLogMessageHandler([pEdit](const char* data,int size){``pEdit.append(data);
});`
void InstallLogMessageFormat(const char* data)
设置日志前缀输出格式,默认没有,日志前缀指的是每条日志前的附加打印,包括时间戳,类型,文件,行数,函数名这几种。
对应格式串为{time} {type} {file} {line} {function}。例如
InstallLogMessageFormat("[{time} {file} {line} {function} {type}]");
LOG_D("hello world");
则输出 [2021-09-01 09:37:13.747 main.cpp 130 main DEBUG]hello world
void InitLogMessage(int interval_ms = 3000)
初始化日志,必须调用该语句后才能输出日志。参数表示刷新时间,单位毫秒,即每隔多长时间把相应的数据从缓存中取出。建议根据实际情况设置1~5s就够了。
一个简单的使用日志的示例,展示了如何初始化日志,创建了一个TCP服务端,并使用日志打印客户端发送过来的消息。
#include "tools/tcpsocket.h"
#include "tools/LogMod.h"
int mains()
{
InitLogMessage(1000);
InstallLogMessageFileState(true);
#ifdef NDEBUG
InstallLogMessageFormat("[{time} {type}]");
#else
InstallLogMessageFormat("[{time} {file} {line} {function} {type}]");
#endif
LOG_D("hello world");
auto recver = [](const char*data,int) {
LOG_I("server recv:%s",data);
};
tool::net::TcpServer server(recver, nullptr);
server.connect("127.0.0.1", 8888,1);
while (getch() != 'q');
return 0;
}
放入的代码应该是用来解决某一类通用的功能的,而不是与具体工程相关的;
代码必须是跨平台的,头文件中不能include平台相关的头文件,所有功能必须放置在namespace tool下,不能使用三方库。
每个添加进去的新模块应该是独立的,即除了Utils.h,该模块不能依赖于其他模块,这样做的目的是,如果某个使用者只需要其中的一个功能
那么他可以直接把功能相关的两个文件拉入工程而不是要把整个tools都拉进来。
Fork 本仓库
新建 Feat_xxx 分支
提交代码
新建 Pull Request
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。