网络通信是Qt框架的常见应用,本文以TCPWebSocket为例介绍了二者实现信息收发的基本方法和细节,可以作为初始开发的基本demo,采用cmake方法管理基本文件依赖关系,本文暂不涉及界面开发部分,直接基于QObject而不是QWidget。

前置环境说明

在Linux环境下进行网络通信实验是比较方便的,Windows下则需要一些步骤去适配动态库差异,本文兼顾了两个系统运行,所以某些步骤对Linux环境可能是多余的,本文采用mingw来编译工程,应该提前检查环境变量,并且在Qt Creator-帮助-关于插件-搜索cmake勾选"load"

Ubuntu防火墙检查

首次使用某端口进行通信时,常常需要打开防火墙,windows会有弹窗提示,点击允许即可,linux则使用ufw检查

1
2
3
4
5
6
7
8
9
10
11
sudo ufw status
sudo ufw allow 8887 #开放端口
sudo ufw enable #启动防火墙
sudo ufw reload #重新加载规则

sudo ufw status #端口已经更新

## 其余规则
sudo ufw delete allow 8887 #取消端口许可
sudo ufw allow from 192.168.254.254 #增加ip许可
sudo ufw delete allow from 192.168.254.254 #取消ip许可

TCP Socket

文件架构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
F:\QT\QT_CODE\TCP
│ clientSocket.cpp
│ clientSocket.h
│ CMakeLists.txt
│ serverSocket.cpp
│ serverSocket.h
├─client
│ client.cpp
│ CMakeLists.txt
├─server
│ CMakeLists.txt
│ server.cpp
└─libs

使用Qt实现客户端接收输入流、服务器端打印字符的效果,类似在Linux操作系统:网络编程中实现的TCP客户端的效果一样:

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
#顶层Cmake
cmake_minimum_required(VERSION 3.5)
project(tcp)

set(CMAKE_AUTOMOC ON)

find_package(Qt5 REQUIRED
COMPONENTS
Network #for tcp
)

#输出动态库/可执行程序
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_SOURCE_DIR}/build/debug)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/build/release)
endif()


#set(SOPATH ${CMAKE_SOURCE_DIR}/libs)
#set(LIBRARY_OUTPUT_PATH ${SOPATH})

# 动态库设计代码
add_library(${PROJECT_NAME} SHARED)

target_sources(${PROJECT_NAME} PRIVATE
clientSocket.h
clientSocket.cpp
serverSocket.h
serverSocket.cpp
)

target_include_directories(${PROJECT_NAME} PRIVATE .)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Network
)

# 执行程序
add_subdirectory(client)
add_subdirectory(server)

Server设计

服务器监听端口连接和接收信息,这里仅使用了一对一通信,如果需要监听多个客户端需要额外创建客户端编号、数组管理即可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//serverSocket.h
#pragma once
#include <QtNetwork>
#define MAXSIZE 128

class ServerSocket:public QObject{
Q_OBJECT
public slots:
void getNewMsg();

public:
ServerSocket(QObject* parent = nullptr);

public:
QTcpServer* tcpServer = nullptr;
QTcpSocket* clientSocket;
};

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
//serverSocket.cpp
#include "serverSocket.h"
#include <QtNetwork>

ServerSocket::ServerSocket(QObject* parent):QObject(parent){
tcpServer = new QTcpServer(this);
if(tcpServer->listen(QHostAddress::Any,9999)){
qDebug()<<"listenning...";
}
connect(tcpServer,&QTcpServer::newConnection,[=](){
clientSocket = tcpServer->nextPendingConnection(); //获得新客户端
qDebug()<<"Connected!!";

//打印客户端信息
QString ip = clientSocket->peerAddress().toString();
quint16 port = clientSocket->peerPort();
qDebug()<<QString("[%1:%2] Connected!!").arg(ip).arg(port);

//连接状态接收信息
if(clientSocket && clientSocket->state()==QTcpSocket::ConnectedState){
qDebug()<<"try to recv:";
connect(clientSocket,&QTcpSocket::readyRead,this,&ServerSocket::getNewMsg);
}
});
}

//打印信息
void ServerSocket::getNewMsg(){
qDebug()<<"begin parse msg";
QByteArray recvMsg = clientSocket->readAll();
if(strncmp(recvMsg.data(),"quit",4)==0){
clientSocket->abort();
clientSocket->deleteLater();
return;
}
QString reMsg = QString::fromUtf8(recvMsg);
qDebug() << reMsg;
}

服务器实例化:构造一个实例监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//server.cpp
#include <QDebug>
#include "serverSocket.h"

using namespace std;

int main(int argc, char*argv[]){
QCoreApplication app(argc, argv);

qDebug() << "This is server";
ServerSocket serverSocket;

return app.exec();
}
服务器依赖处理:
1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.5)
project(server)

add_executable(${PROJECT_NAME})

target_sources(${PROJECT_NAME} PRIVATE server.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE ..)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Network
tcp #our dll
)

Client设计

连接9999端口,并通过循环从输入流获取信息并且发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//clientSocket.h
#pragma once
#include <QDebug>
#include <QCoreApplication>
#include <QTcpSocket>

#define MAXSIZE 128

class ClientSocket:public QObject{
Q_OBJECT
public:
ClientSocket(QObject*parent = nullptr);
void SendMsg();

public slots:
void handleError(QAbstractSocket::SocketError error); //槽函数检查错误

private:
QTcpSocket* tcpClient;
};
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
//clientSocket.cpp
#include "clientSocket.h"
#include <QtNetwork>
#include <QDebug>

ClientSocket::ClientSocket(QObject*parent):QObject(parent){
tcpClient = new QTcpSocket(this);
tcpClient->connectToHost("127.0.0.1",9999);

if (!tcpClient->waitForConnected(3000)) { // 等待 3 秒连接
qDebug() << "Connection failed:" << tcpClient->errorString();
}

connect(tcpClient,&QTcpSocket::connected,[=](){
qDebug()<<"Client connected!!";
});

//Qt 5.15一下使用槽函数检查错误
connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), this, \
SLOT(handleError(QAbstractSocket::SocketError)));
}

void ClientSocket::handleError(QAbstractSocket::SocketError error){
qDebug()<<"Client Connect error:"<<error;
tcpClient->close();
tcpClient->deleteLater();
}

void ClientSocket::SendMsg(){
qDebug()<<tcpClient->state();
char buf[MAXSIZE];
qint64 len = 0;

while(1){
if(strncmp(buf,"quit",4)==0)
break;
fgets(buf,32,stdin);
buf[strlen(buf)-1] = '\0'; //避免回车空行
len += tcpClient->write(buf,strlen(buf));
tcpClient->flush(); //即刻刷新发送
qDebug()<<"Succeed Send:"<<len;
}
}
实例化一个客户端:
1
2
3
4
5
6
7
8
9
10
//client.cpp
#include <QDebug>
#include "clientSocket.h"

int main(int argc, char* argv[]){
qDebug()<<"This is client";
ClientSocket clientSocket;
clientSocket.SendMsg();
return 0;
}
客户端调用动态库方法:
1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.5)
project(client)

add_executable(${PROJECT_NAME})

target_sources(${PROJECT_NAME} PRIVATE client.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE ..)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::Network
tcp #our dll
)

效果与讨论

clientf发server收: TCP单工-clientf发server收

这里一个细节是此处在client的main函数中并没有使用事件循环,而是直接break掉并且return 0,在实际工程中这种写法并不多见,因为也很少使用while去阻塞一个进程任务,因为Qt的事件循环和事件驱动具有更加灵活的机制,所以以上while方法读取输入并不典型。

再者,如果需要实现全双工通信,按照原来的方法需要创建新线程或者新进程去监听和打印,在Qt元对象系统中则也有更方便的方法,因为Qt具有事件循环机制,例如可以通过定时监听输入事件驱动的方式来实现全双工通信,以下WebSocket实现了这两种方式的demo。

QWebSocket

TCP是传输层的通信协议,而WebSocket是基于TCP应用层的全双工通信协议,其定义了更加多的消息交互细节,例如WebSocket面对消息通信,支持二进制和文本两种格式发送,也额外支持信道安全通信协议,例如非加密的ws和加密的wss(TLS加密),更常常用于信息发送和接受。对比http协议,WebSocket不仅支持客户端请求-服务器应答机制,也支持服务器直接发送事件,即客户端和服务器的通信地位是平等的。

定时监听输入

对于服务器和客户端,均设定定时任务,相隔100ms监听输入流,如果得到输入流就通过WebSocketsendTextMessage将信息发出,同时服务器和客户端都通过connect事件打印接收输出,从而实现双工通信,且无需while阻塞输入,文件组织同上述tcp,以下:

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
#顶层Cmake
cmake_minimum_required(VERSION 3.5)
project(websocket)

set(CMAKE_AUTOMOC ON)

find_package(Qt5 REQUIRED
COMPONENTS
WebSockets
)

#输出动态库/可执行程序
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_SOURCE_DIR}/build/debug)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/build/release)
endif()

#set(SOPATH ${CMAKE_SOURCE_DIR}/libs)
#set(LIBRARY_OUTPUT_PATH ${SOPATH})

add_library(${PROJECT_NAME} SHARED)
target_sources(${PROJECT_NAME} PRIVATE
webClient.cpp
webClient.h
webServer.cpp
webServer.h
)

target_include_directories(${PROJECT_NAME} PRIVATE .)
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt5::Core
Qt5::WebSockets
)

add_subdirectory(server)
add_subdirectory(client)

Server设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//webServer.h
#pragma once

#include <QCoreApplication>
#include <QWebSocket>
#include <QWebSocketServer>
#include <QTimer>

class WebServer:public QObject{
Q_OBJECT
public:
WebServer(QObject* parent = nullptr);

void sendMsg();

private:
QWebSocketServer* webServer = nullptr;
QWebSocket* m_socket = nullptr;
QTimer* inputTimer;
};
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
//webServer.cpp
#include "webServer.h"
#include <QDebug>

WebServer::WebServer(QObject *parent):QObject(parent){
inputTimer = new QTimer(this); //输入监听定时器
webServer = new QWebSocketServer("WebServer",QWebSocketServer::NonSecureMode, this); //非安全模式
if(webServer->listen(QHostAddress::Any, 9999)){
qDebug() << "listenning...";
}

connect(webServer,&QWebSocketServer::newConnection, this, [=](){ //新客户端
m_socket = webServer->nextPendingConnection();
QString ip = m_socket->peerAddress().toString();
quint16 port = m_socket->peerPort();
qDebug()<<QString("[%1:%2] Connected!!").arg(ip).arg(port);

if(m_socket!=nullptr && m_socket->state() == QAbstractSocket::ConnectedState){ //接收行为
qDebug() << "try to recv";
connect(m_socket,&QWebSocket::textMessageReceived, this, [=](const QString &msg){
qDebug() << "recv:"<<msg;
});
}

connect(inputTimer, &QTimer::timeout, this, &WebServer::sendMsg);
inputTimer->setSingleShot(false); //100ms重复触发sendMsg
inputTimer->start(100);
});
}

//非阻塞定时轮询发送
void WebServer::sendMsg(){
Q_ASSERT(m_socket);
QTextStream cin(stdin);
if(!cin.atEnd()){
QString input = cin.readLine();
if(!input.isEmpty()){
m_socket->sendTextMessage(input);
m_socket->flush();
}
}
}

实例化服务器对象:

1
2
3
4
5
6
7
8
9
10
11
#include <QDebug> 
#include "webServer.h"

int main(int argc, char*argv[]){
QCoreApplication app(argc, argv);

qDebug() << "This is Server";
WebServer wServer;

return app.exec();
}
server库的调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.5)
project(server)

add_executable(server)
target_sources(server PRIVATE
server.cpp
)

target_include_directories(server PRIVATE ..)

target_link_libraries(server PRIVATE
Qt5::Core
Qt5::WebSockets
websocket
)

Client设计

客户端除了和服务器相同发送和接收策略,因为WebSocket没有专门的wait函数,这里还引入了超时检查,提高客户端连接的鲁棒性:

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
//webClient.h
#pragma once
#include <QCoreApplication>
#include <QWebSocket>
#include <QTimer>

#define MAXSIZE 1024

class WebClient:public QObject{
Q_OBJECT
public:
WebClient(QObject* parent = nullptr);
void SendTextMsg();
void sendMsg();
void onHandleSend();
bool waitEvent(int timesec);
void onClose();

public slots:
void handleError(QAbstractSocket::SocketError);

private:
QWebSocket* webClient;
QTimer* inputTimer;
qint64 sendLen;
};

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
//webClient.cpp
#include "webClient.h"
#include <QDebug>
#include <QTimer>

//超时检查
bool WebClient::waitEvent(int timesec){
Q_ASSERT(webClient != nullptr);
QEventLoop eventLoop;
bool ret = true;
bool r = connect(webClient,&QWebSocket::connected, &eventLoop, &QEventLoop::quit); //连接,退出事件循环
Q_ASSERT(r);
//QTimer mtimer;
//mtimer.singleShot(timesec*1000, &eventLoop, &QEventLoop::quit);
QTimer::singleShot(timesec*1000, &eventLoop,[&]{ //等待timesec,超时,ret=false才退出
ret = false;
eventLoop.quit();
});
eventLoop.exec();
return ret;
}

WebClient::WebClient(QObject* parent) : QObject(parent),sendLen(0){
webClient = new QWebSocket();
webClient->setParent(this);
inputTimer = new QTimer(this);
webClient->open(QUrl("ws://127.0.0.1:9999"));
if(waitEvent(3)){ //3s无超时
qDebug() << "Connected!!";
connect(webClient,&QWebSocket::textMessageReceived, this, [=](const QString &msg){
qDebug() << "recv:"<<msg;
});
connect(inputTimer,&QTimer::timeout, this, &WebClient::sendMsg);
inputTimer->setSingleShot(false);
inputTimer->start(100);
}
//错误处理
connect(webClient, SIGNAL(error(QAbstractSocket::SocketError)), this, \
SLOT(handleError(QAbstractSocket::SocketError)));
}

void WebClient::onClose(){
webClient->close();
webClient->deleteLater();
}

void WebClient::handleError(QAbstractSocket::SocketError error){
qDebug()<<error;
onClose();
}

//非阻塞定时轮询发送
void WebClient::sendMsg(){
Q_ASSERT(webClient);
QTextStream cin(stdin);
if(!cin.atEnd()){
QString input = cin.readLine();
if(!input.isEmpty()){
sendLen += webClient->sendTextMessage(input);
webClient->flush();
qDebug()<<"sendlen:"<<sendLen;
}
}
}

因为没有用while阻塞,这里正常使用事件循环代表客户端持续监听:

1
2
3
4
5
6
7
8
9
10
11
#include <QDebug>
#include "webClient.h"

int main(int argc, char* argv[]){
QCoreApplication app(argc, argv);

qDebug()<<"This is Client";
WebClient webClient;

return app.exec();
}
cmake写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.5)
project(client)

add_executable(client)
target_sources(client PRIVATE
client.cpp
)

target_include_directories(client PRIVATE ..)

target_link_libraries(client PRIVATE
Qt5::Core
Qt5::WebSockets
websocket
)

效果

至此,基于定时监听输入、事件驱动发送、接受的模式就写完了,双端都可以实现信息的发送和接收: 定时监听输入-全双工通信

事件监听输入

windows的cmd/powershell默认为阻塞输入,对于非阻塞监听程序就处于不可输入状态,因此本方法只在Linux环境奏效

去掉定时器对象成员,改为QSocketNotifier监听,定义成员变量:

1
QSocketNotifier* noti;
构造函数定义初始化并连接信号:
1
2
3
4
5
//init
noti = new QSocketNotifier(0,QSocketNotifier::Read, this); //0代表stdin

//signal
connect(noti, &QSocketNotifier::activated, this, &WebClient::sendMsg);

处理输入流:

1
2
3
4
5
6
7
8
void WebClient::sendMsg() {
Q_ASSERT(webClient);
QTextStream cin(stdin);
QString input;
cin.readLineInto(&input);
sendLen += webClient->sendTextMessage(input);
qDebug() << "sendlen:" << sendLen;
}
WebServer同理;这里的代码实现是有缺陷的,监听stdin时发出信息,接收端需要键入回车才会正常收到,但考虑到实际工程很少直接使用stdin,问题就留在这里了。

仓库:见QtNetWorking_Basic

Q&A

  1. Qt Creator不支持查找CMakeLists,仅查找.pro文件;
  • 没有在帮助-关于插件处load cmake;
  1. 编译报错error: undefined reference to 'vtable for ClientSocket'或典型的LNK2001 error
    1
    2
    3
    4
    error LNK2001: 无法解析的外部符号 "public: virtual struct QMetaObject const * __thiscall TraceTest::metaObject(void)const " (?metaObject@TraceTest@@UBEPBUQMetaObject@@XZ)
    error LNK2001: 无法解析的外部符号 "public: virtual void * __thiscall TraceTest::qt_metacast(char const *)" (?qt_metacast@TraceTest@@UAEPAXPBD@Z)
    error LNK2001: 无法解析的外部符号 "public: virtual int __thiscall TraceTest::qt_metacall(enum QMetaObject::Call,int,void * *)" (?qt_metacall@TraceTest@@UAEHW4Call@QMetaObject@@HPAPAX@Z)
    fatal error LNK1120: 3 个无法解析的外部命令
  • MOC文件没有引入,如果是cmake构建的项目必须加上set(CMAKE_AUTOMOC ON)代表使用moc生成必要文件;