网站的图片水印怎么做,项目管理软件worktile,设计制作散发寄递销售给予处分,资源seo网站优化排名一 日志的重要性 在之前的编程经历中#xff0c;如果我们的程序运行出现了问题#xff0c;都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上#xff0c;以此来排除程序中的错误。 这在我们以往所写的程序中使用没啥问题#xff0c;但如果出错的是一个不断在运行…一 日志的重要性 在之前的编程经历中如果我们的程序运行出现了问题都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上以此来排除程序中的错误。 这在我们以往所写的程序中使用没啥问题但如果出错的是一个不断在运行中的服务那问题就大了因为服务器是不间断运行中直接将 错误信息 输出到屏幕上会导致错误排查变得极为困难。 其实我们可以将各种 错误信息 组织管理使 每种错误有属于自己的格式包括时间、文件名及行号、错误等级等利于排查问题同时把这些错误写入一个单独的地方便于我们查找和阅读因为错误信息繁多我们一般写入文件中。 这种错误信息的集合我们便称为日志。
所以接下来我们将会实现一个简易版日志器用于定向输出我们的日志信息。
二 可变参数 日志需要我们指定格式并输出依赖于可变参数。 因此我们需要认识一下可变参数的使用主要是几个宏。
#include stdarg.hva_list // 指向可变参数列表的指针va_start() // 将指针指向起始地址va_arg() // 根据类型提取可变参数列表中的参数va_end() // 将指针置为空 示例我们通过可变参数实现参数遍历
#include stdio.h
#include stdarg.hvoid foreach(int format, ...){va_list p;va_start(p, format);// 接下来就是获取其中的每一个参数for(int i 0; i format; i){printf(%d , va_arg(p, int));}printf(\n);// 置空va_end(p);
}int main(){foreach(5, 1,2,3,4,5);return 0;
}这种依靠自己动手的方法比较麻烦我们也可以借助标准库提供的 vsnprintf() 函数进行参数解析
//头文件:
#include stdio.h
//函数声明:
int vsnprintf(char* str, size_t size, const char* format, va_list ap); char *str 把生成的格式化的字符串存放在这里.size_t size , str可接受的最大字符数 ,防止产生数组越界.const char *format , 指定输出格式的字符串它决定了你需要提供的可变参数的类型、个数和顺序。va_list ap , va_list变量. 函数功能将可变参数格式化输出到一个字符数组。
返回值执行成功返回最终生成字符串的长度若生成字符串的长度大于size则将字符串的前size个字符复制到str同时将原串的长度返回不包含终止符执行失败返回负值并置errno.
#includeiostream
#includestdio.h
#include stdarg.husing namespace std;void logtest(int format,...){va_list a;va_start(a,format);char msg[1024];int n vsnprintf(msg,sizeof(msg),%d-%d-%d-%d-%d,a);if(n 0 ){cout可变参数写入失败endl;}coutmsgendl;va_end(a);
}int main(){logtest(5,1,2,3,4,5);return 0;
} 三 日志器的实现
3.1 日志器的等级
日志是有等级的一般分为五级 Debug 用于调试Info 提示信息Warning 警告Errorr 错误Fatal 致命错误 错误等级越高代表影响越大
当然难免有不明确的错误可以再添加一级UnKnow 未知错误。
#includevector
#includestring// 日志等级
enum
{Debug 0,Info,Warning,Error,Fatal
};string getLevel(int level){//可直接用一个容器存储这些日志等级vectorstring vs {Debug, Info, Warning, Error, Fatal, Unknown};//避免非法情况if(level 0 || level vs.size() - 1)return vs[vs.size() - 1];return vs[level];
}3.2 获取时间 接下来是获取时间信息可以通过 time() 函数获取当前时间戳然后再利用 localtime() 函数构建 struct tm 结构体对象这个对象会将时间戳解析成 年月日 时分秒 等详细信息直接获取即可 strcut tm 结构体的信息如下细节年份已经 -1900 了使用时需要加上 1900月份从 0 开始使用时需要 1。
/* Used by other time functions. */
struct tm
{int tm_sec; /* Seconds. [0-60] (1 leap second) */int tm_min; /* Minutes. [0-59] */int tm_hour; /* Hours. [0-23] */int tm_mday; /* Day. [1-31] */int tm_mon; /* Month. [0-11] */int tm_year; /* Year - 1900. */int tm_wday; /* Day of week. [0-6] */int tm_yday; /* Days in year.[0-365] */int tm_isdst; /* DST. [-1/0/1]*/# ifdef __USE_BSDlong int tm_gmtoff; /* Seconds east of UTC. */const char *tm_zone; /* Timezone abbreviation. */
# elselong int __tm_gmtoff; /* Seconds east of UTC. */const char *__tm_zone; /* Timezone abbreviation. */
# endif
};可以这样获取当前时间 // 获取当前时间
string getTime(){time_t t time(nullptr); //获取时间戳struct tm *st localtime(t); //获取时间相关的结构体char buff[128];//将时间按照特定格式写入字符串中snprintf(buff, sizeof(buff), %d-%d-%d %d:%d:%d, st-tm_year 1900, st-tm_mon 1, st-tm_mday, st-tm_hour, st-tm_min, st-tm_sec); return buff;
}
3.3 日志格式 日志的格式我们一般可以自己规定这里我们规定我们日志的格式为 日志等级 [时间] [PID] {消息体} 接下来就是获取进程 PID这个简单直接使用 getpid() 函数获取即可最后是解析参数需要用到 vsnprintf() 函数只要传入缓冲区和 va_list 指针该函数就可以自动解析出参数并存入缓冲区中 。
void logMessage(int level, const char* format, ...){//截获主体消息char msgbuff[1024];va_list p;va_start(p, format); //将 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取va_end(p);}形成测试版日志信息函数
//处理信息
void logMessage(int level, const char* format, ...){//日志格式日志等级 [时间] [PID] {消息体}string logmsg getLevel(level); //获取日志等级logmsg getTime(); //获取时间logmsg [ to_string(getpid()) ]; //获取进程PID//截获主体消息char msgbuff[1024];va_list p;va_start(p, format); //将 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取va_end(p);logmsg { string(msgbuff) }; //获取主体消息printf(%s\n, logmsg); //这里先打印 方便进行测试} 为什么日志消息最后还是向屏幕输出这样组织日志消息的好处是什么 因为现在还在测试阶段等测试完成后可以将日志消息存入文件中做到持久化存储至于统一组织的好处不言而喻能够确保每条日志消息都包含必要信息便于排查错误 3.4 Log.hpp 头文件代码
#pragma once#include iostream
#include string
#include vector
#include cstdio
#include time.h
#include sys/types.h
#include unistd.h
#include stdarg.husing namespace std;enum{Debug 0,Info,Warning,Error,Fatal
};string getLevel(int level){vectorstring vs {Debug, Info, Warning, Error, Fatal, Unknown};//避免非法情况if(level 0 || level vs.size() - 1) {return vs[vs.size() - 1];}return vs[level];
}string getTime(){time_t t time(nullptr); //获取时间戳struct tm *st localtime(t); //获取时间相关的结构体char buff[128];snprintf(buff, sizeof(buff), %d-%d-%d %d:%d:%d, st-tm_year 1900, st-tm_mon 1, st-tm_mday, st-tm_hour, st-tm_min, st-tm_sec);return buff;
}//处理信息
void logMessage(int level, const char* format, ...){//日志格式日志等级 [时间] [PID] {消息体}string logmsg getLevel(level); //获取日志等级logmsg getTime(); //获取时间logmsg [ to_string(getpid()) ]; //获取进程PID//截获主体消息char msgbuff[1024];va_list p;va_start(p, format); //将 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取va_end(p);logmsg { string(msgbuff) }; //获取主体消息coutlogmsgendl;} 3.5 写入程序中
这里我们借用我们上一篇文章写的TCP程序
我们先将client.hpp 文件中的错误信息日志化:
//client.hpp
#pragma once #includeiostream
#includestring
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h
#includecerrno
#includecstring
#include err.hpp
#include unistd.h
#includeLog.hppnamespace My_client{class client{private:/* data *///套接字int _sock;//服务器ipstd::string server_ip;//服务器端口号uint16_t server_port;public:client(const std::string ip,const uint16_t port):server_ip(ip),server_port(port){}~client(){}//初始化客户端void InitClient(){//1 创建套接字_sock socket(AF_INET,SOCK_STREAM,0);if(_sock -1){logMessage(Fatal, Create Socket Fail! %s, strerror(errno));exit(SOCKET_ERR);}logMessage(Debug, Create Sock Succeess! %d, _sock);}// 启动客户端void StartClient(){//填充服务器的sockaddr_int 结构体信息struct sockaddr_in server;socklen_t lensizeof(server);bzero(server,len);server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(server_ip.c_str());// inet_aton(server_ip.c_str(), server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法server.sin_port htons(server_port);//尝试重连五次int n5;while(n){//开始连接int ret connect(_sock,(const struct sockaddr*)server,len);if(ret0){// 连接成功可以跳出循环break;}// 尝试进行重连logMessage(Warning, 网络异常正在进行重连... 剩余连接次数: %d, --n);sleep(1);}// 如果剩余重连次数为 0证明连接失败if(n 0) {logMessage(Fatal, 连接失败! %s, strerror(errno));close(_sock);exit(CONNECT_ERR);//新加错误标识符}// 连接成功logMessage(Info, 连接成功!);// 进行业务处理Service();}// 业务处理void Service(){char buff[1024];std::string who server_ip - std::to_string(server_port);while(true){// 由用户输入信息std::string msg;std::cout Please Enter ;std::getline(std::cin, msg);// 发送信息给服务器write(_sock, msg.c_str(), msg.size());// 接收来自服务器的信息ssize_t n read(_sock, buff, sizeof(buff) - 1);if(n 0) {// 正常通信buff[n] \0;std::cout Client get: buff from who std::endl;}else if(n 0){// 读取到文件末尾服务器关闭了logMessage(Error, Server %s quit! %s, who.c_str(), strerror(errno));close(_sock); // 关闭文件描述符break;}else{// 读取异常logMessage(Error, Read Fail! %s, strerror(errno));close(_sock); // 关闭文件描述符break;}}}};}
连接成功的例子,显然其它日志信息也一样显示在屏幕中 改动server.hpp 头文件中的代码 // server.hpp#pragma once#includeiostream
#includestring
#includefunctional
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h
#includeerr.hpp
#includecstring
#includeunistd.h
#includecerrno
#includeThreadPool.hpp
#includeTask.hpp
#includeLog.hppnamespace My_server{// 默认端口号const uint16_t default_port 1111;//全连接队列的最大长度const int backlog 32;using func_t std::functionstd::string(std::string);//前置声明class server;//包含我们所需参数的类型class ThreadData{public:ThreadData(int sock,const std::stringip,const uint16_tport,server*ptr):_sock(sock),_clientip(ip),_clientport(port),_current(ptr){}public:int _sock;std::string _clientip;uint16_t _clientport;server* _current;};class server{private:/* data *///套接字int _listensock;//端口号uint16_t _port;// 判断服务器是否结束运行bool _quit;// 外部传入的回调函数func_t _func;public:server(const func_t func,const uint16_t port default_port):_func(func),_port(port),_quit(false){}~server(){}//初始化服务器void InitServer(){//1 创建套接字_listensock socket(AF_INET,SOCK_STREAM,0);if(_listensock -1){//绑定失败logMessage(Fatal, Create Socket Fail! %s, strerror(errno));exit(SOCKET_ERR);}logMessage(Debug, Create Sock Succeess! %d, _listensock);//2 绑定端口号和IP地址struct sockaddr_in local;bzero(local,sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);local.sin_addr.s_addr INADDR_ANY;if(bind(_listensock,(const sockaddr*)local,sizeof(local))){logMessage(Fatal, Bind IPPort Fali %s, strerror(errno));exit(BIND_ERR);}//3 开始监听if(listen(_listensock,backlog) -1){logMessage(Fatal, Listen Fail: %s, strerror(errno));//新增一个报错exit(LISTEN_ERR);}logMessage(Debug, Listen Success!);}//启动服务器void StartServer(){while(!_quit){//1 处理连接请求struct sockaddr_in client;socklen_t len sizeof(client);int sock accept(_listensock,(struct sockaddr*)client,len);//2 如果连接失败 继续尝试连接if(sock -1){logMessage(Warning,Accept Fail!: %s,strerror(errno));continue;}// 连接成功获取客户端信息std::string clientip inet_ntoa(client.sin_addr);uint16_t clientport ntohs(client.sin_port);//std::coutServer accept clientip - clientport sock from _listensock success!std::endl;logMessage(Debug,Server accept %s - %d %d from %d success,clientip.c_str(),clientport,sock,_listensock);// 3.构建任务对象 注意使用 bind 绑定 this 指针My_task::Task t(sock, clientip, clientport, std::bind(server::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// 4.通过线程池操作句柄将任务对象 push 进线程池中处理//s//std::coutstd::endlpush Taskstd::endl;My_pool::ThreadPoolMy_task::Task::getInstance()-pushTask(t);}}void Service(int sock,const std::string clientip,const uint16_t clientport){char buff[1024];std::string who clientip - std::to_string(clientport);while(true){// 以字符串格式读取预留\0的位置ssize_t n read(sock,buff,sizeof(buff)-1);if(n0){//读取成功buff[n]\0;logMessage(Debug,Server get: %s from %s,buff,who.c_str());//std::coutServer get: buff from whostd::endl;//实际处理可以交给上层逻辑指定std::string respond _func(buff);write(sock,buff,strlen(buff));}else if(n0){//表示当前读到了文件末尾结束读取//std::coutClient who sock quit!std::endl;logMessage(Error,Client %s %d quit!,who.c_str(),sock);close(sock);}else{// 读取出问题暂时logMessage(Error, Read Fail! %s, strerror(errno));close(sock); // 关闭文件描述符break;} }}};}
示例 3.6 持久化存储
所谓持久化存储就是将日志消息输出至文件中修改 log.hpp 中的代码即可
指定日志文件存放路径打开文件将日志消息追加至文件中
log.hpp 日志头文件
#pragma once#include iostream
#include string
#include vector
#include cstdio
#include time.h
#include sys/types.h
#include unistd.h
#include stdarg.husing namespace std;enum{Debug 0,Info,Warning,Error,Fatal
};static const string file_name TCP.Log; //在当前目录下创建一个TCP.Log文件string getLevel(int level){vectorstring vs {Debug, Info, Warning, Error, Fatal, Unknown};//避免非法情况if(level 0 || level vs.size() - 1) {return vs[vs.size() - 1];}return vs[level];
}string getTime(){time_t t time(nullptr); //获取时间戳struct tm *st localtime(t); //获取时间相关的结构体char buff[128];snprintf(buff, sizeof(buff), %d-%d-%d %d:%d:%d, st-tm_year 1900, st-tm_mon 1, st-tm_mday, st-tm_hour, st-tm_min, st-tm_sec);return buff;
}//处理信息
void logMessage(int level, const char* format, ...){//日志格式日志等级 [时间] [PID] {消息体}string logmsg getLevel(level); //获取日志等级logmsg getTime(); //获取时间logmsg [ to_string(getpid()) ]; //获取进程PID//截获主体消息char msgbuff[1024];va_list p;va_start(p, format); //将 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取va_end(p);logmsg { string(msgbuff) }; //获取主体消息//持久化。写入文件中FILE* fp fopen(file_name.c_str(), a); //以追加的方式写入if(fp nullptr)return; //不太可能出错fprintf(fp, %s\n, logmsg.c_str());fflush(fp); //手动刷新一下fclose(fp);} 示例 四 守护进程 守护进程 的意思就是让进程不间断的在后台运行即便是 bash 关闭了也能照旧运行。守护进程 就是现实生活中的服务器因为服务器是需要 24H 不间断运行的
4.1.会话、进程组、进程 当前我们的程序在启动后属于 前台进程前台进程 是由 bash 进程替换而来的因此会导致 bash 暂时无法使用. 但是我们的server程序此时又没什么用还影响着原本bash进程的使用我们该怎么做呢 如果在启动程序时带上 符号程序就会变成 后台进程后台进程 并不会与 bash 进程冲突bash 仍然可以使用 后台进程 也可以实现服务器不间断运行但问题在于 如果当前 bash 关闭了那么运行中的后台进程也会被关闭最好的解决方案是使用 守护进程 在正式学习 守护进程 之前需要先了解一组概念会话、进程组、进程 分别运行一批 前台、后台进程并通过指令查看进程运行情况 。
sleep 1000 | sleep 2000 | sleep 3000 sleep 100 | sleep 200 | sleep 300ps -ajx | head -1 ps -ajx | grep sleep | grep -v grep其中 会话 SID
进程组 PGID
进程 PID 显然sleep 1000、2000、3000 处于同一个管道中有血缘关系属于同一个 进程组所以他们的 PGID 都是一样的都是 4261 至于 sleep 100、200、300 属于另一个 进程组PGID 为 4308再仔细观察可以发现 每一组的进程组 PGID 都与当前组中第一个被创建的进程 PID 一致这个进程被称为 组长进程。 无论是 后台进程 还是 前台进程都是从同一个 bash 中启动的所以它们处于同一个 会话 中SID 都是 1939并且关联的 终端文件 TTY 都是 pts/1。 会话 进程组 进程 Linux 中一切皆文件终端文件也是如此这里的终端其实就是当前 bash 输出结果时使用的文件也就是屏幕屏幕也是一个文件终端文件位于 dev/pts 目录下如果向指定终端文件中写入数据那么对方也可以直接收到 (关联终端文件说白了就是打开了文件一方写一方读不就是管道吗) 在同一个 bash 中启动前台、后台进程它们的 SID 都是一样的属于同一个 会话关联了同一个 终端 SID 其实就是 bash 的 PID 我们使用 XShell 等工具登录 Linux 服务器时会在服务器中创建一个 会话bash可以在该会话内创建 进程当 进程 间有关系时构成一个 进程组组长 进程的 PID 就是该 进程组 的 PGID。 在同一个会话中只允许一个前台进程在运行默认是 bash如果其他进程运行了bash 就会变成后台进程暂时无法使用让出前台进程这个位置后台进程与前台进程之前是可以进程切换 如何将一个 后台进程 变成 前台进程 首先通过指令查看当前 会话 中正在运行的 后台进程获取 任务号
jobs查看当前会话中所有的后台进程 接下来通过 任务号 将 后台进程 变成 前台进程此时 bash 就无法使用了。
fg 后台进程号 那如何将 前台进程 变成 后台进程
首先是通过 ctrl z 发送 19 号 SIGSTOP 信号暂停正在运行中的 前台进程.
键盘输入 ctrl z然后通过 任务号可以把暂停中的进程变成 后台进程. 4.2 守护进程化
一般网络服务器为了不受到用户登录重启的影响会以 守护进程 的形式运行有了上面那一批前置知识后就可以很好的理解 守护进程 的本质了
守护进程进程单独成一个会话并且以后台进程的形式运行
说白了就是让服务器不间断运行可以直接使用 daemon() 函数完成 守护进程化。
#include unistd.hint daemon(int nochdir, int noclose);参数解读 nochdir 改变进程的工作路径noclose 重定向标准输入、标准输出、标准错误 返回值成功返回 0失败返回 -1
一般情况下daemon() 函数的两个参数都只需要传递 0默认工作在 / 路径下默认重定向至 /dev/null /dev/null 就像是一个 黑洞可以把所有数据都丢入其中相当于丢弃数据 使用 damon() 函数使之前的server.cc 守护进程化
server.cc 服务器源文件
//智能指针头文件
#includememory
#includeserver.hpp
#includestringusing namespace My_server;
// 业务处理回调函数字符串回响其实这里啥也不干
std::string echo(std::string request){return request;
}int main(){// 直接守护进程化daemon(0, 0);std::unique_ptrserver usvr(new server(echo));usvr-InitServer();usvr-StartServer();return 0;
} 现在服务器启动后会自动变成 后台进程并且自成一个 新会话归操作系统管守护进程 本质上是一种比较坚强的 孤儿进程 注意 现在标准输出、标准错误都被重定向至 /dev/null 中了之前向屏幕输出的数据现在都会直接被丢弃如果想保存数据可以选择使用日志。 可见内容被吞噬了舍弃 如果想终止 守护进程需要通过 kill pid 杀死目标进程 。 使用系统提供的接口一键 守护进程化 固然方便不过大多数程序员都会选择手动 守护进程化可以根据自己的需求定制操作 原理是 使用 setsid() 函数新设一个会话谁调用会话 SID 就是谁的成为一个新的会话后不会被之前的会话影响。 #include unistd.hpid_t setsid(void);返回值成功返回该进程的 pid失败返回 -1
注意 调用该函数的进程不能是组长进程需要创建子进程后调用
手动实现守护进程时需要注意以下几点 忽略异常信号0、1、2 要做特殊处理文件描述符进程的工作路径可能要改变从用户目录中脱离至根目录 具体实现步骤如下
1、忽略常见的异常信号SIGPIPE、SIGCHLD
2、如何保证自己不是组长 创建子进程 成功后父进程退出子进程变成守护进程
3、新建会话自己成为会话的 话首进程
4、可选更改守护进程的工作路径chdir
5、处理后续对于 0、1、2 的问题
对于 标准输入、标准输出、标准错误 的处理方式有两种
暴力处理直接关闭 fd 优雅处理将 fd 重定向至 /dev/null也就是 daemon() 函数的做法 这里我们选择后者守护进程 的函数实现如下
Daemon.hpp 守护进程头文件
#pragma once#include iostream
#include cstring
#include cerrno
#include signal.h
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include err.hpp
#include Log.hppstatic const char *path /home/Manta/cpp/Internet/Log/Log1;void Daemon()
{// 1、忽略常见信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2、创建子进程自己退休pid_t id fork();if (id 0)exit(0);else if (id 0){// 子进程创建失败logMessage(Error, Fork Fail: %s, strerror(errno));exit(FORK_ERR);}// 3、新建会话使自己成为一个单独的组pid_t ret setsid();if (ret -1){// 守护化失败logMessage(Error, Setsid Fail: %s, strerror(errno));exit(SETSID_ERR);}// 4、更改工作路径int n chdir(path);if (n -1){// 更改路径失败logMessage(Error, Chdir Fail: %s, strerror(errno));exit(CHDIR_ERR);}// 5、重定向标准输入输出错误int fd open(/dev/null, O_RDWR);if (fd -1){// 文件打开失败logMessage(Error, Open Fail: %s, strerror(errno));exit(OPEN_ERR);}// 重定向标准输入、标准输出、标准错误dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);
}当然相应的错误码也需要更新
err.hpp 错误码头文件
#pragma onceenum
{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR,SETSID_ERR,CHDIR_ERR,OPEN_ERR
};StartServer() 服务器启动函数 — 位于 server.hpp 服务器头文件中 现在服务器在启动后会自动新建会话以 守护进程 的形式运行 杀死守护进程