前言
閒話少說,因需要寫一個上位機程序,本來我是打算用純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();
最終的界面如下圖所示 。
完整程序如下連接:
https://download.csdn.net/download/LI15951364431/12309265