ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Qt — Qt 网络

Qt — Qt 网络 目录1. 网络概述2. UDP Socket2.1 核心 API 概览2.2 写一个带有界面的 Udp 回显服务器3. TCP Socket3.1 核心 API 概览3.2 TCP 回显服务器4. HTTP Client4.1 核心 API4.2 客户端代码编写1. 网络概述进行网络编程的时候本质上是在编写 应用层代码需要传输层提供支持。传输层最核心的协议有UDP 和 TCP 并且这两协议差别还很大Qt 也就提供了两套 API。使用 Qt 网络编程的 API 需要先在 .pro 文件中添加 network 模块之前学过的 Qt 控件各种内容都是包含在 QtCore 模块中的默认就添加的。模块化处理其它的功能分别封装成不同的模块。默认情况下这些额外的模块不会参与编译需要在 .pro 文件中引入对应的模块才能把对应功能给编译加载进来。Qt 其实提供了 静态库和动态库 的版本2. UDP Socket2.1 核心 API 概览主要的类有两个. QUdpSocket 和 QNetworkDatagramQUdpSocket 表示⼀个 UDP 的 socket 文件名称类型说明对标原⽣ APIbind(const QHostAddress, quint16)方法绑定指定的端口号bindreceiveDatagram()方法返回 QNetworkDatagram . 读取⼀个 UDP 数据报.recvfrom阻塞IOwriteDatagram(const QNetworkDatagram)方法发送⼀个 UDP 数据报.sendto阻塞IOreadyRead信号在收到数据并准备就绪后触发.无 (类似于 IO 多路复⽤的通知机制)readyRead当 socket 收到请求的时候QUdpSocket 就会触发这个信号此时就可以在槽函数里完成读取请求的操作。基于信号槽就天然达成了 “事件驱动” 这样的一种网络编程的方式QNetworkDatagram 表示⼀个 UDP 数据报.名称类型说明对标原⽣ APIQNetworkDatagram(constQByteArray, constQHostAddress , quint16 )构造函数通过 QByteArray , ⽬标 IP 地址,⽬标端⼝号 构造⼀个 UDP 数据报.通常⽤于发送数据时.无data()方法获取数据报内部持有的数据. 返回QByteArray无senderAddress()方法获取数据报中包含的对端的 IP 地址.无,recvfrom 包含了该功能.senderPort()方法获取数据报中包含的对端的端⼝号.无, recvfrom 包含了该功能.2.2 写一个带有界面的 Udp 回显服务器服务端// Widget.cpp #include widget.h #include ui_widget.h #include QMessageBox #include QNetworkDatagram Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-setupUi(this); // 创建出这个对象 socket new QUdpSocket(this); // 设置窗口标题 this-setWindowTitle(服务器); // 连接信号槽 connect(socket, QUdpSocket::readyRead, this, Widget::processRequest); // 先连接信号槽再绑定端口号一旦绑定端口意味着请求就可以被收到了 // 如果在完成绑定之后在连接信号槽之前有客户端把请求发过来了此时就可能读不到这样的请求了请求就没了 // 绑定端口号,一个端口号只能被一个 socket 绑定有可能会绑定失败 bool ret socket-bind(QHostAddress::Any, 8888); if(!ret){ // 绑定失败socket-errorString() 本质是对系统的 errno 机制进行封装相当于 Linux 中用过的 perror QMessageBox::critical(this,服务器启动出错,socket-errorString()); return; } } Widget::~Widget() { delete ui; } // 这个函数完成的逻辑就是服务器的最核心的逻辑了 void Widget::processRequest() { // 1. 读取请求并解析 const QNetworkDatagram requestDatagram socket-receiveDatagram(); QString request requestDatagram.data(); //data()返回的就是一个 QByteArrayQByteArray 是可以赋值给 QString 的 // 2. 根据请求计算响应由于是回显服务器请求是啥响应就是啥所以响应不需要计算就是请求本身 const QString response process(request); // 3. 把响应写回给客户端response.toUtf8()取出 QString 内部的字节数 // 客户端是谁--- 就包含在 requestDatagram 中了 QNetworkDatagram responseDategram(response.toUtf8(),requestDatagram.senderAddress(),requestDatagram.senderPort()); socket-writeDatagram(responseDategram); // 把这次交互的信息显示到界面上 QString log [ requestDatagram.senderAddress().toString() : QString::number(requestDatagram.senderPort()) ] req: request , resp: response; ui-listWidget-addItem(log); } QString Widget::process(const QString request) { // 由于当前是回显服务器响应和请求完全一样 // 对于一个成熟的商业服务器这里的请求到相应的计算过程可能是非常复杂的业务逻辑 return request; }客户端Qt Creator 中是可以同时打开多个项目的如果这两项目中存在同名的文件就非常容易混淆同样的需要加上 network此时写的客户端要能够主动给服务器发起请求大概的布局// widget.cpp #include widget.h #include ui_widget.h #include QNetworkDatagram // 定义两个常量描述服务器的 地址 和 端口 // 端口号本质是一个 2 字节的 无符号 整数quint16 本质上就是一个 unsigned short // 虽然short 通常是2个字节的但是 C 标准中没有明确规定这一点只是说 short 不应该少于2个字节 const QString SERVER_IP 127.0.0.1; const quint16 SERVER_PORT 8888; Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-setupUi(this); socket new QUdpSocket(this); // 修改窗口标题方便区分这是一个客户端程序 this-setWindowTitle(客户端); // 通过信号槽来处理服务器返回的数据 connect(socket, QUdpSocket::readyRead, this, Widget::processResponse); } Widget::~Widget() { delete ui; } void Widget::on_pushButton_clicked() { // 1. 获取到输入框中的内容 const QString text ui-lineEdit-text(); // 2. 构造 UDP 的请求数据 QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress(SERVER_IP),SERVER_PORT); // 3. 发送请求数据 socket-writeDatagram(requestDatagram); // 4. 把发送的请求也添加到列表框中 ui-listWidget-addItem(客户端说: text); // 5. 把输入框的内容也清空一下 ui-lineEdit-setText(); } void Widget::processResponse() { // 通过这个函数来处理收到的响应 // 1. 读取到响应数据 const QNetworkDatagram responseDatagram socket-receiveDatagram(); QString response responseDatagram.data(); // 2. 把响应数据显示到界面上 ui-listWidget-addItem(服务器说: response); }客户端服务器程序测试时候的基本的原则一定是先启动服务器后启动客户端。运行同时启动多个客户端双击进入下面的目录进入Debug目录多次点击exe程序即可启动多个客户端问题1能否把现在的UDP服务器放到云服务器上呢大概率是不行的取决于服务器是否安装了图像化界面Qt 程序需要依赖图形化界面来运行的问题2能否拿现在的 Udp 客户端连之前的 Linux 阶段写的 Udp 服务器呢这个是完全可以的这也是 网络编程/协议 的意义一般商业公司的项目都是通过其它方式编写的服务器程序大概率不会是 Qt但是使用 Qt 编写客户端。3. TCP Socket3.1 核心 API 概览核心类是两个: QTcpServer 和 QTcpSocketQTcpServer ⽤于监听端⼝, 和获取客⼾端连接名称类型说明对标原⽣ APIlisten(const QHostAddress,quint16 port)方法绑定指定的地址和端⼝号, 并开始监听.bind 和 listennextPendingConnection()方法从系统中获取到⼀个已经建⽴好的tcp 连接.返回⼀个 QTcpSocket , 表示这个客户端的连接.通过这个 socket 对象完成和客户端之间的通信.acceptnewConnection信号有新的客户端建⽴连接好之后触发.无(但是类似于 IO 多路复⽤中的通知机制)QTcpSocket 用户客户端和服务器之间的数据交互.名称类型说明对标原生 APIreadAll()方法读取当前接收缓冲区中的所有数据.返回 QByteArray 对象.readwrite(const QByteArray )方法把数据写入 socket 中.writedeleteLater方法暂时把 socket 对象标记为⽆效. Qt会在下个事件循环中析构释放该对象无(但是类似于 半⾃动化的垃圾回收)readyRead信号有数据到达并准备就绪时触发.无(但是类似于 IO 多路复⽤中的通知机制)disconnected信号连接断开时触发.无(但是类似于 IO 多路复⽤中的通知机制)3.2 TCP 回显服务器服务器客户端服务端// widget.cpp #include widget.h #include ui_widget.h #include QMessageBox #include QTcpSocket Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-setupUi(this); // 1. 修改窗口标题 this-setWindowTitle(服务器); // 2. 创建 QTcpServer 的实例 tcpServer new QTcpServer(this); // 3. 通过信号槽指定如何处理连接 connect(tcpServer, QTcpServer::newConnection,this, Widget::processConnection); // 4. 绑定并监听端口号一定要确保准备工作充分了在真正开张营业 // 绑定并监听端口号这个操作的是在初始化的最后一步都是需要把如何处理链接如何处理请求……都准备好之后才能真正绑定端口并监听 bool ret tcpServer-listen(QHostAddress::Any, 8888); if(!ret){ QMessageBox::critical(this, 服务器启动失败!,tcpServer-errorString()); exit(1); } } Widget::~Widget() { delete ui; } void Widget::processConnection() { // 1. 通过 tcpServer 拿到一个 socket 对象通过这个对象来和客户端进行通信 QTcpSocket* clientSocket tcpServer-nextPendingConnection(); QString log [ clientSocket-peerAddress().toString() : QString::number(clientSocket-peerPort()) ] 客户端上线; ui-listWidget-addItem(log); // 2. 通过信号槽来处理客户端发来请求的情况 connect(clientSocket,QTcpSocket::readyRead, this, [](){ // a) 读取出请求数据.readAll 返回的是 QByteArray通过赋值转成 QString QString request clientSocket-readAll(); // b) 根据请求处理响应 const QString response process(request); // c) 把相应写回到客户端 clientSocket-write(response.toUtf8()); // d) 把上述信息记录到日志中 QString log [ clientSocket-peerAddress().toString() : QString::number(clientSocket-peerPort()) ] req: request , resp: response; ui-listWidget-addItem(log); // 上述代码其实不够严谨作为回显服务器是已经够了的 // 实际使用 TCP 的过程中TCP 是面向字节流的一个完整的请求可能会分成多段字节数组进行传输 // 虽然 TCP 已经帮我们处理了很多几首的问题了但是 TCP 本身不负责区分从哪里到哪里是一个完整的应用层数据报粘包问题 // 更严谨的做法是每次收到的数据都是给放到一个大的字节数组缓冲区中并且提前约定好应用层协议的格式分割符长度其他办法 // 再按照协议格式对缓冲区数据进行更细致的解析处理 }); // 3. 通过信号槽来处理客户端断开连接的情况 connect(clientSocket, QTcpSocket::disconnected,this,[](){ // a) 把断开连接的信息通过日志显示出来 QString log [ clientSocket-peerAddress().toString() : QString::number(clientSocket-peerPort()) ] 客户端下线; ui-listWidget-addItem(log); // b) 手动释放 clientSocket每个客户端都有一个这样的对象存在N个随着服务器的运行客户端越来越多如果不释放此时累计的 clientSocket 也会越来越多 // 内存泄漏 文件描述符泄露 // 直接使用 delete 是下策使用 deleteLator 是更加合适的 // delete clientSocket; clientSocket-deleteLater(); // 这个操作不是立即销毁 clientSocket而是告诉 Qt 下一轮事件循环中再进行上述的销毁操作 // 槽函数都是在事件循环中执行的进入到下一轮事件循环意味着上一轮时间循环肯定结束了也意味着当前的槽函数肯定是执行结束了 }); } // 此处写的回显服务器请求和响应是一致的 QString Widget::process(const QString request) { return request; }客户端//widget.cpp #include widget.h #include ui_widget.h #include QMessageBox Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-setupUi(this); // 1. 设置窗口标题 this-setWindowTitle(客户端); // 2. 创建 socket 对象的实例 socket new QTcpSocket(this); // 3. 和服务器建立连接 socket-connectToHost(127.0.0.1,8888); // 调用这个函数此时系统内核就会和对方的服务器之间进行三次握手了 // 三次握手也是需要消耗一定的时间的 // 此处的这个函数不会阻塞等待三次握手完毕属于非阻塞的函数 // 原生的 Linux api也有一个connect函数一般来说一个socket某人都是阻塞IO通信的 // 此时针对这样的 socket 进行 connect 也就是阻塞的了 // 4. 连接信号槽处理响应 connect(socket,QTcpSocket::readyRead,this,[](){ // a) 读取出响应内容 QString response socket-readAll(); // b) 把响应内容显示到界面上 ui-listWidget-addItem(服务器说: response); }); // 5. 等待连接建立的结果确认是否连接成功 bool ret socket-waitForConnected(); if(!ret){ QMessageBox::critical(this,连接服务器出错,socket-errorString()); exit(1); } } Widget::~Widget() { delete ui; } void Widget::on_pushButton_clicked() { // 1. 获取到输入框中的内容 const QString text ui-lineEdit-text(); // 2. 发送数据给服务器 socket-write(text.toUtf8()); // 3. 把发的消息显示到界面上 ui-listWidget-addItem(客户端说: text); // 4. 清空输入框的内容 ui-lineEdit-setText(); }启动多个客户端之前在Linux 时写的TCP 的回显服务器的时候遇到了一个问题多个客户端同时访问的时候就只有一个生效后来引入了多线程每个客户端安排一个单独的线程问题才得到改善。之前存在这个问题的本质原因是写双重循环里层循环没有及时结束导致外层循环不能快速的第二次调用到accept导致第二个客户端无法进行处理了。引入多线程本质上就是吧双重循环化简成两个独立的循环。Qt 的服务器程序中其实一个循环都没写是通过 Qt 内置的 信号槽 来驱动的。信号槽机制很好的化简了我们的程序。一个正经的 UDP/TCP 服务器不太可能会用 Qt 来写服务器一般都是不需要图形化界面的。4. HTTP Client进⾏ Qt 开发时, 和服务器之间的通信很多时候也会⽤到 HTTP 协议.通过 HTTP 从服务器获取数据.通过 HTTP 向服务器提交数据.Qt 中也提供了HTTP的客户端HTTP协议本质上就是基于 TCP 协议实现的实现一个 HTTP客户端/服务器本质也就是基于 TCP socket 进行封装Qt 只是提供了 HTTP 客户端而没有提供 HTTP 服务器的库。4.1 核心 API关键类主要是三个. QNetworkAccessManager , QNetworkRequest , QNetworkReply .QNetworkAccessManager 提供了 HTTP 的核心操作方法说明get(const QNetworkRequest )发起⼀个 HTTP GET 请求. 返回 QNetworkReply 对象post(const QNetworkRequest , const QByteArray )发起⼀个 HTTP POST 请求. 返回 QNetworkReply 对象.QNetworkRequest 表示⼀个 HTTP 请求(不含 body).如果需要发送⼀个带有 body 的请求(比如post), 会在QNetworkAccessManager 的 post ⽅法中通过单独的参数来传⼊body方法说明QNetworkRequest(const QUrl )通过 URL 构造⼀个 HTTP 请求.setHeader(QNetworkRequest::KnownHeadersheader,const QVariant value)【QVariant 表示一个 “类型可变”的值类似于C语言中的 void* 】设置请求头.其中的 QNetworkRequest::KnownHeaders 是⼀个枚举类型, 常⽤取值取值说明ContentTypeHeader描述 body 的类型.ContentLengthHeader描述 body 的⻓度LocationHeader⽤于重定向报⽂中指定重定向地址. (响应中使⽤, 请求⽤不到)CookieHeader设置 cookieUserAgentHeader设置 User-AgentQNetworkReply 表示⼀个 HTTP 响应. 这个类同时也是 QIODevice 的⼦类.方法说明error()获取出错状态.errorString()获取出错原因的⽂本.readAll()读取响应 body.header(QNetworkRequest::KnownHeadersheader)读取响应指定 header 的值.此外, QNetworkReply 还有⼀个重要的信号 finished 会在客户端收到完整的响应数据之后触发.4.2 客户端代码编写此处显示的响应结果大概率是一个 HTMLQPlainTextEdit 来进行表示能够看到响应的原始模样。QTextEdit 天然支持对 HTML 的解析的会对 HTML 进行解析渲染最终显示效果就不是原始的HTMLQTextEdit 背后还做了很多工作当得到的 HTML 比较大的时候是会造成卡顿的运行实际开发中HTTPClient 获取到的数据也不一定非得是 HTML更大的可能性是客户端开发和服务器开发约定好的交互数据格式按照约定的格式客户端拿到之后进行解析并显示到界面上。
返回列表