Linux-> TCP 编程2
目录
本文说明
一:command程序的几个问题
二:代码
1:TcpServer.hpp
2:CommandExcute.hpp
3:Main.cc
4:MainClient.cc
5:safe.txt
6:InetAddr.hpp
7:Log.hpp
三:效果
本文说明
本文实现了一个TCP编程下的command程序,客户端发送命令,会在服务端去执行该命令,然后客户端会显示在服务端执行该命令的结果,并且是在多线程版本下实现的command程序
而TCP相关的接口已经在上篇博客中见过了,整体的代码逻辑是类似的,不再赘述,如下:
https://blog.csdn.net/shylyly_/article/details/152226621
一:command程序的几个问题
Q1:我们的指令从何而来?
A1:和UDP的字典程序一样,我们创建一个文件safe.txt,文件中存储的就是我们允许客户端输入的指令,到时候,再把该文件的内容导入到set容器中即可
Q2:是否需要手动的fork() + pipe() + exec() + dup2() ?
A2:
1:不需要也不能这么做,会使整个服务器进程崩溃!因为我们目前是在多线程的情况下去实现的command程序,所以当某个线程调用 fork()
时,会复制整个进程的所有线程状态,但只复制了调用 fork()
的这一个线程,其他线程都"消失"了。然后 exec()
会替换整个进程的地址空间,导致:①所有其他线程突然死亡,②进程状态不一致,③资源泄漏,④整个服务崩溃!而如果只有单线程则可以使用 fork() + pipe() + exec() + dup2()这一套逻辑,在之前的自定义shell中就实现过
自定义shell博客:https://blog.csdn.net/shylyly_/article/details/149660081
2:系统给我们提供了popen系列的接口,popen内部也是使用 fork()+exec()
,但关键区别在于:popen()
在库函数层面处理了多线程安全问题,现代系统的 popen()
实现会:①:在fork前锁定必要的资源,②:处理多线程环境下的特殊状况,③:提供更安全的进程创建,所以我们只需直接使用系统已经提供好的接口即可!
3:所以其实popen系列的接口和我们之前的 fork() + pipe() + exec() + dup2() 是类似的,只不过他在执行这套逻辑的时候,内部安全地处理了 fork 和 exec 的流程。
Q3:指令有选项怎么提取主命令?
A3:我们只需设定分隔符SEP为空格,然后查找到第一个空格,那么这个空格之前的内容就是主命令,比如"ls -l"则会截取到 ls 主命令,而如果没找到分隔符,则证明该命令没有带选项
Q4:指令的安全性怎么确保?
A4:获取到客户端输入的指令之后,我们应该在白名单safe.txt中查找该指令,找到了证明其是安全的指令,才回去执行该指令,反之不安全,驳回
Q5:松耦合的实现逻辑
A5:
1:和之前几篇的socket编程的逻辑一样,我们的服务类依旧是松耦合的,也就是我们的服务类是回调一个函数,该函数位于其他类中的,所以当我们服务类接收到一条来自客户端的指令的时候我,服务类会是把该指令作为参数传递到回调函数中的,回调函数会调用另一个类中的成员函数
2:在之前的UDP编程博客中,我们常常不需要考虑多线程,因为多线程往往是因为TCP的监听连接后的死循环服务而存在的,因为死循环,所以循环不结束,则无法进行下一次的监听连接!而UDP没有监听连接,所以只需循环地从fd套接字中接收信息和发送信息即可!而发送信息和接收信息等功能都在Server函数中,如果此时需要松耦合,则在Server中再调用回调函数即可
3:而作为多线程,我们的松耦合会显得复杂一点,因为线程必定需要一个线程执行的函数,所以我们在线程的执行函数中,先调用Server函数,该函数内部再去调用回调函数!仅仅多绕了一步而已
最后介绍一下popen和pclose函数:
popen
是标准 C 库函数,用于创建管道并执行 shell 命令。它封装了 fork()
、pipe()
、exec()
等系统调用的复杂细节。
函数原型:
#include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);
参数说明:
-
command
: 要执行的 shell 命令字符串 -
type
:-
"r"
: 读取模式 - 读取命令的输出 -
"w"
: 写入模式 - 向命令输入数据
-
返回值:
FILE*类型,直接使用文件的读写函数进行读取popen执行命令之后的返回信息即可
pclose() 参数:
-
stream
:popen()
返回的文件指针
二:代码
1:TcpServer.hpp
TcpServer.hpp就是服务端的代码
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>#include "InetAddr.hpp"
#include "Log.hpp"// 枚举错误类型
enum
{SOCKET_ERROR = 1, // 创建套接字错误BIND_ERROR, // 绑定错误LISTEN_ERROR, // 监听错误USAGE_ERROR // 运行程序时输入的main的参数错误
};const static int defaultsockfd = -1; // 设定的创建套接字返回的fd的初始值
const static int gbacklog = 16; // 默认的listen接口的第二个参数的初始值class TcpServer; // 声明一下 方便ThreadData类使用// 线程数据类
class ThreadData
{
public:// 构造函数ThreadData(int fd, InetAddr addr, TcpServer *s) : sockfd(fd), clientaddr(addr), self(s){}public:int sockfd; // 创建连接之后返回的fdInetAddr clientaddr; // 客户端的网络属性结构体TcpServer *self; // 服务类指针
};using func_t = std::function<std::string(const std::string &)>; // 服务器类回调的函数 该函数就是command程序的核心// 服务类
class TcpServer
{
public:// 构造函数 端口号 socket的返回值 是否运行 回调函数TcpServer(int port, func_t func) : _port(port), _listensock(defaultsockfd), _isrunning(false), _func(func){}// 初始化服务端void InitServer(){// 1:创建tcp socket 套接字_listensock = ::socket(AF_INET, SOCK_STREAM, 0);// 创建套接字失败 打印语句提醒if (_listensock < 0){LOG(FATAL, "socket error");exit(SOCKET_ERROR);}// 创建套接字成功 打印语句提醒LOG(DEBUG, "socket create success, sockfd is : %d\n", _listensock);// 2 填充sockaddr_in结构struct sockaddr_in local; // 网络通信 所以定义struct sockaddr_in类型的变量memset(&local, 0, sizeof(local)); // 先把结构体清空 好习惯local.sin_family = AF_INET; // 填写第一个字段 (地址类型)local.sin_port = htons(_port); // 填写第二个字段PORT (需先转化为网络字节序)local.sin_addr.s_addr = INADDR_ANY; // 填写第三个字段IP (直接填写0即可,INADDR_ANY就是为0的宏)// 3 bind绑定// 我们填充好的local和我们创建的套接字进行绑定(绑定我们接收信息发送信息的端口)int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));// 绑定失败 打印语句提醒if (n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}// 绑定成功 打印语句提醒LOG(DEBUG, "bind success, sockfd is : %d\n", _listensock);// 4 监听连接// tcp是面向连接的,所以通信之前,必须先建立连接,而在连接之前 又需要先监听// 监听(第二个参数默认为16 在上文已被设置)n = ::listen(_listensock, gbacklog);// 监听失败 打印语句提醒if (n < 0){LOG(FATAL, "listen error");exit(LISTEN_ERROR);}// 监听成功 打印语句提醒LOG(DEBUG, "listen success, sockfd is : %d\n", _listensock);}// Service(服务函数)// 负责监听连接成功之后的数据的发送与接收void Service(int sockfd, InetAddr client){// 来到这里代表监听连接已经成功 打印语句提醒链接的客户端的IP和PORT 以及连接accept返回的fdLOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);// 创建用户的前缀标识符 这样在服务端可以知道是哪个客户端发送的command指令了std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";// 开始循环的接收客户端的发来的指令 并且调用回调函数把结果返回去while (true){char inbuffer[1024]; // 对方端发来的信息 存储在inbuffer中ssize_t n = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); // 读取客户端发来的信息 放进inbuffer中// 读取成功if (n > 0){inbuffer[n] = 0; // 先把inbuffer的n下标置为0 使得读取数组内容时及时停止std::cout << clientaddr << inbuffer << std::endl; // 打印 前缀+指令 就知道是哪个客户端发的哪个指令std::string result = _func(inbuffer); // 调用回调函数_func 得到执行指令后的返回值resultsend(sockfd, result.c_str(), result.size(), 0); // 将返回值result返回给客户端}// 读取失败1: 客户端退出并且关闭了连接else if (n == 0){// client 退出&&关闭连接了LOG(INFO, "%s quit\n", clientaddr.c_str());break; // 则跳出while循环 其清理干净连接产生的fd}// 读取失败2: 单纯的读取失败else{LOG(ERROR, "read error\n", clientaddr.c_str());break; // 则跳出while循环 其清理干净连接产生的fd}}// 客户端断开连接后的清理工作std::cout << "客户端断开连接,开始清理" << std::endl;::close(sockfd); // 重要:关闭套接字std::cout << "连接已关闭" << std::endl;}// 线程执行的函数static void *HandlerSock(void *args) // IO 和 业务进行解耦{pthread_detach(pthread_self()); // 线程分离 避免主线程等待新线程 导致无法并行ThreadData *td = static_cast<ThreadData *>(args); // 把参数恢复成ThreadData *类型的变量 用于调用Server函数td->self->Service(td->sockfd, td->clientaddr); // 调用server函数 进行在Server函数中回调_func函数delete td; // 回收资源return nullptr;}// Loop函数// 用于连接void Loop(){_isrunning = true;// 进行连接while (_isrunning){struct sockaddr_in peer; // 用于接收客户端的网络属性结构体socklen_t len = sizeof(peer);// 连接函数 返回的值是server函数的参数 因为这个fd才是进行数据发送和接收的fdint sockfd = ::accept(_listensock, (struct sockaddr *)&peer, &len);// 连接失败if (sockfd < 0){LOG(WARNING, "accept error\n"); // 打印语句提醒continue;}// 采用多线程pthread_t t;// 创建一个ThreadData类类型的指针 作为HandlerSock函数的参数 方便调用服务器类的成员变量和成员函数ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);pthread_create(&t, nullptr, HandlerSock, td); // 每个线程都会去执行HandlerSock函数}_isrunning = false;}// 析构函数~TcpServer(){if (_listensock > defaultsockfd)::close(_listensock); // 服务类的析构函数才会真正的关闭监听套接字 上面的close都是关闭每次连接产生的套接字}private:uint16_t _port; // 端口号int _listensock; // 监听套接字bool _isrunning; // 是否运行func_t _func; // 回调函数
};
解释:
①:handler函数在类中的使用,首先handler由于在类中,所以必须是static修饰的,这样才可以符合handler的类型(避免this的干扰),其次我们上次采取的是,handler的参数是this指针,但是今天不行,因为我们的server函数的参数是fd和结构体,这些不是服务器类的成员,而是类中的临时变量,这就意味着,我们handler的参数虽然只有一个,但是该参数需要包含三个东西,所以我们把this,fd,结构体 都封装进一个类ThreadData中即可!在吧ThreadData*作为参数传递给handler!
②:使用线程也会有等待线程从而造成阻塞的问题,所以在每个线程的HandlerSock函数中先进行线程分离即可!
③:多线程之间是共享一份文件描述符表的!这意味着千万不能关闭其他线程的fd,比如主线程的监听套接字,或者其他新线程的acceot的fd!
④:然后线程执行函数中调用了Server函数,该Server函数内部会调用回调函数,参数就是客户端传过来的指令字符串!
2:CommandExcute.hpp
CommandExcute.hpp就是command程序的核心,回调函数就位于此类中,这也是松耦合的体现
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Log.hpp"const std::string sep = " "; // 声明分隔符// Command类
// 指令类(内部的成员函数Excute是Command程序的核心)
class Command
{
private:// LoadConf函数// 负责加载指定路径下的白名单文件中的指令void LoadConf(const std::string &conf){// 打开文件std::ifstream in(conf);// 打开失败if (!in.is_open()){LOG(FATAL, "open %s error\n", conf.c_str()); // 打印语句 提醒return;}// 来到这 代表打开已经成功std::string line; // 读取到的每一行 放进line中// 则开始读取每一行while (std::getline(in, line)){// 把读取到的每一行都打印出来 在服务端显现出来 方便查看LOG(DEBUG, "load command [%s] success\n", line.c_str());// 把每次读取到的line都插入到set容器_safe_cmd中_safe_cmd.insert(line);}in.close(); // 关闭文件}public:// 构造函数 (参数为白名单的路径)Command(const std::string &cond_path) : _cond_path(cond_path){// 调用LoadConf函数 把指定路径下的白名单全部读取加载仅set容器_safe_cmd中LoadConf(_cond_path);}// PrefixCommand函数// 用于提取主命令名 避免选项的干扰std::string PrefixCommand(const std::string &cmd){if (cmd.empty()) // 如果命令为空,返回空字符串return std::string();auto pos = cmd.find(sep); // 查找分隔符sepif (pos == std::string::npos) // 如果找不到分隔符,则代表该命名无选项 则返回整个命令return cmd;else // 找到分隔符,返回分隔符之前的部分return cmd.substr(0, pos);// sep是空格// PrefixCommand("ls -a -l") // 返回 "ls"// PrefixCommand("touch a.txt") // 返回 "touch"// PrefixCommand("pwd") // 返回 "pwd"(没有参数)// PrefixCommand("") // 返回 ""}// SafeCheck函数// 检查命令是否在白名单中 杜绝执行危险指令bool SafeCheck(const std::string &cmd){// 调用 PrefixCommand 获取主命令名std::string prefix = PrefixCommand(cmd); // ls -a -l , touch a.txt// 在 set容器_safe_cmd 中查找该主命令名auto iter = _safe_cmd.find(prefix);// 没找到 则代表是非白名单指令 是不安全的 则返回falseif (iter == _safe_cmd.end())return false;return true; // 找到了则代表是白名单命令 是安全的 返回true}// Excute函数// 执行指令函数std::string Excute(const std::string &cmd) // ls -a -l{// result用于存储执行命令之后的返回值ideastd::string result;// 调用SafeCheck函数 (其内部会完成提取主命令名+判断主命令名是否安全)if (SafeCheck(cmd)){// 来到这里代表主命令名是安全的// popen:创建管道和子进程,子进程执行命令,然后返回值为file*方便读取执行命令后的结果// 则打开管道执行命令 "r"代表以读取模式打开,获取命令输出FILE *fp = popen(cmd.c_str(), "r");// 如果打开失败,返回 "failed"if (fp == nullptr){return "failed";}// 读取命令输出char buffer[1024]; // buffer用于存储命令输出 每次存储一部分 不断的+=到result中while (fgets(buffer, sizeof(buffer), fp) != NULL) // fgets():逐行读取命令输出到buffer中{result += buffer; // 将buffer的内容+=到result字符串中}pclose(fp); // 关闭管道,释放资源}// 命令不安全else{result = "坏人\n"; // 打印坏人}return result; // 把result返回去 服务类中的回调函数就完成了 然后result会被send函数发送到客户端!}// 析构函数~Command(){}private:std::set<std::string> _safe_cmd; // 用于存储安全指令的set容器(自动排序、去重)std::string _cond_path; // 白名单文件的路径
};
解释:此类是整个command程序的核心
①:LoadConf函数用于把白名单的指令全部加载到Command类的成员变量set类型的_safe_cmd中,方便以后在set中查找某个指令,以判断是否位于_safe_cmd中。此函数会被构造函数所调用
②:PrefixCommand函数用于提取接收到的指令中的主命令,对于空指令和仅仅只有主命令的指令或者带有选项的指令,都能够做出正确的判断
③:SafeCheck函数用于检查命令是否在白名单中,杜绝执行危险指令的作用。所以第一步肯定是调用PrefixCommand先获取主命令,然后再在已经通过LoadConf函数填充好的_safe_cmd中去查找判断客户端发送的指令是否为安全指令
④:Excute函数就是服务类中回调函数所调用的函数,也是服务类中的构造函数的参数,所以Excute必定必定包含了SafeCheck函数,而SafeCheck内部又包含了PrefixCommand函数,所以Excute是一个综合性函数,其不但会提取主命令,还会确保指令的安全,然后才会去执行该命令
⑤:而执行命令我们上文说过,不再采取fork() + pipe() + exec() + dup2()的操作,而是使用popen接口和pclose去完成,我们一切都交给了popen去做,我们只需从popen的返回值FILE*类型的值fp中去读取popen执行完指令之后返回的信息即可!
⑥:而popen执行完命令返回的信息可能很多,所以我们不推荐不断地往result字符串去+=,而是一部分一部分的读取到1024大小的buffer中,当buffer满了,我们才+=到result中
3:Main.cc
Main.cc就是调用服务器类的代码文件!
#include "TcpServer.hpp"
#include "CommandExcute.hpp"
#include <memory>//
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " local_port\n"<< std::endl;
}// ./tcpserver port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return 1;}EnableScreen(); // 日志打印在屏幕上uint16_t port = std::stoi(argv[1]); // 从main的参数列表中获取端口号Command cmd("./safe.txt"); // 创建一个Command对象 其会自动加载"./safe.txt"下的白名单指令// 创建一个服务类的回调函数cmdExec 其作为服务类的构造函数的参数// 而cmdExec就是bind的Command类中的成员函数Excutefunc_t cmdExec = std::bind(&Command::Excute, &cmd, std::placeholders::_1);// 创建服务类对象 把回调函数cmdExec作为构造函数的参数传进去即可std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, cmdExec);tsvr->InitServer(); // 创建套接字--->bind绑定--->监听tsvr->Loop(); // 连接--->线程会执行线程函数--->线程函数调用Server函数--->Server函数内部调用回调函数--->回调函数就是cmdExecreturn 0;
}
解释:
唯一需要解释的就是我们的bind,之前我们的bind都是在创建服务器对象的时候,在填写构造函数参数的时候去进行bind的,现在只不过是先把回调函数去进行绑定bind了,再把bind之后的cmdExec函数填写到服务器类对象的构造函数的参数中罢了,都是一个道理
bind的用法博客:https://blog.csdn.net/shylyly_/article/details/151109228
4:MainClient.cc
MainClient.cc是客户端的代码
#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>// 启动客户端 需要用户输入服务端的IP+PORT
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1]; // 从main的参数列表中获取到服务端IPuint16_t serverport = std::stoi(argv[2]); // 从main的参数列表中获取到服务端PORT// 1. 创建socketint sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;exit(2);}// 2. 无需显式的bind OS会自己做// 构建目标主机 也就是服务端的网络属性结构体 方便后续的读取和发送数据.struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());// 客户端向服务端 发起连接请求 接口为connectint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));// 连接失败if (n < 0){std::cerr << "connect error" << std::endl; // 打印语句提醒exit(3);}// 来到这代表连接已经成功 则开始发送和接收信息while (true){std::cout << "Please Enter# "; // 打印请输入提示符std::string outstring;std::getline(std::cin, outstring); // 获取用户在客户端输入的消息 存放进outstringssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); // 向服务器发送信息// send 发送成功if (s > 0){char inbuffer[1024];ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0); // 接收服务器返回的信息// recv接收成功if (m > 0){inbuffer[m] = 0; // 在字符串后面置0std::cout << inbuffer << std::endl; // 打印服务器返回的信息 也就是执行命令之后的执行结果}// recv接收失败else{break;}}// send发送失败else{break;}}::close(sockfd); // 关闭客户端套接字return 0;
}
解释:客户端的逻辑完全和TCP博客1类似,不再赘述
5:safe.txt
safe.txt就是一个白名单文件,里面你也可以自由添加一些安全指令
ls
pwd
tree
whoami
who
uname
cat
touch
接下来的就是一些老生常谈的文件了,日志文件和网络属性结构体文件,在之前的博客中都有提到不再赘述了~~
6:InetAddr.hpp
InetAddr类很简单,就是接收一个网络属性结构体,然后可以通过成员函数打印出单独的IP或PORT,让一个网络属性结构体更细粒度得被使用
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>// 网络属性类 (该类可以返回某个用户对应的网络属性中的 IP 或P ORT 或直接返回网络属性结构体)
class InetAddr
{
private:// 私有方法(获取用户的IP 和 PORT)void GetAddress(std::string *ip, uint16_t *port){// 通通需要转网络字节序*port = ntohs(_addr.sin_port); // 存储进了成员变量_ip中*ip = inet_ntoa(_addr.sin_addr); // 存储进了成员变量_port中}public:// 构造函数InetAddr(const struct sockaddr_in &addr) : _addr(addr){GetAddress(&_ip, &_port); // 内部直接调用私有方法 GetAddress(获取用户的IP 和 PORT) 方便之后可以直接获取属性}// 获取用户的IPstd::string Ip(){return _ip;}// 重载InetAddr类的==符号// 用于判断用户是否在存储用户的vector中bool operator==(const InetAddr &addr){//比较ip和port 同时相等 才认为存在!if (_ip == addr._ip && _port == addr._port){return true;}return false;}// 获取用户的网络属性结构体struct sockaddr_in Addr(){return _addr;}// 获取用户的PORTuint16_t Port(){return _port;}// 析构函数~InetAddr(){}private:struct sockaddr_in _addr; // 成员变量_addr 用于接收一个网络属性结构体std::string _ip; // 成员变量_ip 用来存储用户的IPuint16_t _port; // 成员变量_port 用来存储用户的PORT
};
7:Log.hpp
日志博客:https://blog.csdn.net/shylyly_/article/details/151263351
#pragma once#include <iostream> //C++必备头文件
#include <cstdio> //snprintf
#include <string> //std::string
#include <ctime> //time
#include <cstdarg> //va_接口
#include <sys/types.h> //getpid
#include <unistd.h> //getpid
#include <thread> //锁
#include <mutex> //锁
#include <fstream> //C++的文件操作std::mutex g_mutex; // 定义全局互斥锁
bool gIsSave = false; // 定义一个bool类型 用来判断打印到屏幕还是保存到文件
const std::string logname = "log.txt"; // 保存日志信息的文件名字// 日志等级
enum Level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};// 将日志写进文件的函数
void SaveFile(const std::string &filename, const std::string &message)
{std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << std::endl;out.close();
}// 日志等级转字符串--->字符串才能表示等级的意义 0123意义不清晰
std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}// 获取当前时间的字符串
// 时间格式包含多个字符 所以干脆糅合成一个字符串
std::string GetTimeString()
{// 获取当前时间的时间戳(从1970-01-01 00:00:00开始的秒数)time_t curr_time = time(nullptr);// 将时间戳转换为本地时间的tm结构体// tm结构体包含年、月、日、时、分、秒等字段struct tm *format_time = localtime(&curr_time);// 检查时间转换是否成功if (format_time == nullptr)return "None";// 缓冲区用于存储格式化后的时间字符串char time_buffer[1024];// 格式化时间字符串:年-月-日 时:分:秒snprintf(time_buffer, sizeof(time_buffer), "%d-%02d-%02d %02d:%02d:%02d",format_time->tm_year + 1900, // tm_year: 从1900年开始的年数,需要加1900format_time->tm_mon + 1, // tm_mon: 月份范围0-11,需要加1得到实际月份format_time->tm_mday, // tm_mday: 月中的日期(1-31)format_time->tm_hour, // tm_hour: 小时(0-23)format_time->tm_min, // tm_min: 分钟(0-59)format_time->tm_sec); // tm_sec: 秒(0-60,60表示闰秒)return time_buffer; // 返回格式化后的时间字符串
}// 日志函数-->打印出日志
// 格式:时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数
void LogMessage(int level, std::string filename, int line, bool issave, const char *format, ...)
{std::string levelstr = LevelToString(level); // 得到等级字符串std::string timestr = GetTimeString(); // 得到时间字符串pid_t selfid = getpid(); // 得到PID// 使用va_接口+vsnprintf得到用户想要的可变参数的字符串 存储与buffer中char buffer[1024];va_list arg;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::lock_guard<std::mutex> lock(g_mutex); // 引入C++的RAII的锁 保护打印功能// 保存格式为时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数 的日志信息 到message中std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;// 打印到屏幕if (!issave){std::cout << message << std::endl;}// 保存进文件else{SaveFile(logname, message);}
}// 宏定义 省略掉__FILE__ 和 __LINE__
#define LOG(level, format, ...) \do \{ \LogMessage(level, __FILE__, __LINE__, gIsSave, format, ##__VA_ARGS__); \} while (0)// 用户调用则意味着保存到文件
#define EnableScreen() (gIsSave = false)// 用户调用则意味着打印到屏幕
#define EnableFile() (gIsSave = true)
三:效果
解释:
我们客户端和服务器连接之后,客户端输入的指令,会在服务器上运行,返回的也是服务器的shell的信息,pwd,就显示的是服务端的路径......
注意:删除需要ctrl+回车即可