QT下使用Asio库搭建一个简易网络服务器端

        在工作中,有时候会需要搭建一个网络服务器端来管理某些网络连接。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、设备名称)。如果没有获取信息的必要,可在连接到来后,即刻发送连接到来信号即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值