c++ socket编程之服务端编写

发布时间 2023-04-01 11:39:23作者: z5onk0

开头

  • 想要写一个带界面、功能全面、传输高效、运行稳定的马儿,能够在生产环境下工作
  • 在cursor的帮助下,用一天时间完成了服务端和客户端的编写
  • 另外一天时间卡在了中文消息传输处理和大文件传输粘包、分包问题上

功能

  • 收发消息,支持中文消息
  • 发送命令执行并显示命令执行结果
  • 任意格式文件传输,支持大文件
  • 监控客户端状态
  • 多线程建立连接,非阻塞式通信,可同时完成多个传输任务,

服务端技术栈

  • qt ui界面设计
  • 使用QTcpSocket、QTcpServer类进行网络通信
  • 使用QThread类+moveToThread方法建立子线程
  • 主线程和子线程间通过信号和槽进行通信
  • 子线程访问ui的方法:ui线程创建子线程时,将ui指针传递给子线程的public成员,之后子线程就可以通过public成员来操作ui界面

服务端设计

  • 一共四个类,mainwindowserverthreadclientthreadconnectionlist
  • mainwindow是主窗口类,负责ui展示,用户交互,对serverthread进行创建和控制
  • serverthread是对QTcpSocket类的封装,负责建立连接,创建clientthread类,并转发mainwindow的消息
  • clientthread类是对QTcpServer类的封装,负责处理连接,接收来自上层的消息,以及数据收发
  • connectionlist类用于展示客户端状态,在主窗口中点击菜单栏按钮时弹出状态展示对话框
  • serverthread每创建一个clientthread,建立信号槽后,就将其move到线程中运行

服务端核心代码

  • mainwindow
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    listen_status = new QLabel();
    listen_status->setObjectName("listenstatus");
    listen_status->setText(tr("断开监听"));
    ui->statusbar->addPermanentWidget(listen_status);
    ui->disconnect->setEnabled(false);
    ui->sendmsg->setEnabled(false);
    ui->sendcmd->setEnabled(false);
    ui->sendfile->setEnabled(false);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_openfile_clicked()
{
    QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::homePath());
    if(!fileName.isEmpty()){
        ui->choosedfile->setText(fileName);
    }
}

void MainWindow::on_action_triggered()
{
    if (listen_status->text() == "正在监听" )
    {
        connectionlist *connectionList = new connectionlist(this);
        connectionList->setList(m_server->m_clients);
        connectionList->show();
    }
    else
    {
        QMessageBox::information(NULL, "警告", "未启动服务器!");
    }
}

void MainWindow::on_listen_clicked()
{
    m_server = new ServerThread(this);
    m_server->m_port = ui->port;
    m_server->m_maxminum = ui->maxmium;
    m_server->m_recvmsg = ui->recvmsg;
    m_server->m_inputcmd = ui->inputcmd;
    m_server->m_inputfile = ui->choosedfile;
    m_server->m_inputmsg = ui->inputmsg;
    connect(ui->sendmsg, &QPushButton::clicked, m_server, &ServerThread::onMsgButtonClicked);
    connect(ui->sendcmd, &QPushButton::clicked, m_server, &ServerThread::onCmdButtonClicked);
    connect(ui->sendfile, &QPushButton::clicked, m_server, &ServerThread::onFileButtonClicked);
    connect(m_server, &ServerThread::serverClosed, this, &MainWindow::onServerClosed);
    listen_status->setText(tr("正在监听"));
    m_server->startListen();
    ui->listen->setEnabled(false);
    ui->disconnect->setEnabled(true);
    ui->sendmsg->setEnabled(true);
    ui->sendcmd->setEnabled(true);
    ui->sendfile->setEnabled(true);
}

void MainWindow::on_disconnect_clicked()
{
    listen_status->setText(tr("断开监听"));
    m_server->stopListen();
    ui->listen->setEnabled(true);
    ui->disconnect->setEnabled(false);
    ui->sendmsg->setEnabled(false);
    ui->sendcmd->setEnabled(false);
    ui->sendfile->setEnabled(false);
}

void MainWindow::onServerClosed()
{
    if (m_server != nullptr)
    {
        delete m_server;
        m_server = nullptr;
        QMessageBox::information(NULL, "提示", "已释放所有连接资源!");
    }
}


void MainWindow::on_sendmsg_2_clicked()
{
    ui->recvmsg->clear();
}
  • serverthread
#include "serverthread.h"

ServerThread::ServerThread(QObject *parent) : QObject(parent)
{
    m_server = new QTcpServer(this);
}

void ServerThread::setStop()
{
    state = true;
}

void ServerThread::setStart()
{
    state = false;
}

void ServerThread::startListen()
{
    quint16 port = m_port->toPlainText().toInt();
    quint16 maxmium = m_maxminum->toPlainText().toInt();
    qDebug("监听端口是: %d", port);
    qDebug("最大连接数是: %d", maxmium);
    m_server->setMaxPendingConnections(maxmium);
    connect(m_server, &QTcpServer::newConnection, this, &ServerThread::handleNewConnection);
    m_server->listen(QHostAddress::Any, port);
}

void ServerThread::stopListen()
{
    m_server->close();
    QList<QTcpSocket *> clients = m_server->findChildren<QTcpSocket *>();
    for (QTcpSocket *client : clients)
    {
        client->close();
    }
    for (QThread *thread : m_threads)
    {
        thread->exit(0);
    }
    QMessageBox::information(NULL, "提示", "已关闭所有连接和子线程");
    emit serverClosed();
}

void ServerThread::handleNewConnection()
{
    qDebug("新连接已建立");
    QThread *thread = new QThread(this);
    QTcpSocket *new_client = m_server->nextPendingConnection();
    ClientThread *client_thread = new ClientThread(new_client, this);
    client_thread->setStart();
    client_thread->m_recvmsg = this->m_recvmsg;
    client_thread->m_inputmsg = this->m_inputmsg;
    client_thread->m_inputcmd = this->m_inputcmd;
    client_thread->m_inputfile = this->m_inputfile;
    QMessageBox::information(nullptr, "新连接已建立", "新连接来自于: " + client_thread->getClientIP());
    m_clients.append(client_thread->getClientIP());
    client_thread->moveToThread(thread);
    thread->start();
    m_threads.append(thread);
    connect(this, &ServerThread::sendMsg, client_thread, &ClientThread::sendMsg);
    connect(this, &ServerThread::sendFile, client_thread, &ClientThread::sendFile);
    connect(this, &ServerThread::sendCmd, client_thread, &ClientThread::sendCmd);
}

void ServerThread::onMsgButtonClicked()
{
    qDebug("准备给各个client发消息......");
    emit sendMsg();
}

void ServerThread::onFileButtonClicked()
{
    qDebug("准备给各个client发文件......");
    emit sendFile();
}

void ServerThread::onCmdButtonClicked()
{
    qDebug("准备给各个client发命令......");
    emit sendCmd();
}
  • clientthread,实际上实现了一个很简单的数据传输协议
#include "clientthread.h"

ClientThread::ClientThread(QTcpSocket *client_socket, QObject *parent) : QObject(parent)
{
    m_client = client_socket;
    connect(m_client, &QTcpSocket::readyRead, this, &ClientThread::recvMsg);
}

void ClientThread::setStart()
{
    state = true;
}

void ClientThread::setStop()
{
    state = false;
}

void ClientThread::recvMsg()
{
    QByteArray recv_data = m_client->readAll();
    QString client_ip = getClientIP();
    QString data = "[来自" + client_ip + "的消息]\n" + QString::fromUtf8(recv_data);
    qDebug("接收数据: %s", qPrintable(data));
    m_recvmsg->append(QString(data));
    qDebug("已接收到消息并推送至窗口");
}

void ClientThread::sendMsg()
{
    QString data = m_inputmsg->toPlainText();
    QString prefix = "MSG|";
    data = prefix + data;
    QByteArray byteArray = data.toUtf8();
    qDebug("发送数据: %s", qPrintable(data));
    m_client->write(byteArray);
    qDebug("已发送消息至目标机器");
}

void ClientThread::sendCmd()
{
    QString data = m_inputcmd->toPlainText();
    QString prefix = "CMD|";
    data = prefix + data;
    QByteArray byteArray = data.toUtf8();
    qDebug("发送命令: %s", qPrintable(data));
    m_client->write(byteArray);
    qDebug("已发送命令至目标机器");
}

void ClientThread::sendFile()
{
    QString filepath = m_inputfile->toPlainText();
    QFile file(filepath);
    if (!file.open(QIODevice::ReadOnly))
    {
        QMessageBox::warning(NULL, "错误", "打开文件:" + file.errorString() + " 失败,无法发送!");
        return;
    }
    QByteArray fileData = file.readAll();
    qDebug("发送文件大小为: %s", qPrintable(fileData.size()));
    QString prefix = "FILE|";
    QString preFileData = prefix + QString::number(fileData.size()) + "|";
    m_client->write(preFileData.toUtf8());
    m_client->write(fileData);
    qDebug("已发送文件至目标机器");
}

QString ClientThread::getClientIP()
{
    return m_client->peerAddress().toString();
}

遇到的问题

  • 线程回收问题:qt线程必须手动回收,建立线程时,将线程加入到线程列表中,在断开连接后,需要从线程列表中取出线程手动退出
  • socket资源回收问题:建立socket时,将clientsocket加入到列表中,断开连接后取出socket关闭
  • 线程安全问题:本文采用的子线程直接访问ui的方式不太安全,更安全的方法是在子线程中发送带参数的消息,在ui线程中进行处理
  • 外层对象和里层对象通信的问题:两边都可以采用信号槽的机制通信,但只能在外层对象创建里层对象后,在外层对象中connnect,不能再里层对象中connenct
  • 中文消息传输的问题:中文消息乱码的原因是通信两端采用的字符编码不一致,本文中在服务端读取中文字符串后,按照UTF-8编码格式转化为字节数组发送,在客户端接收到字节数组后,转化为Unicode字符串

效果展示

  • 主界面
  • 监控主机列表