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

ops/2024/11/25 14:58:38/

个人主页: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/ops/136600.html

相关文章

详解Qt QSettings 设置类

文章目录 QSettings 详解前言什么是 QSettings&#xff1f;QSettings 的构造函数和常用成员函数构造函数1. 默认构造函数2. 指定组织和应用名称3. 使用自定义文件 常用成员函数1. 写入设置setValue 2. 读取设置value 3. 检查键是否存在contains 4. 删除设置remove 5. 获取所有键…

20241121 android中树结构列表(使用recyclerView实现)

1、adapter-item的布局 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"wrap_content&…

【Spring】RESTful设计风格

前言 REST(Representational State Transfer),即“表现层状态转移”,它定义了互联网软件服务的架构原则。如果一个架构的符合 REST 原则&#xff0c;就被称之为 RESTful 架构&#xff0c;这是目前最流行的互联网软件服务架构设计风格之一。 REST 并不是一种标准&#xff0c;…

深度学习中的循环神经网络(RNN)与时间序列预测

一、循环神经网络&#xff08;RNN&#xff09;简介 循环神经网络&#xff08;Recurrent Neural Networks&#xff0c;简称RNN&#xff09;是一种专门用于处理序列数据的神经网络架构。与传统神经网络不同&#xff0c;RNN具有内部记忆能力&#xff0c;能够捕捉数据中的时间依赖…

(免费送源码)计算机毕业设计原创定制:Java+SSM+JSP+Ajax+MySQLSSM国外鞋服代购平台

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;鞋服代购平台当然也不例外。代购平台是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;采用…

深度学习1

一、库的安装 1、cuda/cpu cuda 有最好&#xff0c;没有无需额外安装&#xff1b;cpu无需其他操作&#xff0c;直接进入下面步骤 查看 nvidia 驱动版本&#xff1a;cmd里面&#xff0c;输入nvidia-smi 查看GPU驱动程序版本 CUDA Version位置&#xff08;保证CUDA版本号<GPU…

Odoo :免费且开源的农牧行业ERP管理系统

文 / 开源智造Odoo亚太金牌服务 引言 提供农牧企业数字化、智能化、无人化产品服务及全产业链高度协同的一体化解决方案&#xff0c;提升企业智慧种养、成本领先、产业互联的核心竞争力。 行业典型痛点 一、成本管理粗放&#xff0c;效率低、管控弱 产品研发过程缺少体系化…

ApiChain 从迭代测试用例到项目回归测试 核心使用教程

项目地址&#xff1a;ApiChain 项目主页 环境变量 环境变量是在特定的开发环境&#xff08;开发、测试、uat等&#xff09;下&#xff0c;保存的一份数据集&#xff0c;环境变量是发送网络请求或者执行单测的一个重要数据源。环境变量根据作用范围可以分为全局环境变量、项目…