Qt网络编程——QTcpServer和QTcpSocket
文章目录
- 核心API
- TCP回显服务器
- TCP回显客户端
核心API
QTcpServer
用于监听端口和获取客户端连接
名称 | 类型 | 说明 | 对标原生API |
---|---|---|---|
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听 | bind和listen |
nextPendingConnection() | 方法 | 从系统中获取到一个建立好的tcp连接 返回一个 QTcpSocket ,表示这个客户端的连接通过这个socket对象完成和客户端之间的通信 | accept |
newCondition() | 信号 | 有新的客户端建立好连接之后触发 | 无(类似IO多路复用的通知机制) |
QTcpSocket
用于客户端和服务器之前的数据交互
tcp读取的数据是字节流,因此读取和返回的都是字节数组,这和udp的
QNetworkDatagram
数据报不一样
事件循环,可以简单理解为是Qt程序内部一个带有“生物钟”这样的东西,周期性执行一些逻辑
名称 | 类型 | 说明 | 对标原生API |
---|---|---|---|
readAll() | 方法 | 读取当前接收缓冲区的所有数据 返回QByteArray对象 | read |
write(const QByteArray&) | 方法 | 将数据写入socket当中 | write |
deleteLater | 方法 | 暂时把socket对象标记为无效 Qt会在下一个事件循环中构造释放该对象 | 无 |
readyRead | 信号 | 有数据到达并准备就绪时触发 | 无 |
disconnected | 信号 | 连接断开时触发 | 无 |
QByteArray
是字节数组,可以和QString
互相转换
QString
的构造函数可以把QByteArray
转换成QString
QString
的toUtf8
函数可以把QString
转成QByteArray
TCP回显服务器
用这些接口,写一个回显服务器。
创建项目之后,如果要进行网络编程,第一步就是在
.pro
文件中加入network
模块
界面:
widget.h
:
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void processConnection();QString process(const QString request);
private:Ui::Widget *ui;QTcpServer *tcpServer;
};
#endif // WIDGET_H
widget.cpp
:
-
绑定和监听端口号一定是要等准备工作做完之后再进行,比如说如何处理连接、如何处理请求等…
-
需要手动释放
clientSocket
,因为它是每个客户端都有这样一个对象,而QTcpServer
和QUdpServer
都是只有一份。如果不对断开连接的客户端进行释放,累计的客户端会越来越多,这会导致两个问题:文件描述符泄漏和内存泄漏。也不能直接
delete clientSocket
,因为当前槽函数主要是围绕clientSocket
来进行操作的,一旦delete
,其他逻辑就无法使用clientSocket
,使用要保证delete
操作是最后一步,而且不会被return或者抛出异常给跳过。Qt提供了
deleteLater
,不是立即销毁,而是告诉Qt,下一轮事件循环中,再进行上述销毁操作。槽函数是在事件循环中进行的,进入下一轮事件循环表明上一轮事件肯定结束了。
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
#include<QTcpSocket>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//修改窗口标题this->setWindowTitle("tcp服务器");//创建QTcpServer实例tcpServer = new QTcpServer(this);//连接信号槽(如何处理连接)connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);//绑定并监听端口号if(!tcpServer->listen(QHostAddress::Any, 8080)){QMessageBox::critical(this, "服务器启动失败", tcpServer->errorString());return;}
}Widget::~Widget()
{delete ui;
}void Widget::processConnection()
{//拿到socket对象QTcpSocket *clientSocket = tcpServer->nextPendingConnection();//peerAddress表示对端的ip地址QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] online";//显示到界面ui->listWidget->addItem(log);//通过信号槽处理客户端发来的请求connect(clientSocket, &QTcpSocket::readyRead, this, [=](){//读取请求QString request = clientSocket->readAll();//处理请求const QString &response = process(request);//返回响应clientSocket->write(response.toUtf8());//记录日志QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "]"+ "req: " + request + ", resp: " + response;ui->listWidget->addItem(log);});//客户端断开连接//disconnected表示已经断开,是一个信号connect(clientSocket, &QTcpSocket::disconnected, this, [=](){QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] offline";ui->listWidget->addItem(log);//手动释放clientSocketclientSocket->deleteLater();});
}QString Widget::process(const QString request)
{return request;
}
此段代码,从某种意义上来说,不够严谨,因为tcp是面向字节流的,可能会分为多段。
更好的做法是,将收到的数据,放到一个较大字节的缓冲区当中,然后按照约定好的协议,进行数据解析提取。
TCP回显客户端
ui界面:
对于客户端,使用的就是
QTcpSocket
,QTcpServer
只是在服务端使用的
- 调用
connectToHost
函数,此时系统就开始和对方服务器三次握手,三次握手也是需要时间的,而这个函数并不会阻塞等待握手完毕,是一个非阻塞的函数。
所以需要搭配waitForConnected
,等待连接建立成功
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//设置窗口标题this->setWindowTitle("客户端");//创建socket对象实例socket = new QTcpSocket(this);//和服务器建立连接socket->connectToHost("127.0.0.1", 8080);//连接信号槽connect(socket, &QTcpSocket::readyRead, this, [=](){//读取响应内容QString response = socket->readAll();//响应内容显示到界面ui->listWidget->addItem("server say# " + response);});//等待连接建立结果if(!socket->waitForConnected()){QMessageBox::critical(this, "连接服务器失败", socket->errorString());return;}}Widget::~Widget()
{delete ui;
}void Widget::on_pushButton_clicked()
{//获取输入框内容const QString &text = ui->lineEdit->text();//发送到服务器socket->write(text.toUtf8());//将发送的消息显示到界面ui->listWidget->addItem("client say#" + text);//清空输入框内容ui->lineEdit->setText("");
}
Linux当中写Tcp服务器的时候,如果多个客户端同时访问,就只会生效一个,然后引入线程,每个客户端一个线程;
而这里并没有出现这类情况,这是因为之前是写的两层循环,里面的循环没有结束,导致外层循环不能快速调用到
accpet
,导致第二个客户端无法进行处理。引入多线程,本质上就是将双重循环,化简成两个独立的循环。
Qt里面的信号槽机制,就无需写这些循环,比较方便。