【Linux网络编程】第七弹---构建类似XShell功能的TCP服务器:从TcpServer类到主程序的完整实现
TcpServer.hpp:TcpServer类基本结构,Execute();Command.hpp:Command类基本结构,构造析构函数,SafeCheck(),HandlerCommand();TcpServerMain.cc;完整代码,TcpServer.hpp,Command.hpp,TcpServerMain.cc~~~
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
上一弹使用TCP协议实现客户端与服务端的通信,此弹实现一个类似于XShell的功能,客户端发出命令,服务端执行命令,并将执行结果返回给客户端!基本结构还是上一弹的结构,此处使用多线程版本即可,无需线程池的代码!
因为客户端的方法需要在TcpServer.hpp.hpp中声明,因此先讲解TcpServer.hpp!
1、TcpServer.hpp
TcpServer.hpp封装TcpServer类!
TcpServer类相较于上一弹只需要稍微修改即可,首先因为需要执行XShell的功能,必不可少的就是函数方法!
函数方法声明:
// sockfd 用于接收消息和发送消息,addr 用于查看是谁发送的
using command_service_t = std::function<void(int sockfd,InetAddr addr)>;
1.1、TcpServer类基本结构
TcpServer类基本结构只需加一个方法成员变量,构造函数加该方法初始化即可!
using command_service_t = std::function<void(int sockfd,InetAddr addr)>;
// 面向字节流
class TcpServer
{
public:
TcpServer(command_service_t service,uint16_t port = gport)
:_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false)
{}
void InitServer();
void Loop();
~TcpServer();
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
command_service_t _service;
};
1.2、 Execute()
Execute()执行回调函数!
static void *Execute(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
td->_self->_service(td->_sockfd,td->_addr); // 执行回调
::close(td->_sockfd);
delete td;
return nullptr;
}
2、Command.hpp
Command类实现类似于XShell的功能,但是需要注意不要让所有命令都可以执行,因为可能会导致删库等相关的问题,因此成员变量可以使用set容器存储允许执行命令的前缀!
2.1、Command类基本结构
成员变量使用set容器存储允许执行命令的前缀,内部实现安全检查,执行命令函数和命令处理函数!
class Command
{
public:
Command();
bool SafeCheck(const std::string &cmdstr);
// 安全执行
std::string Excute(const std::string &cmdstr);
void HandlerCommand(int sockfd, InetAddr addr);
~Command();
private:
std::set<std::string> _safe_command; // 只允许执行的命令
};
2.2、构造析构函数
构造函数将允许使用的命令插入到容器,析构函数无需处理!
注意:此处可以根据个人需要加入命令前缀!
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()
{}
2.3、SafeCheck()
安全检查函数检查字符串的前缀,如果与set容器中的其中一个内容相同则返回true,不相同则返回false!
此处用到C语言的字符串比较函数,比较前n个字节,相等则返回0!
strncmp()
#include <string.h>
int strncmp(const char *s1, const char *s2, size_t n);
SafeCheck()
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;
}
2.4、Excute()
执行命令函数需要处理字符串形式的命令,此处可以使用popen()函数直接执行C语言字符串的命令,如果执行成功(返回值不为空)以行读取的方式将结果拼接到result字符串中,但是有些命令没有执行结果,此时打印success,执行失败(返回值为空)则返回Execute error。
注意:前提需要判断命令是否安全,不安全直接返回unsafe!
popen()
创建一个管道,并将该管道与一个命令(通过shell执行)的输入或输出连接起来。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数
command
: 一个指向以null结尾的字符串的指针,该字符串包含了要执行的命令。type
: 一个指向以null结尾的字符串的指针,该字符串决定了管道的方向。它可以是"r"
(表示读取命令的输出)或"w"
(表示向命令写入输入)。
返回值
- 成功时,
popen
返回一个指向FILE
对象的指针(与fopen函数一样),该对象可用于fread
、fwrite
、fprintf
、fscanf
等标准I/O函数。 - 失败时,返回
nullptr
,并设置errno
以指示错误。
Excute()
// 安全执行
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";
}
2.4、HandlerCommand()
命令处理函数是一个长服务(死循环),先接收客户端的信息,如果接收成功则处理收到的消息(命令),并将处理的结果发送给客户端,如果读到文件结尾或者接收失败则退出循环!
此处换一批接收消息和发送的函数,与read和write还是基本一致的,有稍微差别!
recv()
与套接字(sockets)一起使用,用于从连接的对等端接收数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数
sockfd
: 套接字描述符,标识一个打开的套接字。buf
: 指向一个缓冲区的指针,该缓冲区用于存储接收到的数据。len
: 指定缓冲区的长度(以字节为单位),即recv
函数最多可以接收的数据量。flags
: 通常设置为0,但也可以指定一些特殊的标志来修改recv
的行为。例如,MSG_PEEK
标志允许程序查看数据而不从套接字缓冲区中移除它。
返回值
- 成功时,
recv
返回实际接收到的字节数。如果连接已经正常关闭,返回0。 - 失败时,返回-1,并设置
errno
以指示错误类型。
send()
与套接字(sockets)一起使用,用于向连接的对等端发送数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数
sockfd
: 套接字描述符,标识一个打开的套接字。buf
: 指向包含要发送数据的缓冲区的指针。len
: 指定要发送的数据的字节数。flags
: 通常设置为0,但也可以指定一些特殊的标志来修改send
的行为。例如,MSG_DONTWAIT
标志可以使send
函数在非阻塞套接字上立即返回,如果无法立即发送数据则返回错误。
返回值
- 成功时,
send
返回实际发送的字节数。这个值可能小于len
,特别是当套接字是非阻塞的或发送缓冲区已满时。 - 失败时,返回-1,并设置
errno
以指示错误类型。
void HandlerCommand(int sockfd, InetAddr addr)
{
// 我们把他当做一个长服务
while (true)
{
char commandbuffer[1024]; // 当做字符串
// 1.接收消息(read)
ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO
if (n > 0)
{
commandbuffer[n] = 0;
LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer);
std::string result = Excute(commandbuffer);
// 2.发送消息(write)
::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\n", addr.AddrStr().c_str());
break;
}
}
}
3、TcpServerMain.cc
服务端主函数使用智能指针构造Server对象(参数需要加执行方法),然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!
注意:声明的函数方法只有两个参数,而Command类的命令行处理函数有this指针,因此需要使用bind()绑定函数!
// ./tcpserver 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " local-post" << 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;
}
运行结果
4、完整代码
4.1、TcpServer.hpp
#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#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,
BIND_ERROR,
LISTEN_ERROR
};
const static uint16_t gport = 8888;
const static int gsockfd = -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)
:_service(service), _port(port),_listensockfd(gsockfd),_isrunning(false)
{}
void InitServer()
{
// 1.创建socket
_listensockfd = ::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd < 0)
{
LOG(FATAL,"socket create eror\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 eror\n");
exit(BIND_ERROR);
}
LOG(INFO,"bind success\n");
// 3.因为tcp是面向连接的,tcp需要未来不短地获取连接
// 老板模式,随时等待被连接
if(::listen(_listensockfd,gblcklog) < 0)
{
LOG(FATAL,"listen eror\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);
// 1.获取新连接
int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len);
// 获取失败继续获取
if(sockfd < 0)
{
LOG(WARNING,"sccept reeor\n");
continue;
}
InetAddr addr(client);
LOG(INFO,"get a new link,client info: %s,sockfd:%d\n",addr.AddrStr().c_str(),sockfd); // 4
// 获取成功
// version 2 -- 多线程版 -- 不能关闭fd了,也不需要
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this,addr);
pthread_create(&tid,nullptr,Execute,td); // 新线程分离
}
_isrunning = false;
}
// 无法调用类内成员 无法看到sockfd
static void *Execute(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
pthread_detach(pthread_self()); // 分离新线程,无需主线程回收
td->_self->_service(td->_sockfd,td->_addr); // 执行回调
::close(td->_sockfd);
delete td;
return nullptr;
}
~TcpServer()
{}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
command_service_t _service;
};
4.2、Command.hpp
#pragma once
#include <iostream>
#include <set>
#include <string>
#include <cstring>
#include <cstdio>
#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
}
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 commandbuffer[1024]; // 当做字符串
// 1.接收消息(read)
ssize_t n = ::recv(sockfd, commandbuffer, sizeof(commandbuffer) - 1,0); // TODO
if (n > 0)
{
commandbuffer[n] = 0;
LOG(INFO, "get command from client [%s],command: %s\n", addr.AddrStr().c_str(), commandbuffer);
std::string result = Excute(commandbuffer);
// 2.发送消息(write)
::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\n", addr.AddrStr().c_str());
break;
}
}
}
~Command()
{
}
private:
std::set<std::string> _safe_command; // 只允许执行的命令
};
4.3、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-post" << 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;
}
更多推荐
所有评论(0)