个人主页: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 2
const 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)
{
// 加过滤逻辑 --- TODO
LockGuard 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); // TODO
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;
};
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); // 3
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 sockfd 和 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, 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. 创建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?If the connection or binding succeeds
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 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;
}
效果展示 :
虽然命令有些少, 但是整体还是没有问题的~