Linux网络编程:Socket编程TCP
目录
Version-1 EchoServer
Init
服务端创建套接字
套接字绑定
服务端监听
lisent函数
netstat查看信息
netstat -t查看tcp信息
netstat -tl查看listen状态tcp信息
netstat -tln主机号显示成数字
netstat -tlnp查看更详细信息
Start
服务端获取连接
accept函数
客户端创建套接字
客户端连接服务器
connect函数
关于客户端绑定
服务端处理请求
read函数
write函数
客户端发送请求
服务器测试
单执行流弊端
修改为多进程版
2.子进程中fork,让孙子进程提供服务
关闭文件描述符的必要性
修改为多线程版
修改为线程池版
Comm.hpp
Cond.hpp
InetAddr.hpp
logger.hpp
Makefile
Mutex.hpp
TcpClient.cc
TcpEchoServer.hpp
TcpServer.cc
Thread.hpp
ThreadPool.hpp
Version-2 添加命令行处理业务
IO模块和服务模块分离
服务模块和任务模块分离
修改服务端
任务模块
popen函数
地址转换函数
关于inet_ntoa
地址转换最佳实践inet_pton
业务代码展示
Comm.hpp
Command.hpp
CommandClient.cc
CommandServer.hpp
InetAddr.hpp
logger.hpp
Main.cc
Makefile
Mutex.hpp
Version-1 EchoServer
Init
服务端创建套接字
我们将TCP服务器封装成一个类,当我们定义出一个服务器对象后需要马上对服务器进行初始化,而初始化TCP服务器要做的第一件事就是创建套接字。
TCP服务器在调用socket函数创建套接字时,参数设置如下:
协议家族选择AF_INET,因为我们要进行的是网络通信。
创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器,SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
协议类型默认设置为0即可。
如果创建套接字后获得的文件描述符是小于0的,说明套接字创建失败,此时也就没必要进行后续操作了,直接终止程序即可。
注:
实际TCP服务器创建套接字的做法与UDP服务器是一样的,只不过创建套接字时TCP需要的是流式服务,而UDP需要的是用户数据报服务。
当析构服务器时,可以将服务器对应的文件描述符进行关闭。
套接字绑定
套接字创建完毕后我们实际只是在系统层面上打开了一个文件,该文件还没有与网络关联起来,因此创建完套接字后我们还需要调用bind函数进行绑定操作。具体可以看上一篇文章。
绑定的步骤如下:
1.定义一个struct sockaddr_in结构体,将服务器网络相关的属性信息填充到该结构体当中,比如协议家族、IP地址、端口号等。
2.填充服务器网络相关的属性信息时,协议家族对应就是AF_INET,端口号就是当前TCP服务器程序的端口号。在设置端口号时,需要调用htons函数将端口号由主机序列转为网络序列。
在设置服务器的IP地址时,我们可以设置为本地环回127.0.0.1,表示本地通信。也可以设置为公网IP地址,表示网络通信。
3.如果使用的是云服务器,那么在设置服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据。此外,由于INADDR_ANY本质就是0,因此在设置时不需要进行网络字节序的转换。
4.填充完服务器网络相关的属性信息后,需要调用bind函数进行绑定。绑定实际就是将文件与网络关联起来,如果绑定失败也没必要进行后续操作了,直接终止程序即可。
由于TCP服务器初始化时需要服务器的端口号,因此在服务器类当中需要引入端口号,当实例化服务器对象时就需要给传入一个端口号。而由于我当前使用的是云服务器,因此在绑定TCP服务器的IP地址时不需要绑定公网IP地址,直接绑定INADDR_ANY即可,因此我这里没有在服务器类当中引入IP地址。
可以发现除了创建套接字的服务方式选择不同,其他的和udp都是一样的。
服务端监听
UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。
因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态。就像是餐饮店老板在等放学的你来吃鸭腿饭的时候,就要提前打开店面,此刻餐饮店老板就处于监听状态。
lisent函数
第二个参数,为全连接队列的长度减去1。全连接队列如何理解,就像餐饮店来了很多人点餐,这些人需要排队点餐,最多只能有6个人排队,那么backlog就设置为5.一般不设置太大(8,16,32)
至此Init函数搞定!\^o^/
netstat查看信息
netstat -t查看tcp信息
netstat -tl查看listen状态tcp信息
netstat -tln主机号显示成数字
netstat -tlnp查看更详细信息
多一列显示PID还有进程名称
Start
服务端获取连接
accept函数
参数说明:
sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
返回值说明:
获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。
后两个参数 和前一篇学到的recvfrom中的后两个参数 是一个理解意思。都是用于获取客户端套接字的。
监听套接字与accept函数返回的套接字的作用:
调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。
监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
就像一个饭店,饭店就堪比一个服务器,店内一桌一桌客人就是客户端,饭店里有负责店外拉客的,也有负责店内招待客人的,负责拉客的就是监听套接字,负责服务客人的就是accept返回的套接字。
服务端在获取连接时需要注意:
1.accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。
2.如果要将获取到的连接对应客户端的IP地址和端口号信息进行输出,需要调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列。
3.inet_ntoa函数在底层实际做了两个工作,一是将网络序列转换成主机序列,二是将主机序列的整数IP转换成字符串风格的点分十进制的IP。
客户端创建套接字
客户端连接服务器
connect函数
发起连接请求的函数叫做connect,该函数的函数原型如下:
参数说明:
sockfd:特定的套接字,表示通过该套接字发起连接请求。
(后两个参数就和sendto里的参数意义一样,都是目标网络套接字信息)
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。
返回值说明:
连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
关于客户端绑定
需要注意的是,客户端不是不需要进行绑定,而是不需要我们自己显示进行绑定操作(bind),当客户端向服务端发起连接请求时,系统会给客户端随机指定一个端口号进行绑定。因为通信双方都必须要有IP地址和端口号,否则无法唯一标识通信双方。也就是说,如果connect函数调用成功了,客户端本地会随机给该客户端绑定一个端口号发送给对端服务器。
此外,调用connect函数向服务端发起连接请求时,需要传入服务端对应的网络信息,否则connect函数也不知道该客户端到底是要向哪一个服务端发起连接请求。
服务端处理请求
现在TCP服务器已经能够获取连接请求了,下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字,因为监听套接字获取到一个连接后会继续获取下一个请求连接,为对应客户端提供服务的套接字实际是accept函数返回的套接字,下面就将其称为“服务套接字”。
为了让通信双方都能看到对应的现象,我们这里就实现一个简单的回声TCP服务器,服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出,并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出,此时就能确保服务端和客户端能够正常通信了。
read函数
参数说明:
fd:特定的文件描述符,表示从该文件描述符中读取数据。
buf:数据的存储位置,表示将读取到的数据存储到该位置。
count:数据的个数,表示从该文件描述符中读取数据的字节数。
返回值说明:
如果返回值大于0,则表示本次实际读取到的字节个数。
如果返回值等于0,则表示对端已经把连接关闭了。
如果返回值小于0,则表示读取时遇到了错误。
read返回值为0代表对端连接已经关闭
这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:
写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
读端进程不读,写端进程一直写,此时当管道被写满后写端进程就会被挂起,因为此时空间没有就绪。
写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。
这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。
write函数
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
buf:需要写入的数据。
count:需要写入数据的字节个数。
返回值说明:
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。
客户端发送请求
服务器测试
单执行流弊端
当我们仅用一个客户端连接服务端时,这一个客户端能够正常享受到服务端的服务。
但在这个客户端正在享受服务端的服务时,我们让另一个客户端也连接服务器,此时虽然在客户端显示连接是成功的,但这个客户端发送给服务端的消息既没有在服务端进行打印,服务端也没有将该数据回显给该客户端。
只有当第一个客户端退出后,服务端才会将第二个客户端发来是数据进行打印,并回显该第二个客户端。
通过实验现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器,这个服务器一次只能为一个客户端提供服务。
当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端。
为什么服务器在服务一个客户端的时候,别的客户端也能连接成功?
当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了。
实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。
如何解决?
单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的,此时就要引入多进程或多线程。
下面我们将其修改为多进程的
修改为多进程版
但如此我们父进程是阻塞等待子进程退出的,这不还是串型,错误的
我们期望子进程处理IO时,父进程回到accept继续获取新连接
解决方案
1.signal(最佳实践)#include<signal.h>
signal(SIGCHLD,SIG_IGN);//忽略子进程退出信号,系统自动回收子进程资源防止僵尸进程
在Start函数开头写上这段代码
2.子进程中fork,让孙子进程提供服务
在子进程完成任务之前fork一次,如果fork大于0,子进程直接退(父进程也就无需等待了,看似是阻塞的但是可以立即返回,然后因为我们是在while循环里的,父进程又会回到开头,继续获取新的连接),往后执行IO的是子进程的子进程:孙子进程,新的设计模式get。
而且,孙子进程我们无需等待它退出
而由于子进程创建完孙子进程后就立刻退出了,因此实际为客户端提供IO服务的孙子进程就变成了孤儿进程,该进程就会被系统领养,当孙子进程为客户端提供完服务退出后,系统会自动回收孙子进程,所以服务进程(父进程)是不需要等待孙子进程退出的,而且父进程在一收到子进程退出信息后就立马回到开头,继续获取新的连接,多么伟大的子进程和孙子进程。
注意:我们需要关闭一些文件描述符
服务进程(爷爷进程)调用accept函数获取到新连接后,会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程,而爸爸进程又会调用fork函数创建出孙子进程,然后再将文件描述符表继承给孙子进程。
而父子进程创建后,它们各自的文件描述符表是独立的,不会相互影响。因此服务进程在调用fork函数后,服务进程就不需要再关心刚才从accept函数获取到的文件描述符了,此时服务进程就可以调用close函数将该文件描述符进行关闭。
同样的,对于爸爸进程和孙子进程来说,它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的,因此爸爸进程可以将监听套接字关掉。
关闭文件描述符的必要性
1.对于服务进程来说,当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),如果服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少。
2.而对于爸爸进程和孙子进程来说,还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作,此时就会对监听套接字当中的数据造成影响。
总结一下:就是关闭无用fd防止误操作,以及规避fd泄漏。
demo展示:
修改为多线程版
值得注意的是新线程不能关闭历史fd,因为所有线程共享一张fd表,而进程的fd表是相互独立的所以才会有关闭fd的操作。
多进程、多线程的不足
1.效率问题:获取到新连接后才开始需要创建进程线程。就像进餐馆吃饭,顾客点好后你才开始炒很影响效率,所有会有预制菜的出现。
每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样做不仅麻烦,而且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程。
2.执行流个数没有上限,服务器很容易爆,所以多执行流只能解决小型问题,达不到工业需求。为了解决这一点我们可以引入进程池。
如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也迟迟得不到应答。
修改为线程池版
阶段代码演示:
Comm.hpp
#ifndef __COMMAND_HPP__
#define __COMMAND_HPP__#include<iostream>enum
{OK,SOCKET_CREATE_ERROR,SOCKET_BIND_ERROR,SOCKET_LISTEN_ERROR,SOCKET_CONNECT_ERROR,FORK_ERROR,
};#endif
Cond.hpp
#pragma once#include"Mutex.hpp"class Cond
{
public:Cond(){pthread_cond_init(&_cond, nullptr);}void Wait(Mutex& lock){pthread_cond_wait(&_cond, lock.Get());}void NotifyOne(){pthread_cond_signal(&_cond);}void NotifyAll(){pthread_cond_broadcast(&_cond);}~Cond(){pthread_cond_destroy(&_cond);}
private:pthread_cond_t _cond;
};
InetAddr.hpp
#pragma once// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define Conv(addr) ((struct sockaddr*)&addr)class InetAddr
{
private:void Net2Host(){_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);}void Host2Net(){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = inet_addr(_ip.c_str());}public:// 网络风格地址构造InetAddr(const struct sockaddr_in &addr): _addr(addr){Net2Host();}// 主机地址风格构造InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"): _port(port), _ip(ip){Host2Net();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr* Addr(){return Conv(_addr);}socklen_t Length(){return sizeof(_addr);}std::string ToString(){return _ip+"-"+std::to_string(_port);}bool operator==(const InetAddr&addr){return _ip==addr._ip&&_port==addr._port;//return _ip==addr._ip;}~InetAddr(){}private:struct sockaddr_in _addr; // 网络风格地址// 主机风格地址std::string _ip;uint16_t _port;
};
logger.hpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"enum class LoggerLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};std::string LoggerLevelToString(LoggerLevel level)
{switch (level){case LoggerLevel::DEBUG:return "Debug";case LoggerLevel::INFO:return "Info";case LoggerLevel::WARNING:return "Warning";case LoggerLevel::ERROR:return "Error";case LoggerLevel::FATAL:return "Fatal";default:return "Unknown";}
}std::string GetCurrentTime()
{// 获取时间戳time_t timep = time(nullptr);// 把时间戳转化为时间格式struct tm currtm;localtime_r(&timep, &currtm);// 转化为字符串char buffer[64];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,currtm.tm_hour, currtm.tm_min, currtm.tm_sec);return buffer;
}class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &logmessage) = 0;
};// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lockguard(&_lock);std::cout << logmessage << std::endl;}}private:Mutex _lock;
};const std::string default_dir_path_name = "log";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const std::string dir_path_name = default_dir_path_name,const std::string filename = default_filename): _dir_path_name(dir_path_name), _filename(filename){if (std::filesystem::exists(_dir_path_name)){return;}try{std::filesystem::create_directories(_dir_path_name);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}~FileLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lock(&_lock);std::string target = _dir_path_name;target += '/';target += _filename;std::ofstream out(target.c_str(), std::ios::app);if (!out.is_open()){return;}out << logmessage << "\n";out.close();}}private:std::string _dir_path_name;std::string _filename;Mutex _lock;
};class Logger
{
public:Logger(){}void EnableConsoleStrategy(){_strategy = std::make_unique<ConsoleLogStrategy>();}void EnableFileStrategy(){_strategy = std::make_unique<FileLogStrategy>();}class LogMessage{public:LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger): _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << LoggerLevelToString(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.str();}template <typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 时间戳LoggerLevel _level; // 日志等级pid_t _pid; // 进程pidstd::string _filename; // 文件名int _line; // 行号std::string _loginfo; // 一条合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具体做法};LogMessage operator()(LoggerLevel level, std::string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _strategy;
};Logger logger;#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
Makefile
.PHONY:all
all:tcp_client tcp_servertcp_client:TcpClient.ccg++ -o $@ $^ -std=c++17
tcp_server:TcpServer.ccg++ -o $@ $^ -std=c++17 -lpthread.PHONY:clean
clean:rm -f tcp_client tcp_server
Mutex.hpp
#pragma once#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}pthread_mutex_t *Get(){return &_lock;}void Unlock(){pthread_mutex_unlock(&_lock);}~Mutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex *_mutex) : _mutexp(_mutex){_mutex->Lock();}~LockGuard(){_mutexp->Unlock();}private:Mutex *_mutexp;
};
TcpClient.cc
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include"Command.hpp"
#include"InetAddr.hpp"void Usage(std::string proc)
{std::cerr << "Usage:" << proc << " serverip serverport" << std::endl;
}//./tcp_client serverip serverport
int main(int argc,char* argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"create client sockfd error"<<std::endl;exit(SOCKET_CREATE_ERROR);}//tcp_client不用显示bind//客户端自己的socket地址,让本地os自主随机选择,尤其是端口号//客户端向目标服务器发送连接请求即可InetAddr server(server_port,server_ip);if(connect(sockfd,server.Addr(),server.Length())!=0){std::cerr<<"connect server error"<<std::endl;exit(SOCKET_CONNECT_ERROR);}std::cout<<"connect "<<server.ToString()<<" success"<<std::endl;while(true){std::cout<<"Please Enter@ ";std::string line;std::getline(std::cin,line);ssize_t n=write(sockfd,line.c_str(),line.size());if(n>=0){char buffer[1024];ssize_t m=read(sockfd,buffer,sizeof(buffer)-1);if(m>0){buffer[m]=0;std::cout<<buffer<<std::endl;}}}return 0;
}
TcpEchoServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__#include "Command.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"
#include"ThreadPool.hpp"
#include"Cond.hpp"#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;using task_t =std::function<void()>;class TcpEchoServer
{
private:void HandlerIO(int sockfd, InetAddr client){char buffer[1024];while (true){buffer[0] = 0;ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::string echo_string = "server echo# ";echo_string += buffer;LOG(LoggerLevel::DEBUG) << client.ToString() << " say: " << buffer;// tcp面向字节流,文件也是面向字节流write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LoggerLevel::INFO) << "client " << client.ToString() << " quit,me too,close fd: " << sockfd;break;}else{LOG(LoggerLevel::WARNING) << "read client " << client.ToString() << "error, sockfd: " << sockfd;break;}}close(sockfd); // 一定要关闭}public:TcpEchoServer(uint16_t port = gport): _listensockfd(gdefaultfd), _port(port){}void Init(){// 1.创建套接字_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LoggerLevel::FATAL) << "create tcp socket error";exit(SOCKET_CREATE_ERROR);}LOG(LoggerLevel::INFO) << "create tcp socket success: " << _listensockfd;// 2.绑定套接口InetAddr local(_port);if (bind(_listensockfd, local.Addr(), local.Length()) != 0){LOG(LoggerLevel::FATAL) << "bind socket error";exit(SOCKET_BIND_ERROR);}LOG(LoggerLevel::INFO) << "bind socket success" << _listensockfd;// 3.服务端监听// 一个tcp server listen启动之后,服务器已经算运行了,只是还无法进行IO,我们在Start中设计if (listen(_listensockfd, gbacklog) != 0){LOG(LoggerLevel::FATAL) << "listen socket error";exit(SOCKET_LISTEN_ERROR);}LOG(LoggerLevel::INFO) << "listen socket success" << _listensockfd;}// 静态方法无法调用类内成员方法或者成员属性,但我们需要Handler函数// 方法一:Handler放在类外当作普通函数使用// 方法二(推荐):内部类,将外类设计为内部类成员变量,通过内部类对象访问类属性class ThreadData{public:ThreadData(int sockfd, TcpEchoServer *self,const InetAddr&addr) : _sockfd(sockfd), _self(self),_addr(addr){}public:int _sockfd;TcpEchoServer *_self;InetAddr _addr;};// static消除this影响static void *Routine(void *args){ThreadData *td=static_cast<ThreadData*>(args);//线程分离,detach的线程不需要join,结束的时候该线程资源会被系统自动回收pthread_detach(pthread_self());td->_self->HandlerIO(td->_sockfd,td->_addr);delete td;return nullptr;}void Start(){signal(SIGCHLD, SIG_IGN);while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(LoggerLevel::WARNING) << "accept client error";continue; // 跳过这一次,继续“拉客”}InetAddr clientaddr(peer);LOG(LoggerLevel::INFO) << "获取新连接成功,sockfd is: " << sockfd << "client addr: " << clientaddr.ToString();// 服务端处理请求//version-4 线程池ThreadPool<task_t>::GetInstance()->Enqueue([this,sockfd,clientaddr](){this->HandlerIO(sockfd,clientaddr);});// version-3 多线程//多线程共享主线程曾经打开的文件描述符表//主线程和新线程需要关闭历史fd吗?不可以!因为所有线程共享一个表,不是独立的// pthread_t tid;// ThreadData *td = new ThreadData(sockfd, this,clientaddr);// pthread_create(&tid, nullptr, Routine, (void *)td);// 主线程不退,其余线程并型执行,分离线程// version-1 单进程// HandlerIO(sockfd,clientaddr);// version-2 多进程// 创建子进程,子进程可以继承父进程socketfd// pid_t id=fork();// if(id<0)// {// LOG(LoggerLevel::FATAL)<<"资源不足,创建子进程失败";// exit(FORK_ERROR);// }// else if(id==0){// //child,handler// //child也能看到sockfd以及_listensockfd// close(_listensockfd);// if(fork()>0)// {// exit(OK);// }// HandlerIO(sockfd,clientaddr);// exit(OK);// }// else{// //father// //父进程创建子进程,阻塞等待子进程退出->串型 错误的// //我们期望子进程处理IO时,父进程回到accept继续获取新连接// close(sockfd);// pid_t rid=waitpid(id,nullptr,0);// (void)rid;// }}}~TcpEchoServer() {}private:int _listensockfd; // 监听套接字uint16_t _port;
};#endif
TcpServer.cc
#include"TcpEchoServer.hpp"
#include<memory>
void Usage(std::string proc)
{std::cerr << "Usage:" << proc << " localport" << std::endl;
}
int main(int argc,char* argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t serverport=std::stoi(argv[1]);//打印日志选择显示器策略EnableConsoleStrategy();std::unique_ptr<TcpEchoServer> tsvr=std::make_unique<TcpEchoServer>(serverport);tsvr->Init();tsvr->Start();return 0;
}
Thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h>
#include "logger.hpp"
#define get_lwp_id() syscall(SYS_gettid);using func_t = std::function<void(const std::string&name)>;
const std::string threadnamedefault = "Nobody";class Thread
{
public:Thread(func_t func, const std::string &name = threadnamedefault) : _name(name), _func(func), _isrunning(false){LOG(LoggerLevel::INFO) << _name << "create thread obj success";}// static->变成全局,因为类内函数会多一个this指针,会显示参数不兼容static void *start_routine(void *args){Thread *self = static_cast<Thread *>(args);// self->self->_isrunning = true;self->_lwpid = get_lwp_id();self->_func(self->_name);pthread_exit((void *)0);}void Start(){// this把当前对象传进来int n = pthread_create(&_tid, nullptr, start_routine, this);if (n == 0){LOG(LoggerLevel::INFO) << _name << "running success";}}void Stop(){int n = pthread_cancel(_tid);(void)n;}// 检测线程结束并且回收的功能void Join(){if (!_isrunning)return;int n = pthread_join(_tid, nullptr);if (n == 0){LOG(LoggerLevel::INFO) << _name << "join success";}}~Thread(){// LOG(LoggerLevel::INFO) << _name << "destory thread obj success";}private:bool _isrunning; // 线程创建标识符pthread_t _tid;pid_t _lwpid;std::string _name;func_t _func;
};#endif
ThreadPool.hpp
#pragma once#include <iostream>
#include<cstdio>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"//单例线程池-懒汉模式
const static int defaultthreadnum = 3; // for debugtemplate <class T>
class ThreadPool
{
private:bool QueueIsEmpty(){return _q.empty();}void Routine(const std::string &name){// for testwhile (1){// 把任务从线程获取到线程私有(数据或资源仅对特定线程可见,临界区->线程私有栈)T t;{LockGuard Lockguard(&_lock);while (QueueIsEmpty()&&_is_running){_wait_thread_num++;_cond.Wait(_lock);_wait_thread_num--;}if(!_is_running&&QueueIsEmpty()){LOG(LoggerLevel::INFO)<<"线程池退出&&任务队列为空,"<<name<<"退出";break;}// 队列中此时有任务了,拿到任务t,但是//1.线程池退出---销毁历史任务//2.线程池,没有退出---正常处理任务t = _q.front();_q.pop();}t();//for debug//LOG(LoggerLevel::DEBUG) << name << "handler task:" << t.ResultToString();}}//构造函数私有化,这样就不允许创建对象了ThreadPool(int threadnum = defaultthreadnum): _threadnum(threadnum), _is_running(false), _wait_thread_num(0){for (int i = 0; i < _threadnum; ++i){// 方法一// bind预先绑定函数与参数// auto f=std::bind(hello,this);// Thread t(f);// 方法二(推荐,可读性高):// lambda使得线程调用类内函数的方法std::string name = "thread-" + std::to_string(i + 1);_threads.emplace_back([this](const std::string &name){ this->Routine(name); }, name);// Thread t([this](){// this->hello();// },name);// _threads.push_back(std::move(t));}LOG(LoggerLevel::INFO) << "thread_pool obj create success";}ThreadPool<T> &operator=(const ThreadPool<T> &)=delete;ThreadPool(const ThreadPool<T> &)=delete;
public:// 启动线程void Start(){if (_is_running)return;_is_running = true;for (auto &t : _threads){t.Start();}LOG(LoggerLevel::INFO) << "thread_pool obj running success";}// 取消线程// 核心思想:我们应该让线程走正常的唤醒逻辑退出// 1.如果被唤醒&&任务队列没有任务 = 让线程退出// 2.如果被唤醒&&任务队列有任务 = 线程不能立即退出,而应该让线程把任务处理完,再退出// 3.线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完成,在退出// 3 || 2 -> 1//如果线程有任务,线程是不会休眠的void Stop(){if (!_is_running)return;_is_running = false;if (_wait_thread_num)_cond.NotifyAll();// 不推荐这种做法// if (!_is_running)// return;// _is_running = false;// for (auto &t : _threads)// {// t.Stop();// }// LOG(LoggerLevel::INFO)<<"thread_pool obj stop success";}void Wait(){for (auto &t : _threads){t.Join();}LOG(LoggerLevel::INFO) << "thread_pool obj wait success";}void Enqueue(const T &t){if(!_is_running)return;{LockGuard lockguard(&_lock);_q.push(t);if (_wait_thread_num > 0){_cond.NotifyOne();}}}static std::string ToHex(ThreadPool<T> *addr){char buffer[64];snprintf(buffer,sizeof(buffer),"%p",addr);return buffer;}//获取单例static ThreadPool<T> *GetInstance(){//成员方法可以访问类内静态属性吗?可以!if(!_instance){_instance=new ThreadPool<T>();LOG(LoggerLevel::DEBUG)<<"线程池单例首次被使用,创建并初始化"<<ToHex(_instance);_instance->Start();}else{LOG(LoggerLevel::DEBUG)<<"线程池单例已经存在,直接获取"<<ToHex(_instance);}return _instance;}~ThreadPool(){}private:// 任务队列std::queue<T> _q; // 临界资源// 多个线程std::vector<Thread> _threads;int _threadnum;int _wait_thread_num;// 保护机制Mutex _lock;Cond _cond;// 其他属性bool _is_running;//单例中静态指针static ThreadPool<T> *_instance;
};//类内声明静态指针,在类外需要初始化
template<class T>
ThreadPool<T> *ThreadPool<T>::_instance=nullptr;
Version-2 添加命令行处理业务
IO模块和服务模块分离
我们可以再封装一个任务处理模块,将服务模块和任务模块分离
服务模块和任务模块分离
修改服务端
任务模块
我们可以通过popen函数解析命令行字符串
popen函数
popen作用:
命令字符串command传进来后,在底层将其处理完,以特定方式type将其打开(一般就设置为r就行,如果需要输入的那加上w)
返回类型:成功返回一个文件对象,我们通过一个文件指针将该对象的数据读取出来,之后用pclose将其关掉,如此命令完成。失败返回NULL。
整体的解析命令行函数就是这样,很简单,不过我们还需要禁用一些命令,比如rm,sudo等等,
我们通过一个创建白名单数组,里边存放能实现的命令,用户输入命令的时候,只能实现白名单里有的命令。也可以定义黑名单,用户输入黑名单中的命令一律不可执行
地址转换函数
关于inet_ntoa
地址转换最佳实践inet_pton
在地址转换的时候推荐用这个
修改InetAddr.hpp
业务代码展示
Comm.hpp
#ifndef __COMMAND_HPP__
#define __COMMAND_HPP__#include<iostream>enum
{OK,SOCKET_CREATE_ERROR,SOCKET_BIND_ERROR,SOCKET_LISTEN_ERROR,SOCKET_CONNECT_ERROR,FORK_ERROR,
};#endif
Command.hpp
#pragma once#include<iostream>
#include<string>
#include<vector>
#include<cstdio>class Command
{
private:bool IsSafe(const std::string &cmd){for(auto&c:_command_white_list){//echo "hello";rm也会读取成功if(cmd==c){return true;}}return false;}
public:Command(){_command_white_list.push_back("ls -a -l");_command_white_list.push_back("ll");_command_white_list.push_back("cat test.txt");_command_white_list.push_back("touch touch.txt");_command_white_list.push_back("tree");_command_white_list.push_back("who");_command_white_list.push_back("whoami");_command_white_list.push_back("pwd");}std::string Exec(const std::string &cmd){if(!IsSafe(cmd)){return "坏人";}std::string result;FILE* fp=popen(cmd.c_str(),"r");if(fp==NULL){result=cmd+"exec error";return result;}else{char buffer[1024];//fgets按行读取while(fgets(buffer,sizeof(buffer),fp)!=nullptr){//c语言读取字符串末尾默认加上0result+=buffer;}pclose(fp);}return result;}~Command(){}
private://命令行白名单std::vector<std::string> _command_white_list;
};
CommandClient.cc
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include"Comm.hpp"
#include"InetAddr.hpp"void Usage(std::string proc)
{std::cerr << "Usage:" << proc << " serverip serverport" << std::endl;
}//./tcp_client serverip serverport
int main(int argc,char* argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"create client sockfd error"<<std::endl;exit(SOCKET_CREATE_ERROR);}//tcp_client不用显示bind//客户端自己的socket地址,让本地os自主随机选择,尤其是端口号//客户端向目标服务器发送连接请求即可InetAddr server(server_port,server_ip);if(connect(sockfd,server.Addr(),server.Length())!=0){std::cerr<<"connect server error"<<std::endl;exit(SOCKET_CONNECT_ERROR);}std::cout<<"connect "<<server.ToString()<<" success"<<std::endl;while(true){std::cout<<"Please Enter@ ";std::string line;std::getline(std::cin,line);ssize_t n=write(sockfd,line.c_str(),line.size());if(n>=0){char buffer[1024];ssize_t m=read(sockfd,buffer,sizeof(buffer)-1);if(m>0){buffer[m]=0;std::cout<<buffer<<std::endl;}}}return 0;
}
CommandServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include<functional>static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;//业务分离
using callback_t =std::function<std::string(const std::string&)>;//只负责IO通信,业务交给上层
class CommandServer
{
private://处理业务void HandlerIO(int sockfd, InetAddr client){char buffer[1024];while (true){buffer[0] = 0;//业务:解析命令字符串,实现远程控制ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;// std::string echo_string = "server echo# ";// echo_string += buffer;LOG(LoggerLevel::DEBUG) << client.ToString() << " say: " << buffer;//上层处理业务,我们在服务端实现然后传入cb函数std::string result= _cb(buffer);// tcp面向字节流,文件也是面向字节流,所以用write//将结果输出到端口write(sockfd, result.c_str(), result.size());}else if (n == 0){LOG(LoggerLevel::INFO) << "client " << client.ToString() << " quit,me too,close fd: " << sockfd;break;}else{LOG(LoggerLevel::WARNING) << "read client " << client.ToString() << "error, sockfd: " << sockfd;break;}}close(sockfd); // 一定要关闭}public:CommandServer(callback_t cb,uint16_t port = gport): _listensockfd(gdefaultfd), _port(port),_cb(cb){}void Init(){// 1.创建套接字_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LoggerLevel::FATAL) << "create tcp socket error";exit(SOCKET_CREATE_ERROR);}LOG(LoggerLevel::INFO) << "create tcp socket success: " << _listensockfd;// 2.绑定套接口InetAddr local(_port);if (bind(_listensockfd, local.Addr(), local.Length()) != 0){LOG(LoggerLevel::FATAL) << "bind socket error";exit(SOCKET_BIND_ERROR);}LOG(LoggerLevel::INFO) << "bind socket success" << _listensockfd;// 3.服务端监听// 一个tcp server listen启动之后,服务器已经算运行了,只是还无法进行IO,我们在Start中设计if (listen(_listensockfd, gbacklog) != 0){LOG(LoggerLevel::FATAL) << "listen socket error";exit(SOCKET_LISTEN_ERROR);}LOG(LoggerLevel::INFO) << "listen socket success" << _listensockfd;}class ThreadData{public:ThreadData(int sockfd, CommandServer *self,const InetAddr&addr) : _sockfd(sockfd), _self(self),_addr(addr){}public:int _sockfd;CommandServer *_self;InetAddr _addr;};// static消除this影响// 静态方法无法调用类内成员方法或者成员属性,但我们需要Handler函数// 方法一:Handler放在类外当作普通函数使用// 方法二(推荐):内部类,将外类设计为内部类成员变量,通过内部类对象访问类属性,做法在上面↑static void *Routine(void *args){ThreadData *td=static_cast<ThreadData*>(args);//线程分离,detach的线程不需要join,结束的时候该线程资源会被系统自动回收pthread_detach(pthread_self());td->_self->HandlerIO(td->_sockfd,td->_addr);delete td;return nullptr;}void Start(){signal(SIGCHLD, SIG_IGN);while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(LoggerLevel::WARNING) << "accept client error";continue; // 跳过这一次,继续“拉客”}InetAddr clientaddr(peer);LOG(LoggerLevel::INFO) << "获取新连接成功,sockfd is: " << sockfd << "client addr: " << clientaddr.ToString();// 服务端处理请求//version-4 线程池// ThreadPool<task_t>::GetInstance()->Enqueue([this,sockfd,clientaddr](){// this->HandlerIO(sockfd,clientaddr);// });// version-3 多线程// 多线程共享主线程曾经打开的文件描述符表// 主线程和新线程需要关闭历史fd吗?不可以!因为所有线程共享一个表,不是独立的pthread_t tid;ThreadData *td = new ThreadData(sockfd, this,clientaddr);pthread_create(&tid, nullptr, Routine, (void *)td);// 主线程不退,其余线程并型执行,分离线程// version-1 单进程// HandlerIO(sockfd,clientaddr);// version-2 多进程// 创建子进程,子进程可以继承父进程socketfd// pid_t id=fork();// if(id<0)// {// LOG(LoggerLevel::FATAL)<<"资源不足,创建子进程失败";// exit(FORK_ERROR);// }// else if(id==0){// //child,handler// //child也能看到sockfd以及_listensockfd// close(_listensockfd);// if(fork()>0)// {// exit(OK);// }// HandlerIO(sockfd,clientaddr);// exit(OK);// }// else{// //father// //父进程创建子进程,阻塞等待子进程退出->串型 错误的// //我们期望子进程处理IO时,父进程回到accept继续获取新连接// close(sockfd);// pid_t rid=waitpid(id,nullptr,0);// (void)rid;// }}}~CommandServer() {}private:int _listensockfd; // 监听套接字uint16_t _port;callback_t _cb;
};#endif
InetAddr.hpp
#pragma once// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define Conv(addr) ((struct sockaddr*)&addr)class InetAddr
{
private:void Net2Host(){_port = ntohs(_addr.sin_port);// _ip = inet_ntoa(_addr.sin_addr);char ipbuffer[64];inet_ntop(AF_INET,&(_addr.sin_addr),ipbuffer,strlen(ipbuffer));_ip=ipbuffer;}void Host2Net(){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr));}public:// 网络风格地址构造InetAddr(const struct sockaddr_in &addr): _addr(addr){Net2Host();}// 主机地址风格构造InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"): _port(port), _ip(ip){Host2Net();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr* Addr(){return Conv(_addr);}socklen_t Length(){return sizeof(_addr);}std::string ToString(){return _ip+"-"+std::to_string(_port);}bool operator==(const InetAddr&addr){return _ip==addr._ip&&_port==addr._port;//return _ip==addr._ip;}~InetAddr(){}private:struct sockaddr_in _addr; // 网络风格地址// 主机风格地址std::string _ip;uint16_t _port;
};
logger.hpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"enum class LoggerLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};std::string LoggerLevelToString(LoggerLevel level)
{switch (level){case LoggerLevel::DEBUG:return "Debug";case LoggerLevel::INFO:return "Info";case LoggerLevel::WARNING:return "Warning";case LoggerLevel::ERROR:return "Error";case LoggerLevel::FATAL:return "Fatal";default:return "Unknown";}
}std::string GetCurrentTime()
{// 获取时间戳time_t timep = time(nullptr);// 把时间戳转化为时间格式struct tm currtm;localtime_r(&timep, &currtm);// 转化为字符串char buffer[64];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,currtm.tm_hour, currtm.tm_min, currtm.tm_sec);return buffer;
}class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &logmessage) = 0;
};// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lockguard(&_lock);std::cout << logmessage << std::endl;}}private:Mutex _lock;
};const std::string default_dir_path_name = "log";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const std::string dir_path_name = default_dir_path_name,const std::string filename = default_filename): _dir_path_name(dir_path_name), _filename(filename){if (std::filesystem::exists(_dir_path_name)){return;}try{std::filesystem::create_directories(_dir_path_name);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}~FileLogStrategy(){}virtual void SyncLog(const std::string &logmessage) override{{LockGuard lock(&_lock);std::string target = _dir_path_name;target += '/';target += _filename;std::ofstream out(target.c_str(), std::ios::app);if (!out.is_open()){return;}out << logmessage << "\n";out.close();}}private:std::string _dir_path_name;std::string _filename;Mutex _lock;
};class Logger
{
public:Logger(){}void EnableConsoleStrategy(){_strategy = std::make_unique<ConsoleLogStrategy>();}void EnableFileStrategy(){_strategy = std::make_unique<FileLogStrategy>();}class LogMessage{public:LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger): _curr_time(GetCurrentTime()), _level(level), _pid(getpid()), _filename(filename), _line(line), _logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << LoggerLevelToString(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.str();}template <typename T>LogMessage &operator<<(const T &info){std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 时间戳LoggerLevel _level; // 日志等级pid_t _pid; // 进程pidstd::string _filename; // 文件名int _line; // 行号std::string _loginfo; // 一条合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具体做法};LogMessage operator()(LoggerLevel level, std::string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _strategy;
};Logger logger;#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
Main.cc
#include"CommandServer.hpp"
#include"Command.hpp"
#include<memory>
void Usage(std::string proc)
{std::cerr << "Usage:" << proc << " localport" << std::endl;
}
//解析命令
//ls -a -l
// std::string CommandExec(const std::string&commandstr)
// {
// //线程内部也可以创建进程,线程内fork出的子进程默认是只有一个执行流的进程
// //1.创建管道
// //2.fork
// //3.命令分析exec// }
int main(int argc,char* argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t serverport=std::stoi(argv[1]);//打印日志选择显示器策略EnableConsoleStrategy();Command cmdobj;//服务端处理业务,业务函数作为参数传递给处理IO的类,然后在类中将IO信息传递到业务函数处理IOstd::unique_ptr<CommandServer> tsvr=std::make_unique<CommandServer>([&cmdobj](const std::string&cmd)->std::string{return cmdobj.Exec(cmd);},serverport);tsvr->Init();tsvr->Start();return 0;
}
Makefile
.PHONY:all
all:command_client command_servercommand_client:CommandClient.ccg++ -o $@ $^ -std=c++17
command_server:Main.ccg++ -o $@ $^ -std=c++17 -lpthread.PHONY:clean
clean:rm -f command_client command_server
Mutex.hpp
#pragma once#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}pthread_mutex_t *Get(){return &_lock;}void Unlock(){pthread_mutex_unlock(&_lock);}~Mutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex *_mutex) : _mutexp(_mutex){_mutex->Lock();}~LockGuard(){_mutexp->Unlock();}private:Mutex *_mutexp;
};
此篇完。