Socket网络编程(2)-command_server
目录
引言
代码展示
复用代码部分
网络地址封装类InetAddr.hpp
锁的封装类LockGuard.hpp
日志类Log.hpp
新增/修改代码部分
命令业务类-Command.hpp
Tcp服务端源文件-TcpServerMain.cc
Tcp服务器端头文件-TcpServer.hpp
Tcp客户端源文件-TcpClientMain.cc
引言
通过前面的简单回显用户数据的基础版本,我们已经能够知道tcp通信的基础原理及过程,接下来我们可以来带点业务了,本章我们就来讲讲我们的指令服务版本。
代码展示
复用代码部分
网络地址封装类InetAddr.hpp
#pragma once#include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>// 封装网络地址类 class InetAddr { private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);//_ip = inet_ntoa(addr.sin_addr);char ip_buf[32];::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;}public:InetAddr(const struct sockaddr_in &addr): _addr(addr){ToHost(addr); // 将addr进行转换}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}InetAddr(){}bool operator==(const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr; };
锁的封装类LockGuard.hpp
#pragma once#include <pthread.h>class LockGuard { public:LockGuard(pthread_mutex_t *mutex) : _mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t *_mutex; };
日志类Log.hpp
#pragma once#include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <ctime> #include <stdarg.h> #include <fstream> #include <string.h> #include <pthread.h>namespace log_ns {enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};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 "UNKNOW";}}std::string GetCurrTime(){time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage &lg){pthread_mutex_lock(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}pthread_mutex_unlock(&glock);}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info = log_info;// 打印出日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(level, Format, ...) do {lg.logMessage(__FILE__, __LINE__, level, Format, ##__VA_ARGS__); }while (0)#define EnableScreen() do {lg.Enable(SCREEN_TYPE);}while(0)#define EnableFile() do {lg.Enable(FILE_TYPE);}while(0) }
新增/修改代码部分
命令业务类-Command.hpp
#pragma once#include <iostream> #include <string> #include <cstring> #include <cstdio> #include <set> #include "Log.hpp" #include "InetAddr.hpp"using namespace log_ns; class Command { public:Command(){// 白名单_safe_command.insert("ls");_safe_command.insert("touch"); // touch filename_safe_command.insert("pwd");_safe_command.insert("whoami");_safe_command.insert("which"); // which pwd}~Command(){}bool SafeCheck(const std::string &cmdstr){for (auto &cmd : _safe_command){if (strncmp(cmd.c_str(), cmdstr.c_str(), cmd.size()) == 0){return true;}}return false;}std::string Excute(const std::string &cmdstr){if (!SafeCheck(cmdstr)){return "unsafe";}std::string result;FILE *fp = popen(cmdstr.c_str(), "r");if (fp){char line[1024];while (fgets(line, sizeof(line), fp)){result += line;}return result.empty() ? "success" : result; // 争对创建文件这种本身没有打印信息的指令}return "execute error";}void HandlerCommand(int sockfd, InetAddr addr){// 我们把他当作一个长服务while (true){char commandbuf[1024]; // 当作字符串,ls -lssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0);if (n > 0){commandbuf[n] = 0;LOG(INFO, "get command from client %s, command: %s\n", addr.AddrStr().c_str(), commandbuf);std::string result = Excute(commandbuf);::send(sockfd, result.c_str(), result.size(), 0);}else if (n == 0){LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}}}private:std::set<std::string> _safe_command; // 我们只以较为安全的命令来测试 };
命令业务类,顾名思义就是专门处理我们客户输入的指令的类,具体是如何做到的呢?我们就一步一步来看。
我们先来分析我们的私有成员变量,我们使用了一个集合类型的变量,这是干什么用的呢?我们知到Linux的指令使用方式非常多样,比如除了基础的指令之外还有多种指令一起使用的情况,而有些情况不做特殊处理是会出现bug的,因此我们这里为了不发生这些我们本次不关注的问题的干扰,我们预先定义一些安全的指令集合来保证我们业务的正常运行。
我们的构造函数的作用就是预先设置一部分安全的指令。比如ls,touch,pwd,whoami,which这些基础的指令。
我们需要有一个安全检查功能的函数,用于检查用户输入的指令是否是安全的。
安全检查功能结束后我们就可以启动我们的服务了,我们这里使用长服务的方式,因为用户不止输入一次指令。
Excute
函数:
- 接收一个字符串类型的命令(
cmdstr
)作为参数。- 首先调用
SafeCheck
函数对命令进行安全检查,若检查不通过则返回 "unsafe"。- 若安全检查通过,通过
popen
函数执行该命令(以读模式打开,用于获取命令输出)。- 使用
fgets
循环读取命令执行过程中的输出内容,拼接至result
字符串中。- 命令执行完成后,若输出结果为空(如创建文件这类无输出的命令),返回 "success";否则返回命令的实际输出结果。
- 若
popen
调用失败(如无法执行命令),返回 "execute error"。
HandlerCommand
函数:
- 接收一个套接字描述符(
sockfd
)和客户端地址(InetAddr
类型的addr
),作为处理客户端命令的核心逻辑。- 采用无限循环实现 "长服务" 模式,持续处理来自同一客户端的命令。
- 通过
recv
函数从套接字接收客户端发送的命令(存储在commandbuf
缓冲区,最多接收 1023 字节,预留一个字节给字符串结束符)。- 若接收成功(
n > 0
),为命令添加字符串结束符,通过日志记录客户端地址和接收的命令。- 调用
Excute
函数执行该命令,获取执行结果,并通过send
函数将结果发送回客户端。- 若接收字节数为 0(
n == 0
),表示客户端主动关闭连接,记录日志并退出循环。- 若接收失败(
n < 0
),记录错误日志并退出循环。Tcp服务端源文件-TcpServerMain.cc
#include "TcpServer.hpp" #include "Command.hpp"#include <memory>// ./tcpserver 8888 int main(int argc, char *argv[]) {if (argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);Command cmdservice;std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::bind(&Command::HandlerCommand, &cmdservice, std::placeholders::_1, std::placeholders::_2), port);tsvr->InitServer();tsvr->Loop();return 0; }
源文件部分跟我们上一篇文章几乎一模一样,就是多了个绑定业务可调用对象的模块,这回我们为了简洁,直接在参数部分将这个可调用对象绑定好传给服务端去处理。
Tcp服务器端头文件-TcpServer.hpp
#pragma once #include <iostream> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring> #include <sys/wait.h> #include <pthread.h> #include "Log.hpp" #include "InetAddr.hpp" using namespace log_ns;enum {SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR };const static int gport = 8888; const static int gsock = -1; const static int gbacklog = 8;using command_server_t = std::function<void(int sockfd, InetAddr addr)>; class TcpServer { public:TcpServer(command_server_t service, uint16_t port = gport): _port(port), _listensockfd(gsock), _isrunning(false), _service(service){}void InitServer(){// 1.创建socket_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(FATAL, "listensockfd create error\n");exit(SOCKET_ERROR);}LOG(INFO, "listensockfd create success, fd: %d\n", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;// 2.bind _listensockfd 和 Socket addrif (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(INFO, "bind success\n");// 3.因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接if (::listen(_listensockfd, gbacklog) < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERROR);}LOG(INFO, "listen success\n");}class ThreadData{public:int _sockfd;TcpServer *_self;InetAddr _addr;public:ThreadData(int sockfd, TcpServer *self, const InetAddr &addr): _sockfd(sockfd), _self(self), _addr(addr){}};void Loop(){_isrunning = true;while (_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// 4. 获取连接int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");continue;}InetAddr addr(client);LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);// version 0 --- 不稳定版本// Service(sockfd, addr);// // version 1 ---多进程版本// pid_t id = fork();// if (id == 0)// {// // child// ::close(_listensockfd); // 建议// if (fork() > 0)// exit(0);// Service(sockfd, addr);// exit(0);// }// // father// ::close(sockfd);// int n = waitpid(id, nullptr, 0);// if (n > 0)// {// LOG(INFO, "wait child success.\n");// }// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了pthread_t tid;ThreadData *td = new ThreadData(sockfd, this, addr);pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离}_isrunning = false;}static void *Execute(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_self->_service(td->_sockfd, td->_addr);::close(td->_sockfd);delete td;return nullptr;}void Service(int sockfd, InetAddr addr){// 长服务while (true){char inbuffer[1024]; // 当作字符串ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){inbuffer[n] = 0;LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer);std::string echo_string = "[server echo]# ";echo_string += inbuffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());break;}else{LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str());break;}}::close(sockfd);}~TcpServer(){}private:uint16_t _port;int _listensockfd;bool _isrunning;command_server_t _service; };
服务端的头文件我们也只是在原基础上将简单的echo业务转换为我们的command指令执行业务。
私有成员变量这一块多了一个可调用对象类型的变量_service,也就是我们的业务。
多了这一个业务变量,我们自然就要在构造函数上面给我们的业务赋值,这个值就是我们源文件中传的对象。
构建服务器这块我们是一模一样的,我就不细说了。
void Loop(){_isrunning = true;while (_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);// 4. 获取连接int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept error\n");continue;}InetAddr addr(client);LOG(INFO, "get a new link, client info: %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd);// version 0 --- 不稳定版本// Service(sockfd, addr);// // version 1 ---多进程版本// pid_t id = fork();// if (id == 0)// {// // child// ::close(_listensockfd); // 建议// if (fork() > 0)// exit(0);// Service(sockfd, addr);// exit(0);// }// // father// ::close(sockfd);// int n = waitpid(id, nullptr, 0);// if (n > 0)// {// LOG(INFO, "wait child success.\n");// }// version 2 --- 多线程版本 --- 不能关闭fd了,也不需要了pthread_t tid;ThreadData *td = new ThreadData(sockfd, this, addr);pthread_create(&tid, nullptr, Execute, td); // 新线程进行分离}_isrunning = false;}
Loop循环里面我们也是没做什么改动,只不过这回我们不用线程池,我们就用多线程版本,也就是执行线程函数。
我们先分离线程,然后做指针的安全类型转换,然后调用可调用对象执行任务,执行完成之后我们关闭套接字,我们的任务就完成了。
Tcp客户端源文件-TcpClientMain.cc
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstring>// ./tcpclient server-ip server-port int main(int argc, char *argv[]) {if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建socketint sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}// 注意:不需要显示的bind,但是一定要有自己的IP和port,所以需要隐式的bind,os会自动bind sockfd,用自己的IP和随机端口号// 什么时候进行自动bind?connectstruct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect sockfd error" << std::endl;exit(2);}while (true){std::string message;std::cout << "Enter # ";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());char echo_buffer[1024];n = read(sockfd, echo_buffer, sizeof(echo_buffer));if (n > 0){echo_buffer[n] = 0;std::cout << echo_buffer << std::endl;}else{break;}}::close(sockfd);return 0; }
这个文件我们是没有任何修改的,因为客户端跟上回的echo一样,我们的客户端只需要写和读,不需要执行业务,所以无需修改。