开头
- 想要写一个带界面、功能全面、传输高效、运行稳定的马儿,能够在生产环境下工作
- 在cursor的帮助下,用一天时间完成了服务端和客户端的编写
- 另外一天时间卡在了中文消息传输处理和大文件传输粘包、分包问题上
功能
- 收发消息,支持中文消息
- 发送命令执行并显示命令执行结果
- 任意格式文件传输,支持大文件
- 监控客户端状态
- 多线程建立连接,非阻塞式通信,可同时完成多个传输任务,
服务端技术栈
- qt ui界面设计
- 使用QTcpSocket、QTcpServer类进行网络通信
- 使用QThread类+moveToThread方法建立子线程
- 主线程和子线程间通过信号和槽进行通信
- 子线程访问ui的方法:ui线程创建子线程时,将ui指针传递给子线程的public成员,之后子线程就可以通过public成员来操作ui界面
服务端设计
- 一共四个类,
mainwindow
、serverthread
、clientthread
和connectionlist
mainwindow
是主窗口类,负责ui展示,用户交互,对serverthread进行创建和控制
serverthread
是对QTcpSocket类的封装,负责建立连接,创建clientthread类,并转发mainwindow
的消息
clientthread
类是对QTcpServer类的封装,负责处理连接,接收来自上层的消息,以及数据收发
connectionlist
类用于展示客户端状态,在主窗口中点击菜单栏按钮时弹出状态展示对话框
serverthread
每创建一个clientthread
,建立信号槽后,就将其move到线程中运行
服务端核心代码
#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();
}
#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字符串
效果展示
- 主界面
- 监控主机列表