写在前面

本文是Qt5下使用socket通信的一个示例,通过该示例,你可以学到:

  1. Qt5下信号与槽的使用方式
  2. Qt中线程与线程之间的通信方式
  3. 拉姆达表达式的简单使用
  4. Qt5下Socket通信的流程

界面样式

客户端启动后的页面如上图所示,端口默认为6665,IP为127.0.0.1连接服务器按钮可点击,断开连接发送信息按钮不可点击,左下角连接状态后面的图片为一把红色的锁。

服务器启动后的页面如上图所示,监听端口默认为6665启动监听按钮可点击,左下角连接状态后面的图片为一把红色的锁。

当点击服务器的启动监听按钮后,该按钮变为不可点击,此时点击客户端的连接服务器按钮,如果连接成功,客户端的历史信息一栏将会出现“连接服务器,成功!”的信息,连接服务器按钮变为不可点击,断开连接发送信息按钮变为可点击。同时,服务器和客户端左下角连接状态后面的图片都会变为一把绿色的盾牌。如上图所示。

当客户端点击断开连接按钮,三个按钮的状态将会回到客户端启动的时候,客户端的历史信息栏将会多出一条“与服务器断开连接”的信息,同时服务器和客户端左下角连接状态后面的图片都会变为一把红色的锁。如上图所示。

如上图所示,在正常连接的状态下,客户端在发送的信息一栏中输入要发送的文字,然后点击发送信息按钮,此时,客户端的历史信息和服务器的历史信息一栏中都会出现“客户端say:xxxxxx”内容。

同理,服务器在发送的信息一栏中输入要发送的文字,然后点击发送信息按钮,此时,服务器的历史信息和客户端的历史信息一栏中都会出现“服务器say:xxxxxx”内容。

客户端代码

mainwindow.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

void initUi();
void connectRegistration();

signals:
void startConnect(unsigned short, QString);
void sendMsg(QString);
void disconnect();

private:
Ui::MainWindow *ui;
QLabel * m_status;
QThread *sub_thread;
SendMsg *sendmsg;
};

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::initUi()
{
setWindowTitle("客户端");

ui->port->setText("6665");
ui->ip->setText("127.0.0.1");
ui->disconnect->setEnabled(false);
ui->sendMsg->setEnabled(false);

// 状态栏
m_status = new QLabel;
m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20));
ui->statusbar->addWidget(new QLabel("连接状态:"));
ui->statusbar->addWidget(m_status);
}

首先构建一个初始化UI页面的函数initUi(),把默认的端口号、IP、按钮的状态以及状态栏的图片设置好。

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

initUi();

// 创建线程对象
sub_thread = new QThread;
// 创建任务对象
sendmsg = new SendMsg;
// sendmsg任务对象在执行的时候,会在子线程里面执行
sendmsg->moveToThread(sub_thread);

connectRegistration();

sub_thread->start();
}

然后我们在主函数中调用initUi(),并且创建一个线程对象,用该线程去处理连接服务器、监听服务器、向服务器发消息等任务,而不是把所有的功能都放在主线程中去做。

sendmsg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SendMsg : public QObject
{
Q_OBJECT
public:
explicit SendMsg(QObject *parent = nullptr);

// 连接服务器
void connectServer(unsigned short port, QString ip);

// 发送消息
void sendMessage(QString msg);

// 客户端主动请求断开连接
void disconnect();

signals:
void connectOk();
void disconnectOK();
void newMessage(QByteArray);


private:
QTcpSocket * m_t;
};

sendmsg.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
SendMsg::SendMsg(QObject *parent) : QObject(parent)
{

}

// 第四步,连接服务器
void SendMsg::connectServer(unsigned short port, QString ip)
{
m_t = new QTcpSocket;
m_t->connectToHost(QHostAddress(ip), port);

// 第五步,若接成功,子线程发射连接成功的信号
connect(m_t, &QTcpSocket::connected, this, [=]()
{
emit connectOk();
});

// 第十一步,子线程监听服务器
connect(m_t, &QTcpSocket::readyRead, this, [=](){
QByteArray data = m_t->readAll();
// 第十二步,有新消息到达,通知主线程
emit newMessage(data);
});

// 第十四步,注册服务器断开连接的监听
connect(m_t, &QTcpSocket::disconnected, this, [=]()
{
m_t->close();
m_t->deleteLater();
emit disconnectOK();
});
}

// 第十步,子线程向服务器发送信息
void SendMsg::sendMessage(QString msg)
{
m_t->write(msg.toUtf8());
}


// 第十八步,子线程关闭连接
void SendMsg::disconnect()
{
m_t->close();
}

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void MainWindow::connectRegistration()
{
// 第一步,点击连接服务器按钮
connect(ui->connServer, &QPushButton::clicked, this, [=](){
// 第二步,主线程发射连接服务器的信号
QString ip = ui->ip->text();
unsigned short port = ui->port->text().toUShort();

emit startConnect(port, ip);
ui->connServer->setEnabled(false);
});

// 第三步,在子线程中执行连接服务器的操作
connect(this, &MainWindow::startConnect, sendmsg, &SendMsg::connectServer);

// 第六步,主线程处理子线程发出的连接成功的信号
connect(sendmsg, &SendMsg::connectOk, this, [=](){
ui->record->append("连接服务器,成功!");
m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20));
ui->disconnect->setEnabled(true);
ui->sendMsg->setEnabled(true);
});

// 第七步,点击发送信息按钮
connect(ui->sendMsg, &QPushButton::clicked, this, [=](){
// 第八步,主线程发射发送信息的信号
QString msg = ui->msg->toPlainText();
ui->record->append("客户端say:" + msg);
emit sendMsg(msg);
});

// 第九步,在子线程中执行发送消息的操作
connect(this, &MainWindow::sendMsg, sendmsg, &SendMsg::sendMessage);

// 第十三步,主线程处理子线程发出的新消息到达的信号
connect(sendmsg, &SendMsg::newMessage, this, [=](QByteArray data){
ui->record->append("服务器say:" + data);
});

// 第十六步,点击断开服务器按钮
connect(ui->disconnect, &QPushButton::clicked, this, [=](){
emit disconnect();
ui->connServer->setEnabled(true);
ui->disconnect->setEnabled(false);
ui->sendMsg->setEnabled(false);
});

// 第十七步,子线程执行断开连接的操作
connect(this, &MainWindow::disconnect, sendmsg, &SendMsg::disconnect);

// 第十五步、第十九步,主线程执行子线程发出的服务器已断开连接的信号
connect(sendmsg, &SendMsg::disconnectOK, this, [=](){
// sub_thread->quit();
// sub_thread->wait();
// sendmsg->deleteLater();
// sub_thread->deleteLater();
m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20));
ui->record->append("与服务器断开连接");
ui->connServer->setEnabled(true);
ui->disconnect->setEnabled(false);
ui->sendMsg->setEnabled(false);
});

OK,代码稍微有点长,我用注释说明了每一步执行的任务以及子线程和主线程之间的通信情况(即信号/槽的对应情况)。connectRegistration()函数就是一个信号与槽的汇总。

第一、二、三步。当我们点击客户端页面上的“连接服务器”按钮(对应mainwindow.cpp里connect(ui->connServer, &QPushButton::clicked, this, [=](){...},这里采用了拉姆达表达式的写法),主线程会发出一个startConnect的信号(对应emit startConnect(port, ip);),告诉子线程,需要连接服务器。而子线程中处理该信号的槽叫connectServer(对应mainwindow.cpp里connect(this, &MainWindow::startConnect, sendmsg, &SendMsg::connectServer);)。这就是主线程与子线程通信的方式。

第四步。子线程开始连接服务器(对应sendmsg.cpp里m_t->connectToHost(QHostAddress(ip), port);)。

第五、六步。若子线程连接服务器成功了,要发出一个connectOk的信号(对应sendmsg.cpp里emit connectOk();)。处理该信号的函数在主线程中(对应mainwindow.cpp里connect(sendmsg, &SendMsg::connectOk, this, [=](){...}),这里会将按钮的状态、历史信息栏以及连接状态的图标重置。

第七、八、九步。点击客户端页面上的“发送信息按钮”(对应mainwindow.cpp里connect(ui->sendMsg, &QPushButton::clicked, this, [=](){...}),主线程会发送一个sendMsg的信号,告诉子线程需要发送信息(对应emit sendMsg(msg);),处理该信号的槽叫sendMessage(对应connect(this, &MainWindow::sendMsg, sendmsg, &SendMsg::sendMessage);)。

第十步。子线程向服务器发送信息(对应sendmsg.cpp里sendMessage(QString msg)函数)。

第十一、十二步,子线程中需要监听服务器,以处理服务器中发送来的信息并通知主线程更新UI(对应sendmsg.cpp里connect(m_t, &QTcpSocket::readyRead, this, [=](){...})。

第十三步,子线程发送信号newMessage后,主线程需要更新UI(对应mainwindow.cpp里ui->record->append("服务器say:" + data);

以上就是一个完整的通信的流程。

同时,接下来,还需要考虑当服务器关闭后,客户端这里需要断开连接的情况。

第十四步,子线程关闭并释放socket对象,通知主线程(对应sendmsg.cpp里emit disconnectOK();)。

第十五步,主线程执行断开连接的信号(对应mainwindow.cpp里connect(sendmsg, &SendMsg::disconnectOK, this, [=](){...})。

第十六、十七、十八、十九步,客户端点击“断开连接”按钮,主动释放连接,那么主线程发射信号emit disconnect();,子线程处理该信号(disconnect()),关闭连接后再通知主线程。

服务器代码

mainwindow.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void initUi();
void connectRegistion();

signals:
void setListenSig(unsigned short);
void sendMsgSig(QString);

private:
Ui::MainWindow *ui;

QLabel * m_status;
QThread * sub_thread;
RecvMsg * recv_msg;
};

mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

initUi();
connectRegistion();
}

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

void MainWindow::connectRegistion()
{
// 绑定启动监听的事件
connect(ui->setListen, &QPushButton::clicked, this, [=](){
unsigned short port = ui->port->text().toUShort();
emit setListenSig(port);
ui->setListen->setEnabled(false);
});

connect(this, &MainWindow::setListenSig, recv_msg, &RecvMsg::setListenSlt);

connect(recv_msg, &RecvMsg::newConnectionSig, this, [=](){
m_status->setPixmap(QPixmap(":/connect.png").scaled(20, 20));
});

connect(recv_msg, &RecvMsg::recvMsgSig, this, [=](QByteArray data){
ui->record->append("客户端say:" + data);
});

// 发送信息
connect(ui->sendMsg, &QPushButton::clicked, this, [=](){
// 以纯文本的方式发送
QString msg = ui->msg->toPlainText();
emit sendMsgSig(msg);
ui->record->append("服务端say:" + msg);
});

connect(this, &MainWindow::sendMsgSig, recv_msg, &RecvMsg::sendMsgSlt);
connect(recv_msg, &RecvMsg::breakConnection, this, [=](){
m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20));
});
}


void MainWindow::initUi()
{
sub_thread = new QThread;
recv_msg = new RecvMsg;

ui->port->setText("6665");

setWindowTitle("服务器");

// 状态栏
m_status = new QLabel;
m_status->setPixmap(QPixmap(":/disconnect.png").scaled(20, 20));
ui->statusbar->addWidget(new QLabel("连接状态:"));
ui->statusbar->addWidget(m_status);

recv_msg->moveToThread(sub_thread);

sub_thread->start();
}

recvmsg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RecvMsg : public QObject
{
Q_OBJECT
public:
explicit RecvMsg(QObject *parent = nullptr);

// 设置监听
void setListenSlt(unsigned short port);

// 发送消息
void sendMsgSlt(QString msg);

signals:
void newConnectionSig();
void recvMsgSig(QByteArray);
void breakConnection();
private:
QTcpServer * m_s;
QTcpSocket * m_t;
};

recvmsg.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
RecvMsg::RecvMsg(QObject *parent) : QObject(parent)
{

}

void RecvMsg::setListenSlt(unsigned short port)
{
// 创建监听服务器
m_s = new QTcpServer;
// 启动监听
m_s->listen(QHostAddress::Any, port);

// 等待连接
connect(m_s, &QTcpServer::newConnection, this, [=](){
m_t = m_s->nextPendingConnection();

// 检测是否可以接受数据
connect(m_t, &QTcpSocket::readyRead, this, [=](){
QByteArray data = m_t->readAll();
emit recvMsgSig(data);
});

// 断开连接
connect(m_t, &QTcpSocket::disconnected, this, [=](){
m_t->close();
m_t->deleteLater();
emit breakConnection();
});

emit newConnectionSig();
});
}

void RecvMsg::sendMsgSlt(QString msg)
{
m_t->write(msg.toUtf8());
}

执行的过程请参照分析客户端代码的时候来,此处不再赘述。客户端与服务器的完整代码可以点击链接 socket_demo 下载。使用 Qt Creator 将两个项目导入后即可查看。