计算机网络socket编程(6)_TCP实网络编程现 Command_server

server/2024/11/28 16:34:47/

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

计算机网络socket编程(6)_TCP实网络编程现 Command_server

收录于专栏【计算机网络
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌 
  

目录

功能介绍 

1. InetAddr.hpp

2. LockGuard.hpp

3. Log.hpp

4. Command.hpp

5. TcpServer.hpp

6. TcpServerMain.cc

7. TcpClientMain.cc


功能介绍 

和上回 TCP 网络编程一样, 实现简单的 Command_server

还有就是网络编程代码真的是又多又杂, 有的时候我自己都烦, 没办法网络部分就是这样的, 我最近会尽快更完这个 socket 编程, 提早进入概念部分, 一直编程感觉少了什么~ 还得跟概念结合起来看, 感兴趣的宝子们不要忘记了点赞关注哦! 我现在在网络部分真的待不了一点, 希望我能尽快挣脱网络, 更新数据库 MySQL 的东西吧!  

1. InetAddr.hpp

 老演员了, InetAddr 类封装了 IP 地址和端口号,提供了转换网络字节序和主机字节序的方法,支持比较、获取 IP 地址、端口号、地址字符串等功能。这个类的设计是典型的用于网络编程,特别是在处理 IPv4 地址时非常有用。

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.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_p to n// p: process// n: net// inet_pton(int af, const char *src, void *dst);// inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);::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);}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;}std::string AddrStr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

2. LockGuard.hpp

老演员 +1, LockGuard 类是一个封装了互斥锁的RAII对象,提供了简化的锁管理功能。它确保在对象生命周期结束时自动释放锁,从而避免了忘记解锁和潜在的死锁问题。它特别适用于多线程程序中对共享资源的保护,能显著提高代码的可读性和可靠性。 

#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;
};

3. Log.hpp

 老演员+1, 这段代码是一个 C++ 日志系统的实现,它提供了日志记录功能,可以将日志输出到屏幕或写入文件。系统使用了互斥锁(pthread_mutex_t)来保证日志输出的线程安全,并且提供了多种日志级别(DEBUG, INFO, WARNING, ERROR, FATAL)。

#pragma once#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"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 "UNKNOWN";}}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;// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );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){// 加过滤逻辑 --- TODOLockGuard lockguard(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}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)
};

4. Command.hpp

这个代码实现了一个简单的命令执行器,允许客户端通过网络发送命令并执行,同时对命令进行安全检查,确保执行的是在白名单中的安全命令。 

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <set>
#include "Log.hpp"
#include "InetAddr.hpp"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() {}// ls;rm -rf /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 -a -l -> os --> ls -ssize_t n = ::recv(sockfd, commandbuf, sizeof(commandbuf) - 1, 0); // TODOif (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;
};

Command 构造函数

构造函数初始化时,构建了一个白名单 safe_command,该白名单包括允许执行的命令。只有这些命令可以在后续的调用中被安全地执行。

"ls":列出目录内容。

"touch":创建空文件(命令格式如 touch filename)。

"pwd":显示当前工作目录。

"whoami":显示当前用户。

"which":显示命令的完整路径。

这些命令一般是无害的,可以允许远程执行。其他的命令(例如 rm -rf /)则会被拒绝。

SafeCheck 函数

这个函数用于检查命令是否安全。它遍历白名单中的每个命令,使用 strncmp 判断是否有命令前缀匹配。如果有匹配的命令前缀(例如 ls -l),则认为这个命令是安全的,返回 true,否则返回 false。

strncmp 的作用:strncmp 比较两个字符串的前 n 个字符,cmd.size() 是匹配的字符长度,因此如果 cmdstr 以白名单命令的某个前缀开始,它将被认为是一个安全命令

Excute 函数

这个函数用于执行实际的命令。首先,它会调用 SafeCheck 函数来确保命令是安全的。如果命令不在白名单中,返回 "unsafe"。

如果命令安全,它通过 popen 调用操作系统的命令行执行命令,并读取命令输出:

popen(cmdstr.c_str(), "r") 会执行命令,并返回一个文件指针,可以通过它读取命令的标准输出。

使用 fgets 一行一行地读取命令输出,直到没有输出为止。

最后将结果返回。如果命令有输出,则返回输出内容;如果命令没有输出,则返回 "success"。

如果执行命令时发生错误(例如,popen 失败),返回 "execute error"。

HandlerCommand 函数

这个函数处理与客户端的交互。它是一个长连接服务,持续接收和处理客户端发送的命令。

工作流程:

使用 recv 函数接收来自客户端的命令,存储在 commandbuf 中。recv 会返回接收到的字节数,如果接收到有效数据(n > 0),继续处理。

通过 LOG(INFO, ...) 记录从客户端接收到的命令。

使用 Excute 函数执行命令并获取执行结果。

使用 send 将命令的执行结果返回给客户端。

如果接收到的数据为 0(即客户端关闭连接),则记录日志并结束循环。

如果发生读取错误(n < 0),记录错误日志并结束循环。

这个方法基本上是一个长连接的服务器循环,不断接收和执行命令,直到客户端断开连接或发生错误。

5. TcpServer.hpp

这段代码实现了一个基本的 TCP 服务器 类 (TcpServer),通过多线程处理客户端的连接。 

#pragma once
#include <iostream>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Log.hpp"
#include "InetAddr.hpp"using namespace log_ns;enum
{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERR
};const static int gport = 8888;
const static int gsock = -1;
const static int gblcklog = 8;using command_service_t = std::function<void(int sockfd, InetAddr addr)>;class TcpServer
{
public:TcpServer(command_service_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, "socket create error\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd: %d\n", _listensockfd); // 3struct 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 sockfd 和 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, gblcklog) < 0){LOG(FATAL, "listen error\n");exit(LISTEN_ERR);}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(){// signal(SIGCHLD, SIG_IGN);_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 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;//         }//     }// }~TcpServer() {}private:uint16_t _port;int _listensockfd;bool _isrunning;command_service_t _service;
};

常量定义

定义了一些常量,用于表示不同阶段可能发生的错误类型(如 SOCKET_ERROR, BIND_ERROR, LISTEN_ERR

gport:默认的服务器端口号 8888。

gsock:默认的套接字文件描述符值,表示未初始化的套接字。

gblcklog:listen() 系统调用的 backlog(最大连接数),即服务器在开始接收连接之前,内核的连接队列最大容量。

command_service_t 类型定义

定义了一个类型别名 command_service_t,它是一个 std::function 类型,表示一个接受两个参数(sockfd 和 addr)并返回 void 的函数。这个函数在实际的应用中将用于处理每个客户端的连接请求。

TcpServer 类构造函数

command_service_t service:构造函数接收一个处理客户端请求的回调函数 service,即每当一个客户端连接建立后,服务器就调用该函数来处理连接。

uint16_t port:默认端口为 8888,服务器将监听该端口。

初始化服务器 (InitServer 方法)

创建 Socket:使用 ::socket() 创建一个 TCP 套接字 (SOCK_STREAM)。

如果创建失败(socket 返回值小于 0),日志记录错误并退出程序。

绑定 Socket 地址:通过 ::bind() 将创建的套接字与指定的端口和 IP 地址绑定。

服务器绑定到本机所有可用 IP 地址 (INADDR_ANY),并监听指定的端口。

监听连接:通过 ::listen() 开始监听传入的连接,gblcklog 参数指定了等待连接的最大队列长度。

ThreadData 类

ThreadData:用于保存每个客户端连接的信息,包含:

_sockfd:客户端的套接字文件描述符。

_self:指向当前 TcpServer 实例的指针。

_addr:客户端的地址信息(封装在 InetAddr 对象中)。

Loop 方法

Loop:这是服务器的主循环,主要功能是:

使用 ::accept() 接受新的客户端连接。

每当接收到一个新连接时,使用 pthread_create 创建一个新线程,线程通过 Execute 函数处理客户端的请求。

每个连接的信息(套接字和客户端地址)封装在 ThreadData 中传递给线程。

pthread_create:创建一个新线程来处理客户端的请求。线程通过 Execute 方法执行指定的服务函数。

Execute 方法

Execute:线程的入口函数,执行以下操作:

使用 pthread_detach(pthread_self()) 将线程设为分离状态,确保线程结束后自动回收资源。

调用传入的回调函数 td->_self->_service(td->_sockfd, td->_addr),即执行传递给服务器的具体业务逻辑。

关闭客户端连接的套接字。

删除 ThreadData 对象。

6. 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;
}

7. TcpClientMain.cc

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// ./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?If the connection or binding succeedsstruct 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 socket 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;
}

 效果展示 : 

虽然命令有些少, 但是整体还是没有问题的~ 


http://www.ppmy.cn/server/145668.html

相关文章

Leetcode(快慢指针习题思路总结,持续更新。。。)

这种模式&#xff0c;有一个非常出门的名字&#xff0c;叫龟兔赛跑。这种算法的两个指针的在数组上&#xff08;或是链表上&#xff0c;序列上&#xff09;的移动速度不一样。快的一个指针肯定会追上慢的一个&#xff08;可以想象成跑道上面跑得快的人套圈跑得慢的人&#xff0…

语义版本控制

注意&#xff1a; 本文内容于 2024-11-27 22:25:05 创建&#xff0c;可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容&#xff0c;请访问原文地址&#xff1a;语义版本控制。感谢您的关注与支持&#xff01; 由于自己平时喜欢写点小玩意&#xff0c;自然而…

PTC在电池中的作用

一、电池安全性的重要性 在现代电子设备中,电池作为能源储存和供应的核心组件,其性能和安全性一直是关注的重点。尤其是在锂离子电池等高能量密度电池的广泛应用中,电池发生过流、过热、短路等问题可能导致电池失效,甚至引发热失控和火灾等安全事故。因此,如何提高电池的…

富格林:有效追损正确提高出金

富格林指出&#xff0c;随着近几年贵金属行业持续走热&#xff0c;越来越多的投资者开始涌入市场增值财富。由于面对陌生市场的束手无策&#xff0c;不少新手投资者遭遇亏损的同时却不知该如何进行正确追损。事实上&#xff0c;掌握正确的交易方法对于提高出金以及有效追损都有…

平安产险厦门分公司:深化风险减量服务,开展安全驾驶巡回培训

为进一步提升管理货运车辆的企业客户的安全生产能力&#xff0c;增强驾驶员的安全意识与驾驶技能&#xff0c;平安产险厦门分公司秉持“金融为民”初心&#xff0c;积极践行金融工作政治性、人民性&#xff0c;开展“风险减量”专项行动——《风险减量&#xff0c;安全驾驶》巡…

YOLOv11融合[ECCV 2018]RCAN中的RCAB模块及相关改进思路

YOLOv11v10v8使用教程&#xff1a; YOLOv11入门到入土使用教程 YOLOv11改进汇总贴&#xff1a;YOLOv11及自研模型更新汇总 《Image Super-Resolution Using Very Deep Residual Channel Attention Networks》 一、 模块介绍 论文链接&#xff1a;https://arxiv.org/abs/1807…

论文阅读《Dual Personalization on Federated Recommendation》

论文概况 本文是2024 IJCAI的一篇联邦推荐论文&#xff0c;提出了提出了一种新的双重个性化机制&#xff0c;以有效地学习用户和项目的细粒度个性化。 Introduction 我们设计了一种新的双重个性化机制&#xff0c;通过个性化评分功能和细粒度的项目嵌入个性化来捕获用户偏好。…

Java基础面试题05:简述快速失败(fail-fast)和安全失败(fail-safe)的区别 ?

在 Java 集合中&#xff0c;fail-fast 和 fail-safe 是两种不同的遍历机制&#xff0c;用来定义在遍历集合时是否允许修改集合内容。它们的区别在于&#xff1a; fail-fast&#xff1a;不允许在遍历过程中修改集合&#xff0c;一旦发现修改&#xff0c;立刻抛出异常。fail-saf…