QT-3.4 socket编程

本文介绍了一个基于 Qt 的 Socket 通信示例,通过事件循环处理非阻塞 I/O,无需使用额外线程。该教程提供了客户端和服务端的实现细节,包括如何建立连接、发送接收数据及管理客户端。

Introduction

You can find the original German version of this tutorial written by forum member anda_skoa here.

This tutorial gives an example of the use of eventloop based Qt socket classes.
Eventloop based means that one can deal with non-blocking input/output without threads.

It is important to pay attention to the fact that when you work with eventloop, you can not block any function called from eventloop. In the case of QSocket it means that slots connected to QSocket signals must be used only for short tasks.

Besides, it means that the methods of QSocket class terminate usually immediately and the task is delayed, therefore the result of the task can be known only on the ground of signals.

Production

The demo project is supplied with qmake project data. In order to compile the server and the client it is nessesary to set QMAKESPEC and QTDIR propely. Then the production goes easely:

# > make
The main makefile produces two respective makefiles and then makes the both the server and the client.

Client

The client has simple GUI made with Designer. The first interesting code snippet is located in the constructor of ClientMainWindow class:

1:
2:
3:
4:
5:
m_socket = new QSocket(this);
QObject::connect(m_socket, SIGNAL(connected()), this, SLOT(slotConnected()));
QObject::connect(m_socket, SIGNAL(connectionClosed()), this, SLOT(slotDisconnected()));
QObject::connect(m_socket, SIGNAL(error(int)), this, SLOT(slotError(int)));
QObject::connect(m_socket, SIGNAL(readyRead()), this, SLOT(slotRead()));
Here the instance of QSocket is created and its signals are connected with the slots of the window class.

As stated above, the result of work of a QSocket method can be got through a signal. For example, if QSocket::connectToHost establishes connection successfully, then the socket emits connected() signal. If the attempt of connection failed, error(int) is sent so that the integer parameter will give us the hint for finding the reason of the failure (see ClientMainWindow::slotError method connected for that purpose).

It is very easy to set up a server connection:

1:
2:
3:
4:
QString host = m_host->text();
int port = m_port->value();

m_socket->connectToHost(host, port);
The QSocket::connectToHost method establishes a connection and returns after that immediately.
The first parameter is the hosts IP address or hostname transformed into IP address by means of QDns.

Please note that after the call the eventloop is not blocked, thus we have in that call thereafter e.g. an infinite loop.

QSocket is inherited from QIODevice and therefore can be used for example as the destination of a stream:

1:
2:
3:
4:
5:
6:
7:
8:
QString input = m_input->text();
if (input.isEmpty()) return;

if (m_socket->state() == QSocket::Connected)
{
QTextStream stream(m_socket);
stream << input << endl;
}
Here it is checked whether the socket is connected and if yes, a text line is sent through the socket.
In this case the endline is important because the protocol of this simple example in based on the assumption that a transmission is always ended with newline, in other words, consists of plain lines.

In general, if arbitrary data is transferred, it is always better to transfer only little packets and to store the rest in a buffer.
Using bytesWritten signals one can track when all the data of a whole current packet were transferred and when it is time to send the next one.

To read from the socket it is possible either again using QIODevice functionality or as in the example with the method of the QSocket class:

1:
2:
3:
QString text;
while (m_socket->canReadLine())
text += m_socket->readLine();
This code reads from the slot connected with readyRead socket signal. This signal is emitted every time when new data arrive to the socket.
Since we know that the trailing newline comes, we begin to do something if the a whole line can be read.

In general, it is checked with QSocket::bytesAvialable the amount of the data received and then this amount is read in a buffer.

Server

The server has also simple GUI.
The ServerSocket class derived from QServerSocket performs the main part of the server functionality - the establishment of client connections.
The abstract method (pure virtual) newConnection(int) is implemented for that purpose:

1:
2:
3:
4:
5:
6:
7:
void ServerSocket::newConnection(int socketfd)
{
QSocket* socket = new QSocket(parent());
socket->setSocket(socketfd);

emit newClient(socket);
}
The integer parameter is the file descriptor of data socket of new connection.
We create a new QSocket instance and let it work with this file descriptor.
In order to use this new connection we have to let our server know that a new connection was already established and it is available now. This is done with a self-explanatory newClient(QSocket*) signal. The new socket as the parameter is passed to newClient().

The first part of servermainwindow.cpp file is the client class:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
class Client
{
public:
Client(QSocket* socket, QListViewItem* item)
: m_socket(socket), m_item(item), m_num(++m_count) {};

~Client() {
delete m_socket;
delete m_item;
}

inline QSocket* socket() { return m_socket; }

inline QListViewItem* item() { return m_item; }

inline int number() { return m_num; }

protected:
static int m_count;
QSocket* m_socket;
QListViewItem* m_item;
int m_num;
};
The class is more or less grouped data associated with a client: the socket for data transfer to client, the ListView item in the declaration of the class and the client number.
The number is a sequential number used for the sake of simplicity here to name a client.

The class is defined in CPP file since it has importance only for the server class.

In the constructor of the ServerMainWindow class we use a feature of the Qt container:

1:
m_clients.setAutoDelete(true);
setAutoDelete(true) instructs the container to delete all its entries when they are removed from it.
We will use this later in order to delete all the client instances with simple clear() call.

Our server behaves like a simple chatserver. It sends the lines of text to all clients continually.
The sending process take place in the same manner as in case of client with a QTextStream, which treats connection sockets as QIODevice.

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
void ServerMainWindow::sendToClients(const QString& text)
{
if (text.isNull()) return;

// iterate over all clients and send them the text
QPtrDictIterator iter(m_clients);
for (; iter.current() != 0; ++iter)
{
QSocket* sock = iter.current()->socket();
QTextStream stream(sock);
stream << text;
}
}
We use iterator so that access all entries of the dictionary.
The entries in the dictionary are therefore client instances and therefore they have the socket() method returning the connection socket.

In order to be able to accept all connections we must create a ServerSocket and connect it to newClient:

1:
2:
3:
4:
5:
6:
7:
8:
9:
m_server = new ServerSocket(m_port->value(), this);
if (!m_server->ok())
{
delete m_server;
m_server = 0;
return;
}

QObject::connect(m_server, SIGNAL(newClient(QSocket*)), this, SLOT(slotNewClient(QSocket*)));
If bind() succeeded to reserve the port, the QServerSocket::ok() method returns "true" otherwise - "false".

If it failed, we delete useless SocketServer instant and terminate the method. Otherwise we connect newClient signal with a slot which processes the new connection.

This method looks like the following:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
QListViewItem* item = new QListViewItem(m_list, socket->peerAddress().toString(), QString::number(socket->peerPort()));
Client* client = new Client(socket, item);

// notify all others about the newcomer
sendToClients(QString("Server: Client %0 connected/n").arg(client->number()));

m_clients.insert(socket, client);

QObject::connect(socket, SIGNAL(connectionClosed()), this, SLOT(slotClientDisconnected()));
QObject::connect(socket, SIGNAL(readyRead()), this, SLOT(slotSocketRead()));

// great the newcomer
QTextStream stream(socket);
stream << "Server: Hi/nYou are client " << client->number() << endl;
First of all we create a ListView item indicating the connection in our GUI. Then we create the client instance which we use internally for administration of the connection. Then we add this client it in the dictionary after telling all previously registered clients about this latest registered client.

We connect QSocket signals with corresponding slots (as done in the client code) in order to manage incoming data and disconnection.

If a client terminates the connection, the socket emits connectionClosed signal. The slot is called and the following code is run:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
QObject* sender = const_cast(QObject::sender());
QSocket* socket = static_cast(sender);

qDebug("client disconnected");

//disconnect signals
socket->disconnect();

// remove from dict
Client* client = m_clients.take(socket);

sendToClients(QString("Server: Client %0 disconnected/n").arg(client->number()));

delete client;
Since we connected the signal of each client socket with the same slot, first of all we have to find out which client's socket has emitted the signal.
We implement that with QObject::sender(). At this point it is necessary to pay attention that this slot may not be called as a method because in this case the value returned by the method is undefined!

The take() removes the entry from the dictionary without performing the automatic deletion which we have permitted with AutoDelete. If we need not the client instance any longer, we can use remove instead of take and delete.

We process incoming data similarly as in client code:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
QObject* sender = const_cast(QObject::sender());
QSocket* socket = static_cast(sender);

Client* client = m_clients.find(socket);

while (socket->canReadLine())
{
QString line = socket->readLine(); // read the line of text

// and send it to everone including the sender
sendToClients(QString("Client %0: %1").arg(client->number()).arg(line));

client->item()->setText(2, line.left(line.length()-1));
m_list->update();
}
As in the previous method we determine first which the socket has new data. Then we use this pointer in order to look up the associated client instance in our dictionary.
Then we read all available lines and send them to all the clients including the current client.
At the end we update still the view of the server GUI.

If the server is to stop its service, we must turn off all the clients:

1:
2:
3:
4:
5:
6:
7:
8:
QPtrDictIterator iter(m_clients);
for (; iter.current() != 0; ++iter)
{
// disconnect the socket's signals from any slots or signals
iter.current()->socket()->disconnect();
}

m_clients.clear(); // delete all clients (m_clients has autodelete ON)
We iterate trough the dictionary and disconnect the signals and slots. Then we destroy all the instances of the clients by emptying the dictionary. Since AutoDelete was enabled, it destroys all the entries. Then the client destructor deletes the socket of the connection which thereby is closed.

Translated from German by Alexander Atamas

 

使用 Qt 框架基于 C++ 进行数据采集开发,通常涉及多个模块的协作,包括串口通信、网络通信、文件读写、定时器控制以及用户界面交互等。以下是一个系统性的开发方法概述: ### 3.1 数据采集方式的选择 在 Qt 中进行数据采集,首先需要确定数据来源。常见的数据采集方式包括: - **串口通信**:适用于通过 RS-232、RS-485 等接口连接的传感器或设备。 - **网络通信**:适用于通过 TCP/IP 或 UDP 协议获取远程设备的数据。 - **模拟/数字输入**:适用于连接数据采集卡(DAQ)设备,常用于工业控制场景。 - **定时器触发**:用于周期性地从本地文件、数据库或模拟信号中读取数据。 Qt 提供了 `QSerialPort` 类用于串口通信[^1],`QTcpSocket` 和 `QUdpSocket` 用于网络通信[^1],`QTimer` 可用于设定定时采集任务[^1]。 ### 3.2 使用 QSerialPort 实现串口数据采集 对于串口设备,可以使用 `QSerialPort` 类来打开串口、设置波特率、数据位、停止位等参数,并通过信号槽机制实时读取数据。 示例代码如下: ```cpp #include <QSerialPort> #include <QSerialPortInfo> #include <QDebug> QSerialPort *serial; void initSerialPort() { serial = new QSerialPort(); serial->setPortName("COM1"); // 设置串口号 serial->setBaudRate(QSerialPort::Baud9600); // 设置波特率 serial->setDataBits(QSerialPort::Data8); // 数据位 serial->setParity(QSerialPort::NoParity); // 校验位 serial->setStopBits(QSerialPort::OneStop); // 停止位 serial->setFlowControl(QSerialPort::NoFlowControl); // 流控制 if (serial->open(QIODevice::ReadOnly)) { qDebug() << "串口打开成功"; connect(serial, &QSerialPort::readyRead, readData); } else { qDebug() << "无法打开串口"; } } void readData() { QByteArray data = serial->readAll(); qDebug() << "接收到数据:" << data; } ``` ### 3.3 使用 QTcpSocket 实现网络数据采集 如果数据来自远程服务器或设备,可以通过 `QTcpSocket` 或 `QUdpSocket` 进行网络通信。 示例代码如下: ```cpp #include <QTcpSocket> #include <QDebug> QTcpSocket *tcpSocket; void connectToServer() { tcpSocket = new QTcpSocket(); tcpSocket->connectToHost("192.168.1.100", 8080); // IP和端口 if (tcpSocket->waitForConnected(3000)) { qDebug() << "连接成功"; connect(tcpSocket, &QTcpSocket::readyRead, readNetworkData); } else { qDebug() << "连接失败"; } } void readNetworkData() { QByteArray data = tcpSocket->readAll(); qDebug() << "接收到网络数据:" << data; } ``` ### 3.4 数据处理与用户界面更新 采集到的数据通常需要进行解析、格式转换、过滤或可视化处理。Qt 提供了丰富的类支持数据处理,例如 `QByteArray`、`QString`、`QJsonDocument` 等。 同时,可以使用 `QLabel`、`QLineEdit`、`QTableWidget` 或 `QChart`(Qt Charts 模块)来展示采集到的数据。 ### 3.5 定时任务与多线程支持 为了避免阻塞主线程影响 UI 响应,建议将数据采集任务放在子线程中执行。Qt 提供了 `QThread` 和 `QtConcurrent` 支持多线程编程。 此外,使用 `QTimer` 可以实现定时轮询采集任务: ```cpp QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &DataCollector::采集数据); timer->start(1000); // 每秒采集一次 ``` ### 3.6 数据存储 采集到的数据可以保存到本地文件或数据库中: - **文件存储**:使用 `QFile` 和 `QTextStream` 进行文本或二进制文件写入。 - **数据库存储**:结合 Qt SQL 模块操作 SQLite、MySQL 等数据库。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值