当前位置: 首页 > news >正文

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一样,我们的客户端只需要写和读,不需要执行业务,所以无需修改。

http://www.dtcms.com/a/469192.html

相关文章:

  • vscode 连接远程服务器同步方法
  • 传统数据安全措施与云计算数据安全的区别
  • Linux下如何在vim里使用异步编译和运行?
  • Python高效实现Excel转PDF:无Office依赖的轻量化方案
  • 做网站PPPOE网络可以吗一个好网站设计
  • 混淆矩阵在金融领域白话解说
  • 深耕金融调研领域,用科学调研破解银行服务困境(市场调研)
  • 未备案网站处理系统写作墨问题 网站
  • 【Linux】手搓日志(附源码)
  • Excel 下拉选项设置 级联式
  • pycharm自动化测试初始化
  • nacos3.0.4升级到3.1.0
  • linux入门5.5(高可用)
  • JAVA·数组的定义与使用
  • Transformer 面试题及详细答案120道(81-90)-- 性能与评估
  • 可以做软件的网站有哪些功能中国新闻社待遇
  • 【鉴权架构】SpringBoot + Sa-Token + MyBatis + MySQL + Redis 实现用户鉴权、角色管理、权限管理
  • 三星S25Ultra/S24安卓16系统Oneui8成功获取完美root权限+LSP框架
  • ffmpeg 播放视频 暂停
  • 老题新解|大整数的因子
  • Eureka的自我保护机制
  • 探索颜色科学:从物理现象到数字再现
  • AirSim_SimJoyStick
  • 第五部分:VTK高级功能模块(第149章 Remote模块 - 远程模块类)
  • 道可云人工智能每日资讯|《政务领域人工智能大模型部署应用指引》发布
  • 自己做网站哪家好win10 wordpress安装教程视频
  • wordpress整体搬迁宁波seo深度优化平台有哪些
  • 4K Wallpaper mac v2.7.dmg 安装教程(Mac电脑详细安装步骤4K壁纸Mac下载安装)
  • Mac 软件出现「应用程序“xxx”不能打开」的解决办法
  • 东航集团客户网站是哪家公司建设4k高清视频素材网站