前言
闲话少说,因需要写一个上位机程序,本来我是打算用纯C++开发的,奈何我不是程序员科班出身,对多线程+win socket那一套不是很熟悉,平时写写简单的测试案例还可以,但是要我写出一个能稳定运行的落地的项目,纯C++开发对我来说太难了。其实Qt我也不是很熟悉,但Qt毕竟大多模块帮你封装好了,适用qt可以大大缩短开发周期。
qt多线程
网上关于qt多线程的大多都是继承QThread,然后重写run函数。这种方式其实还是应用c++共享内存多线程通信那一套思想,实现起来比较复杂。Qt官网推荐使用继承QObject,然后把对象movetToThread的方式,充分利用Qt的信号槽机制进行主线程和子线程之间的通信,这种方式有点像Go语言的channel机制,非常方便且简单。使用Qt的多线程需记住以下几个思想:
1. 线程所有者
子线程是在主线程中创建的,应该使用主线程控制子线程的生命周期,比如在主线程中结束子线程(quit),而不是在子线程中自己结束自己。
2. 线程资源
Qt文档特别强调,资源只属于创建它的那个线程。
代码实现
1. 服务端头文件
基本思想:QTcpServer有一个newConnection信号,监听时,每次有新的连接都会发出这个信号。我们建立一个槽函数来处理这个信号,当有新的连接时,把新的客户端socket添加到服务器的vector中,服务器管理所有客户端socket。当有客户端断开连接时,客户端socket会发出disconnected信号,我们建立槽函数来处理这个信号。具体的服务器头文件如下:
typedef struct tagMsg
{
int port;
QString ip;
QByteArray buf;
int size;//! buf的字节数
}Msg;
typedef struct tagClient
{
int port;
QString ip;
QTcpSocket* sock;
}Client;
class CTcpServer : public QObject
{
Q_OBJECT
public:
CTcpServer(QObject *parent = nullptr);
~CTcpServer();
signals:
//! 有新的客户端
void signal_newClient(const QString ip, const int port);
//! 发送给客户端的消息
void signal_newMsg(const Msg);
//! 客户端断开连接
void signal_disConncet(const QString ip, const int port);
public slots:
void init();
//! 监听
void onConnect(const QString ip, const int port);
void recvData();
void sendData(Msg);
private slots:
//! 处理新的客户端连接
void newConnect();
//! 处理客户端断开连接
void onDisConnect();
private:
QTcpServer* _server;
QList<Client> _clients;
QTcpSocket* _currentSock;
};
这里需要重点说明一下那个init槽函数。 前面有说过,Qt资源只属于创建它的那个线程。QTcpServer不能在构造函数中初始化,因为这个CCtcpServer是在主线程中创建的,即属于主线程,如果在构造函数中创建QTcpServer,则其也属于主线程,但是那些槽函数属于子线程,在子线程中直接调用主线程的资源是不允许的(可以想象一下这个被允许的话,如果主线程创建了很多子线程,每个子线程都可以直接调用主线程的资源,那么主线程的资源是不是就不安全了)。所以,要想在子线程中调用QTcpServer,那么这个tcp必须在子线程中建立,这就要求在子线程start的时候创建这个资源。
2. 服务端实现代码
CTcpServer::CTcpServer(QObject *parent)
: QObject(parent)
{
_server = nullptr;
_currentSock = nullptr;
}
CTcpServer::~CTcpServer()
{
if (nullptr != _server)
{
delete _server;
_server = nullptr;
}
}
void CTcpServer::init()
{
if (_server != nullptr)
{
delete _server;
_server = nullptr;
}
else
{
_server = new QTcpServer;
}
}
void CTcpServer::onConnect(const QString ip, const int port)
{
if (_server != nullptr)
{
if (!_server->isListening())
{
QHostAddress addr(ip);
_server->listen(addr, port);
connect(_server, &QTcpServer::newConnection, this, &CTcpServer::newConnect);
}
}
}
void CTcpServer::newConnect()
{
_currentSock = _server->nextPendingConnection();
//! 获取远端客户端ip和端口号
QString ip = _currentSock->peerAddress().toString();
int port = _currentSock->peerPort();
emit signal_newClient(ip, port);
Client tmp;
tmp.ip = ip;
tmp.port = port;
tmp.sock = _currentSock;
_clients.append(tmp);
connect(_currentSock, &QTcpSocket::readyRead, this, &CTcpServer::recvData);
connect(_currentSock, &QTcpSocket::disconnected, this, &CTcpServer::onDisConnect);
}
void CTcpServer::recvData()
{
for (QList<Client>::iterator it = _clients.begin(); it != _clients.end(); ++it)
{
if ((it->sock)->isReadable())
{
QByteArray buf = (it->sock)->readAll();
if (!buf.isEmpty())
{
//! 获取远端socket的ip和端口号
QString ip = (it->sock)->peerAddress().toString();
int port = (it->sock)->peerPort();
Msg msg;
msg.buf = buf;
msg.ip = ip;
msg.port = port;
emit signal_newMsg(msg);
}
}
}
}
void CTcpServer::onDisConnect()
{
for (QList<Client>::iterator it = _clients.begin(); it != _clients.end(); ++it)
{
if (QAbstractSocket::UnconnectedState == it->sock->state())
{
emit signal_disConncet(it->ip, it->port);
it = _clients.erase(it, it + 1);
it--;
}
}
}
void CTcpServer::sendData(Msg msg)
{
qDebug() << "sendData\n";
QString ip = msg.ip;
int port = msg.port;
for (QList<Client>::iterator it = _clients.begin(); it != _clients.end(); ++it)
{
if (it->ip == ip && it->port == port)
{
qDebug() << "发送字节:" << it->sock->write(msg.buf) << "\n";
}
}
}
3. 主窗口
在主窗口中我们把子线程的started信号与CTcpServer的init信号连接,确保QTcpServer在子线程中被创建,如下所示。
_tcp_server = new CTcpServer;
_server_thread = new QThread;
_tcp_server->moveToThread(_server_thread);
connect(_server_thread, &QThread::started, _tcp_server, &CTcpServer::init);
_server_thread->start();
最终的界面如下图所示 。