之前发布了一篇文章,前端是一样的,只不过后端用的nodejs技术做的后台脚本,那个脚本太简单了,毕竟是解释性语言,就是节省开发时间,而且性能也不会太差。
这次则用C++方式实现了一版后台,用了boost做xml解析和互斥锁,websocketpp做websocket库,libssh2则是与sshd交互的库。程序采用多线程方式,有心跳监测,经过大量测试,运行相对很稳定的一段程序。
后台典型代码:
// global.h
#ifndef __GLOBAL_H__
#define __GLOBAL_H__
#include <iostream>
#include <list>
#include <vector>
#include <map>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <thread>
#include <signal.h>
using namespace std;
#endif
// SSH.h
#ifndef _SSH_H_20220720__
#define _SSH_H_20220720__
#include <iostream>
#include <map>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <libgen.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <termios.h>
#include <libssh2.h>
using namespace std;
class SSH
{
private:
const char *host = NULL;
unsigned short port = 0;
const char *username = NULL;
const char *password = NULL;
int sock = -1;
LIBSSH2_SESSION *session = NULL;
LIBSSH2_CHANNEL *channel = NULL;
public:
SSH(const char *host, const unsigned short port, const char *username, const char *password);
~SSH();
bool Connect();
void DissConnect();
ssize_t Write(const char *buffer, int n);
ssize_t Read(char *buffer, int n);
int Resize(int width, int height);
int Socket() {return sock;}
bool ChannelEof() {return libssh2_channel_eof(channel) == 1;}
};
#endif
// SSH.cpp
#include "SSH.h"
SSH::SSH(const char *host, const unsigned short port, const char *username, const char *password)
{
this->host = host;
this->port = port;
this->username = username;
this->password = password;
}
SSH::~SSH()
{
DissConnect();
}
void SSH::DissConnect()
{
if (channel)
{
libssh2_channel_free(channel);
channel = NULL;
}
if (session)
{
libssh2_session_disconnect(session, "Session Shutdown, Thank you for playing");
libssh2_session_free(session);
session = NULL;
}
if (sock != -1)
{
close(sock);
sock = -1;
}
libssh2_exit();
}
bool SSH::Connect()
{
if (libssh2_init(0) != 0)
{
fprintf(stderr, "libssh2 initialization failed\n");
return false;
}
LIBSSH2_SESSION *session = NULL;
LIBSSH2_CHANNEL *channel = NULL;
int sock = socket(AF_INET, SOCK_STREAM, 0);
this->sock = sock;
unsigned long hostaddr = inet_addr(this->host);
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(this->port);
sin.sin_addr.s_addr = hostaddr;
if (connect(sock, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)) != 0)
{
fprintf(stderr, "Failed to established connection!\n");
DissConnect();
return false;
}
int sockFlags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, sockFlags | O_NONBLOCK);
/* Open a session */
session = libssh2_session_init();
this->session = session;
if (libssh2_session_startup(session, sock) != 0)
{
fprintf(stderr, "Failed Start the SSH session\n");
DissConnect();
return false;
}
/* Authenticate via password */
if (libssh2_userauth_password(session, username, password) != 0)
{
fprintf(stderr, "Failed to authenticate\n");
DissConnect();
return false;
}
/* Open a channel */
channel = libssh2_channel_open_session(session);
if (channel == NULL)
{
fprintf(stderr, "Failed to open a new channel\n");
DissConnect();
return false;
}
this->channel = channel;
/* Request a PTY */
if (libssh2_channel_request_pty(channel, "xterm") != 0)
{
fprintf(stderr, "Failed to request a pty\n");
DissConnect();
return false;
}
/* Request a shell */
if (libssh2_channel_shell(channel) != 0)
{
fprintf(stderr, "Failed to open a shell\n");
DissConnect();
return false;
}
libssh2_channel_set_blocking(channel, false);
return true;
}
ssize_t SSH::Write(const char *buffer, int n)
{
return libssh2_channel_write(channel, buffer, n);
}
ssize_t SSH::Read(char *buffer, int n)
{
return libssh2_channel_read(channel, buffer, n);
}
int SSH::Resize(int width, int height)
{
return libssh2_channel_request_pty_size(channel, width, height);
}
// websocket.h
#ifndef __WEBSOCKET_H_20220722__
#define __WEBSOCKET_H_20220722__
#include "global.h"
void RunWebsocketServer(); // 启动并运行websocket服务端
void WebSocketCheckClientAlive(); // 主动给客户端发心跳,保持其在线状态,踢掉超时的客户端
void RunEPollSockt();
#endif
// websocket.cpp
#include "SSH.h"
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <json.hpp>
using namespace nlohmann;
#define EVENT_NUM 5
typedef websocketpp::server<websocketpp::config::asio> WebsocketServer;
typedef WebsocketServer::message_ptr message_ptr;
static map<string, string> config;
static WebsocketServer server;
// 标志唯一的客户端(client_ip_port变量):固定的前缀+客户端IP+客户端PORT组成,如 [::ffff:1.119.166.13]:61662
// 每个客户端 -> 连接句柄
static map<string, websocketpp::connection_hdl> client_hdl_map;
// 客户端连接句柄,对应的是活跃时间戳
static map<string, time_t> client_time_map;
static map<string, SSH *> client_ssh_map;
static map<int, SSH *> sock_ssh_map;
static map<int, websocketpp::connection_hdl> sock_hdl_map;
boost::mutex map_mutex; // 互斥锁
int epfd = -1;
void AddSockToEpoll(int sock)
{
struct epoll_event ev;
ev.data.fd = sock; // 要监视的文件描述符,可以是任何打开的在/proc/pid/fd/目录下的fd
ev.events = EPOLLIN; // 监听读状态同时设置LT模式
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev); // 注册epoll事件
}
void DelSockFromEpoll(int sock)
{
struct epoll_event ev;
ev.data.fd = sock; // 要监视的文件描述符,可以是任何打开的在/proc/pid/fd/目录下的fd
ev.events = EPOLLIN; // 监听读状态同时设置LT模式
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, &ev); // 移除epoll事件
}
static WebsocketServer::connection_ptr GetConFromHdl(websocketpp::connection_hdl hdl)
{
try
{
return server.get_con_from_hdl(hdl);
}
catch (websocketpp::exception const &e)
{
cout << "GetConFromHdl failed: " << e.what() << endl;
return NULL;
}
}
bool OnValidate(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return false;
}
string client_ip_port = con->get_remote_endpoint();
string key = "Sec-WebSocket-Protocol";
string token = con->get_request_header(key);
std::cout << key << ": " << token << std::endl;
if (token != "shell")
{
return false;
}
map_mutex.lock();
SSH *ssh = new SSH(config["sshhost"].c_str(), atoi(config["sshport"].c_str()), config["sshusername"].c_str(), config["sshpassword"].c_str());
if (!ssh->Connect())
{
delete ssh;
return false;
}
client_ssh_map[client_ip_port] = ssh;
client_hdl_map[client_ip_port] = hdl;
client_time_map[client_ip_port] = time(NULL);
sock_ssh_map[ssh->Socket()] = ssh;
sock_hdl_map[ssh->Socket()] = hdl;
AddSockToEpoll(ssh->Socket());
con->append_header(key, token); // 以后回复数据都要加这个头部信息
cout << client_ip_port << " write to memory ok!" << endl;
map_mutex.unlock();
return true;
}
void SendMsgToClient(websocketpp::connection_hdl hdl, const string msg)
{
string client_ip_port = "Unknown";
try
{
WebsocketServer::connection_ptr con = server.get_con_from_hdl(hdl);
client_ip_port = con->get_remote_endpoint();
server.send(hdl, msg, websocketpp::frame::opcode::text);
}
catch (websocketpp::exception const &e)
{
std::cout << "send msg to " << client_ip_port << " failed because: "
<< "(" << e.what() << ")" << std::endl;
}
}
void RunEPollSockt()
{
char buffer[4096];
struct epoll_event events[EVENT_NUM];
epfd = epoll_create(EVENT_NUM);
while (1)
{
int nfds = epoll_wait(epfd, events, 5, -1);
for (int i = 0; i < nfds; i++)
{
int fd = events[i].data.fd;
SSH *ssh = sock_ssh_map[fd];
websocketpp::connection_hdl hdl = sock_hdl_map[fd];
string str;
while (1)
{
memset(buffer, 0, sizeof buffer);
if (ssh->Read(buffer, sizeof buffer - 1) > 0)
{
str += buffer;
}
else
{
break;
}
}
if (!str.empty())
{
SendMsgToClient(hdl, str);
}
}
}
}
void OnOpen(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return;
}
string client_ip_port = con->get_remote_endpoint();
cout << "client connected: " << client_ip_port << endl;
server->send(hdl, config["copyright"] + "\r\n", websocketpp::frame::opcode::text);
}
map<string, websocketpp::connection_hdl>::iterator EraseMemory(const string &client_ip_port)
{
map<string, websocketpp::connection_hdl>::iterator iter = client_hdl_map.begin();
if (client_ip_port == "Unknown" || client_ip_port.empty())
{
cout << client_ip_port << ", Cann't erase memory!" << endl;
return iter;
}
iter = client_hdl_map.find(client_ip_port);
if (iter != client_hdl_map.end())
{
cout << client_ip_port << " closed, erase memory for it..." << endl;
iter = client_hdl_map.erase(iter);
client_time_map.erase(client_ip_port);
SSH *ssh = client_ssh_map[client_ip_port];
if (ssh)
{
DelSockFromEpoll(ssh->Socket());
sock_ssh_map.erase(ssh->Socket());
sock_hdl_map.erase(ssh->Socket());
client_ssh_map.erase(client_ip_port);
delete ssh;
}
cout << client_ip_port << " erase memory success!" << endl;
}
else
{
cout << client_ip_port << " no need to erase memory!" << endl;
}
return iter;
}
void OnClose(WebsocketServer *server, websocketpp::connection_hdl hdl)
{
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return;
}
map_mutex.lock();
string client_ip_port = con->get_remote_endpoint();
EraseMemory(client_ip_port);
map_mutex.unlock();
}
void OnMessage(WebsocketServer *server, websocketpp::connection_hdl hdl, message_ptr data)
{
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return;
}
string str = data->get_payload();
json obj = json::parse(str, nullptr, false);
if (obj.is_discarded())
{
cout << "Not valid msg: " << str << endl;
}
else
{
map_mutex.lock();
string client_ip_port = con->get_remote_endpoint();
SSH *ssh = client_ssh_map[client_ip_port];
if (obj["Op"] == "stdin")
{
string msg = obj["Data"];
cout << "receive msg from " << client_ip_port << ": " << msg << endl;
ssh->Write(msg.c_str(), msg.size());
}
else
{
int width = obj["Cols"];
int height = obj["Rows"];
printf("%s resize pty width = %d height = %d\n", client_ip_port.c_str(), width, height);
ssh->Resize(width, height);
}
map_mutex.unlock();
}
}
// 如果客户端发来ping,可以更新活跃时间戳,并回复pong
bool OnPing(WebsocketServer *server, websocketpp::connection_hdl hdl, std::string payload)
{
server->get_alog().write(websocketpp::log::alevel::app, payload);
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return false;
}
map_mutex.lock();
string client_ip_port = con->get_remote_endpoint();
cout << "recv ping from " << client_ip_port << ": " << payload << endl;
if (client_time_map.find(client_ip_port) != client_time_map.end())
{
client_time_map[client_ip_port] = time(NULL); // 客户端ping时更新时间戳
try
{
server->pong(hdl, "pong"); // 客户端ping时回复pong
}
catch (websocketpp::exception const &e)
{
cout << "pong failed: " << e.what() << endl;
}
}
map_mutex.unlock();
return true;
}
// 服务器会定时发送ping给客户端,客户端会自动回复pong,回复时更新活跃时间戳
bool OnPong(WebsocketServer *server, websocketpp::connection_hdl hdl, std::string payload)
{
server->get_alog().write(websocketpp::log::alevel::app, payload);
WebsocketServer::connection_ptr con = GetConFromHdl(hdl);
if (!con)
{
return false;
}
string client_ip_port = con->get_remote_endpoint();
cout << "recv pong from " << client_ip_port << ": " << payload << endl;
map_mutex.lock();
if (client_time_map.find(client_ip_port) != client_time_map.end())
{
client_time_map[client_ip_port] = time(NULL); // 客户端回复pong时更新活跃时间戳
}
map_mutex.unlock();
return true;
}
static void ReadConfigXml()
{
config["port"] = "8880";
using boost::property_tree::ptree;
ptree pt;
ptree root;
string cfg_path = "config.xml";
try
{
read_xml(cfg_path, pt);
root = pt.get_child("root");
}
catch (std::exception &e)
{
std::cout << "Error: " << e.what() << endl;
return;
}
std::string str = pt.get<string>("root.server.<xmlattr>.port");
if (!str.empty())
{
config["port"] = str;
}
str = pt.get<string>("root.ssh.<xmlattr>.host");
if (!str.empty())
{
config["sshhost"] = str;
}
str = pt.get<string>("root.ssh.<xmlattr>.port");
if (!str.empty())
{
config["sshport"] = str;
}
str = pt.get<string>("root.ssh.<xmlattr>.username");
if (!str.empty())
{
config["sshusername"] = str;
}
str = pt.get<string>("root.ssh.<xmlattr>.password");
if (!str.empty())
{
config["sshpassword"] = str;
}
str = pt.get<string>("root.ssh.<xmlattr>.copyright");
if (!str.empty())
{
config["copyright"] = str;
}
}
void RunWebsocketServer()
{
ReadConfigXml();
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
server.set_reuse_addr(true); // 设置套接字选项SO_REUSEADDR
server.set_ping_handler(bind(&OnPing, &server, ::_1, ::_2));
server.set_pong_handler(bind(&OnPong, &server, ::_1, ::_2));
// Set logging settings
// server.set_access_channels(websocketpp::log::alevel::all);
server.clear_access_channels(websocketpp::log::alevel::all); // 不输出日志
// Initialize ASIO
server.init_asio();
// Register our open handler
server.set_open_handler(bind(&OnOpen, &server, ::_1));
// Register our close handler
server.set_close_handler(bind(&OnClose, &server, _1));
// Register our message handler
server.set_message_handler(bind(&OnMessage, &server, _1, _2));
server.set_validate_handler(bind(&OnValidate, &server, ::_1));
// Listen on port 2152
unsigned short port = atoi(config["port"].c_str());
try
{
server.listen(port);
cout << "listen at port " << port << endl;
// Start the server accept loop
server.start_accept();
// Start the ASIO io_service run loop
server.run();
}
catch (websocketpp::exception const &e)
{
cout << "error: " << e.what() << endl;
exit(-1);
}
}
// 定时扫描所有客户端,断开超时未回复的客户端
void WebSocketCheckClientAlive()
{
int timeout = 3;
while (1)
{
sleep(timeout);
map_mutex.lock();
printf("client size = %d\n", client_hdl_map.size());
for (map<string, websocketpp::connection_hdl>::iterator iter = client_hdl_map.begin(); iter != client_hdl_map.end();)
{
string client_ip_port = iter->first;
websocketpp::connection_hdl hdl = iter->second;
printf("check [%s] alive?\n", client_ip_port.c_str());
WebsocketServer::connection_ptr pconn;
try
{
pconn = server.get_con_from_hdl(hdl);
}
catch (websocketpp::exception const &e)
{
cout << "get_con_from_hdl failed: " << e.what() << endl;
iter = EraseMemory(client_ip_port);
continue;
}
iter++;
time_t last_seconds = client_time_map[client_ip_port];
time_t this_seconds = time(NULL);
int escaped_seconds = this_seconds - last_seconds;
int pass_seconds = this_seconds - last_seconds;
if (pass_seconds > timeout * 2 + 2)
{
printf("%s last pong is %d sec ago, timeout! Close it now!\n", client_ip_port.c_str(), escaped_seconds);
try
{
pconn->close(websocketpp::close::status::normal, "timeout");
}
catch (websocketpp::exception const &e)
{
cout << "close failed: " << e.what() << endl;
}
}
else if (pass_seconds >= timeout)
{
printf("%s last pong is %d sec ago. Now ping it!\n", client_ip_port.c_str(), escaped_seconds);
try
{
server.ping(hdl, "ping");
}
catch (websocketpp::exception const &e)
{
cout << "ping failed: " << e.what() << endl;
}
}
else
{
printf("%s last pong is %d sec ago! No need to ping!\n", client_ip_port.c_str(), escaped_seconds);
}
}
map_mutex.unlock();
}
}
// main.cpp
/**
* @file main.cpp
* @author 丁华能
* @brief 连接过来的websocket带token,根据token查session中的信息,根据信息关联客户端,订阅消息,收到数据后根据关联客户端进行发送。
* @version 0.1
* @date 2022-05-18
*
* @copyright Copyright (c) 2022
*
*/
#include "websocket.h"
using namespace std;
void epoll_thread()
{
RunEPollSockt();
}
void websocket_thread()
{
RunWebsocketServer();
}
void init_daemon(void){
pid_t pid;
int i;
pid = fork();
if(pid < 0)
{
printf("Error Fork\n");
exit(1);
}
else if(pid > 0)
{
exit(0);
}
setsid();
// umask(0);
//忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
for(i=0; i<3; close(i++));
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
}
void ChangeDIRToCurrentApp(const char *argv0)
{
string path(argv0);
path = path.substr(0, path.find_last_of('/'));
chdir(path.c_str());
}
int main(int argc, char **argv)
{
ChangeDIRToCurrentApp(argv[0]);
if (argc == 1)
{
init_daemon();
}
thread subscribe(epoll_thread);
thread websocket(websocket_thread);
WebSocketCheckClientAlive();
subscribe.join();
websocket.join();
return 0;
}
<!-- config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<server port="92" />
<ssh host="127.0.0.1" port="22" username="test" password="111111" copyright="版权所有©"/>
</root>
Makefile脚本内容如下:
.PHONY : clean
GCC = g++
ROOTDIR = …
INCLUDE = -I$(ROOTDIR)/lib/inc
LIB =
libs = -lboost_system -lpthread -lssh2
runlibs = -Wl,-rpath=.
src = $(wildcard *.cpp)
obj = $(patsubst %.cpp,%.o, $(src))
bin = sshserver
$(bin) : $(obj)
$(GCC) $(runlibs) $^ -o $@ $(LIB) $(libs)
$(obj): %.o : %.cpp
$(GCC) -c -g $(INCLUDE) $< -o $@
clean :
rm -f $(obj) $(bin)
websocketpp的下载地址:https://github.com/zaphoyd/websocketpp
boost的下载地址:https://www.boost.org/
libssh2下载地址:https://github.com/libssh2/libssh2
注意看main.cpp中支持参数部分,运行带任意参数就是调试模式,否则守护进程运行。
前端代码参考文章:https://blog.csdn.net/canlynetsky/article/details/125928915