目录
本文说明
本文实现了一个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()
等系统调用的复杂细节。
函数原型:
cpp
#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就是服务端的代码
cpp
#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; // 创建连接之后返回的fd
InetAddr 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返回的fd
LOG(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 得到执行指令后的返回值result
send(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才是进行数据发送和接收的fd
int 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程序的核心,回调函数就位于此类中,这也是松耦合的体现
cpp
#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); // 查找分隔符sep
if (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);
// 没找到 则代表是非白名单指令 是不安全的 则返回false
if (iter == _safe_cmd.end())
return false;
return true; // 找到了则代表是白名单命令 是安全的 返回true
}
// Excute函数
// 执行指令函数
std::string Excute(const std::string &cmd) // ls -a -l
{
// result用于存储执行命令之后的返回值idea
std::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就是调用服务器类的代码文件!
cpp
#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类中的成员函数Excute
func_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函数内部调用回调函数--->回调函数就是cmdExec
return 0;
}
解释:
唯一需要解释的就是我们的bind,之前我们的bind都是在创建服务器对象的时候,在填写构造函数参数的时候去进行bind的,现在只不过是先把回调函数去进行绑定bind了,再把bind之后的cmdExec函数填写到服务器类对象的构造函数的参数中罢了,都是一个道理
bind的用法博客: https://blog.csdn.net/shylyly_/article/details/151109228
4:MainClient.cc
MainClient.cc是客户端的代码
cpp
#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的参数列表中获取到服务端IP
uint16_t serverport = std::stoi(argv[2]); // 从main的参数列表中获取到服务端PORT
// 1. 创建socket
int 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());
// 客户端向服务端 发起连接请求 接口为connect
int 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); // 获取用户在客户端输入的消息 存放进outstring
ssize_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; // 在字符串后面置0
std::cout << inbuffer << std::endl; // 打印服务器返回的信息 也就是执行命令之后的执行结果
}
// recv接收失败
else
{
break;
}
}
// send发送失败
else
{
break;
}
}
::close(sockfd); // 关闭客户端套接字
return 0;
}
**解释:**客户端的逻辑完全和TCP博客1类似,不再赘述
5:safe.txt
safe.txt就是一个白名单文件,里面你也可以自由添加一些安全指令
cpp
ls
pwd
tree
whoami
who
uname
cat
touch
接下来的就是一些老生常谈的文件了,日志文件和网络属性结构体文件,在之前的博客中都有提到不再赘述了~~
6:InetAddr.hpp
InetAddr类很简单,就是接收一个网络属性结构体,然后可以通过成员函数打印出单独的IP或PORT,让一个网络属性结构体更细粒度得被使用
cpp
#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) 方便之后可以直接获取属性
}
// 获取用户的IP
std::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;
}
// 获取用户的PORT
uint16_t Port()
{
return _port;
}
// 析构函数
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 成员变量_addr 用于接收一个网络属性结构体
std::string _ip; // 成员变量_ip 用来存储用户的IP
uint16_t _port; // 成员变量_port 用来存储用户的PORT
};
7:Log.hpp
日志博客:https://blog.csdn.net/shylyly_/article/details/151263351
cpp
#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年开始的年数,需要加1900
format_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+回车即可