在工作中,有时候会需要搭建一个网络服务器端来管理某些网络连接。QT自带的QSever可能不是那么好用,在工作过程中,也接触到了另一个C++的网络库Asio,并以此搭建一个简易的Server端。
首先,下载好Asio所需的文件,博主这里使用的是Asio的代码文件,也可使用库文件,请按需使用。(Asio库包含在Boost库中,但是也可以单独使用)
单独的Asio下载链接:Asio C++ Library
由于我这里使用的是Asio的文件,我就将其文件放在工程下,在调用处引用头文件即可。
// 引用Asio头文件
#include "asio.hpp"
// 使用asio的命名空间,如果使用的是boost库则为 boost::asio::ip::tcp
using asio::ip::tcp;
在使用过程中,我需要一个将套接字部分信息进行存储,因此,在文件中声明了两个类,分别为Session类和Sever类,分别对应服务器和会话。会话Session类中存储有设备IP、设备名称、设备ID信息。Session类头文件声明:
class Session : public QObject,public std::enable_shared_from_this<Session>
{
Q_OBJECT
public:
Session(tcp::socket socket, std::function<void(Session*)> on_disconnect);
~Session();
void close(); // 关闭当前套接字,用于下线操作
void getClinetInfo(); // 获取套接字信息,如IP、ID、设备名称
tcp::socket socket_; // 网络套接字
uint32_t getIndex(); // 获取设备ID
QString getIPaddress(); // 获取IP地址
QString getDeviceName(); // 获取设备名称
signals:
void firstConnection(QString index,QString name,QString ip); // 接收设备信息信号
void recvFirstPackage(); // 接收到第一个数据包
void clientOffline(); // 设备下线
private:
void isConnectionAlive();
private:
std::function<void(Session*)> on_disconnect_; // 处理网络下线的函数
QString clientIP; // 设备IP
uint32_t index; // 设备ID
QString deviceName; // 设备名称
bool infoReadSuccess; // 设备信息是否读取成功
char data[1024]; // 用于接收网络信息
int maxLength = 1024;
QThread *heartBeat = nullptr; // 检测存活线程
};
服务器类头文件声明:
class NetServer : public QObject
{
Q_OBJECT
public:
explicit NetServer(asio::io_context& io_context, short port,QObject *parent = nullptr);
void disconnectClient(int clientIndex); // 下线指定客户端
void pauseListening(); // 暂停监听
void resumListening(); // 继续开始监听
int getSessionFromID(uint32_t index); // 通过设备ID获取会话,用于发送和接收数据
QVector<std::shared_ptr<Session>> getConnectList(); // 获取连接列表
signals:
void newConnection(QString index,QString name,QString ip); // 新的连接到来
void clientDisconnected(QString index,QString name,QString ip); // 客户端下线
private:
void doAccept(); // 接收到来的客户端连接
void removeSocket(Session* session); // 下线操作
private:
tcp::acceptor acceptor_; // 用于处理接收客户端连接服务器
tcp::socket socket_; // 连接到来处理
asio::io_context& io_context_;
QVector<std::shared_ptr<Session>> sessionList; // 列表列表
mutable std::mutex sessionListMutex;
bool is_listening_;
};
因为在使用过程中,需要接收第一个包来获取当前连接信息,因此连接到来信号会在第一个包接受信息完毕后,才发送接收信号。
看完上述函数声明及其变量声明后,接下来内容为函数实现部分。
首先当服务器类被初始化完成后,服务器开始监听,递归调用doAccept函数进行监听操作
void NetServer::doAccept()
{
if (is_listening_)
{
acceptor_.async_accept(socket_,[this](std::error_code ec)
{
if (!ec)
{
std::lock_guard<std::mutex> lock(sessionListMutex);
auto new_session = std::make_shared<Session>(std::move(socket_), [this](Session* session) {
removeSocket(session);
}); // 将Socket移入,将removeSocket函数传递过去用于处理客户端下线操作
sessionList.append(new_session);
new_session->getClinetInfo();
connect(new_session.get(),&Session::firstConnection,this,&NetServer::newConnection);
DebugLogger::log("New connection accepted");
qDebug()<<__LINE__<<"New connection accepted";
}
doAccept(); // 递归调用,接收连接到来
});
}
}
当有新的连接到来时,会生成一个新的Session会话类,当会话类第一包接收完毕后,发送Session类的firstConnection信号,然后再又信号槽将服务器类的newConnection发射,告诉调用服务器类的对象当前已有一个连接成功到来。且会调用会话类获取信息函数,接收第一个数据包完成对设备名称、设备ID的获取。
当连接到来是,在构造函数中,会获取当前连接IP以及开启连接存活的心跳检测
Session::Session(tcp::socket socket, std::function<void (Session *)> on_disconnect)
: socket_(std::move(socket)),on_disconnect_(on_disconnect)
{
// 会话建立好后,默认获取IP
try {
clientIP = QString::fromStdString(socket_.remote_endpoint().address().to_string());
connect(this,&Session::recvFirstPackage,this,[=]{
if (heartBeat == nullptr)
{
heartBeat = QThread::create([&]{
isConnectionAlive();
});
connect(this,&Session::clientOffline,heartBeat,&QThread::deleteLater);
heartBeat->start();
}
});
}
catch (const std::exception& e) {
clientIP = "Unknown";
}
}
简易的心跳检测
void Session::isConnectionAlive()
{
if (!socket_.is_open()) // 套接字不是打开状态则返回不做处理,防止触发下线信号
{
return;
}
asio::error_code ec;
// 尝试发送空数据(非阻塞)
socket_.write_some(asio::buffer(""), ec);
if (ec)
{
emit clientOffline(); // 连接关闭则发送下线信号
close();
on_disconnect_(this);
}
QThread::sleep(2); // 设置发送时间间隔
isConnectionAlive(); // 重复调用自身往套接字写空数据包
}
设备信息获取处,当信息获取完毕后,发送recvFirstPackage信号将当前会话的心跳检测开启,紧接着第二个信号则是将当前连接的设备ID、设备名称和IP发送给服务器端,再由服务器端发送给调用服务器端的对象使用。
void Session::getClinetInfo()
{
auto self(shared_from_this());
memset(data,0,maxLength);
socket_.async_read_some(asio::buffer(data, maxLength),
[this, self](std::error_code ec, std::size_t length) {
if (!ec) // 没有错误信息说明接收成功
{
// 处理接收到的第一个数据包
std::string ip_addr = socket_.remote_endpoint().address().to_string();
index = data[4];
deviceName = BitConvert::hexStringToValue(data + 7,length - 9); //减去开头发送方、接收方、任务标识加结尾0x0d、0x0a
emit recvFirstPackage();
emit firstConnection(QString::number(index),deviceName,clientIP);
}
});
}
会话类删除调用的函数disconnectClient()。当会话下线时,会调用该函数,并将其从列表中删除。
void NetServer::disconnectClient(int clientIndex)
{
std::lock_guard<std::mutex> lock(sessionListMutex);
for (auto iter : qAsConst(sessionList))
{
if (iter.get()->getIndex() == (uint32_t)clientIndex) // 找到设备ID后关闭套接字连接,并将列表中的会话删除
{
iter.get()->close();
removeSocket(iter.get());
break;
}
}
}
当函数下线时,需要关闭当前会话,调用close函数
void Session::close()
{
std::error_code ec;
socket_.shutdown(tcp::socket::shutdown_both, ec); // 关闭套接字
if (ec)
{
DebugLogger::log("Shutdown error for " + clientIP + ": " + "Error code is:" + QString::number(ec.value()) + ".Error info is:" + QString::fromStdString(ec.message()));
}
socket_.close(ec);
if (ec)
{
DebugLogger::log("Close error for " + clientIP + ": " + "Error code is:" + QString::number(ec.value()) + ".Error info is:" + QString::fromStdString(ec.message()));
}
}
服务器开始监听和恢复监听函数
void NetServer::pauseListening()
{
is_listening_ = false;
DebugLogger::log("Server paused listening......");
}
void NetServer::resumListening()
{
if (!is_listening_)
{
is_listening_ = true;
DebugLogger::log("Server resumed listening");
doAccept();
}
}
当上述步骤做完后,一个简易的使用Asio库进行编写的网络服务器端就写好了,能够正常识别到连接的到来,通过接收首包获取设备信息(设备ID、设备名称)。如果没有获取信息的必要,可在连接到来后,即刻发送连接到来信号即可。