目录
引言
通过前面的简单回显用户数据的基础版本,我们已经能够知道tcp通信的基础原理及过程,接下来我们可以来带点业务了,本章我们就来讲讲我们的指令服务版本。
代码展示
复用代码部分
网络地址封装类InetAddr.hpp
cpp#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
cpp#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
cpp#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 2 const 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
cpp#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 -l ssize_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
cpp#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
cpp#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 addr if (::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,也就是我们的业务。
多了这一个业务变量,我们自然就要在构造函数上面给我们的业务赋值,这个值就是我们源文件中传的对象。
构建服务器这块我们是一模一样的,我就不细说了。
cppvoid 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
cpp#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. 创建socket int 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?connect struct 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一样,我们的客户端只需要写和读,不需要执行业务,所以无需修改。
Socket网络编程(2)-command_server
板鸭〈小号〉2025-10-15 23:21
相关推荐
维尔切3 小时前
Docker 存储与数据共享温柔一只鬼.3 小时前
Docker快速入门——第四章Docker镜像温柔一只鬼.4 小时前
Docker快速入门——Windowns系统下Docker安装(2025最新理解与完整,附带WSL1如何升级为WSL2)何朴尧4 小时前
centos/cuos如何开启软件源派阿喵搞电子5 小时前
关于使用docker部署srs服务器的相关指令qq_339191145 小时前
aws ec2防ssh爆破, aws服务器加固, 亚马逊服务器ssh安全,防止ip扫描ssh。 aws安装fail2ban, ec2配置fail2bancsdn_Hzx5 小时前
Linux添加一个系统服务重生之我在20年代敲代码6 小时前
【Linux】初始线程问道飞鱼6 小时前
【Linux知识】Linux磁盘开机挂载