Qt 给用户提供了网络编程的接口,包括TCP、UDP、HTTP三种协议的API以及各种类,可以了解一下。
而在 QT 中想要使用网络编程,必须在pro文件中添加 network 模块,否则无法包含网络编程所需的头文件。
UDP
UDP是传输层的协议,该协议是无输出,不可靠,面向数据包,全双工的通信协议。
QT为 UDP 协议专门设置了两个类:QUdpSocket 以及 QNetworkDatagram 。
QUdpSocket类
该类是专门用于 Udp 的类,包含 Udp 协议所需的绑定、接收数据包、发送数据包等API。
bind(const QHostAddress&,quint16) | 函数 | 绑定端口号 | 类似linux中的bind |
receiveDatagram() | 函数 | 返回一个 QNetworkDatagram类型的数据包 | 类似linux中的recvfrom |
writeDatagram() | 函数 | 发送一个QNetworkDatagram类型的数据包 | 类型linux中的sendto |
readyRead | 信号 | 当一个端口号可以读取时即可触发该信号 | 类似linux中的多路转接:select、poll和epoll等 |
QNetworkDatagram
该类表明一个数据包,可以从该类所实例化的对象中读取所需的数据、发送端的端口号和 ip 地址
QNetworkDatagram(const QByteArray&,const QHostAddress&,quint16) | 方法 | QNetworkDatagram的构造函数,用于创建一个对象 |
data() | 方法 | 获取数据包中的数据,返回一个 QByteArray 对象 |
sederAddress() | 方法 | 获取数据包发送者的 IP 地址 |
sederPort() | 方法 | 获取数据包发送者的 端口号 |
和Linux不同的是,linux的接收和发送函数:recvfrom 和 sendto ,它们都需要一个 sockaddr 的结构体对象来获取发送端的 IP 地址和端口号,而QT则是将其拆开,数据包中直接包含对端的IP地址和端口号
UDP回显服务器
首先写服务器的代码,在 pro 文件添加 network 模块后,即可使用上述API。
在服务器一侧我们使用 listWidget 控件,用来显示所有接收的数据。
然后在头文件中添加 QUdpSocket 对象,用来后续接收客户端的消息。
然后在构造函数中我们初始化 socket 后,需要先进行信号槽绑定然后再进行端口号绑定。
必须先进行信号槽绑定再进行端口号绑定,这是为了防止如果有客户端发起请求了,但是信号槽函数未绑定,导致该请求丢失的情况。
然后再写好槽函数,写好接收和显示逻辑。
UDP回显客户端
同样的,在pro文件中添加network模块后即可使用UDP的API。
客户端需要发送数据和接收数据,因此除了 listWidget 来显示数据之外,还需要 lineEidt 和 pushbutton 两个控件,分别用来输入数据和发送数据。
在头文件中添加控件的槽函数,其他和Server一样。
而在构造函数中,除了初始化 socket 之外,还需要绑定 readyRead 信号和process槽函数,因为客户端也需要接收服务器发送的数据。
而pushButton 的槽函数就需要先从 lineEdit 中读取文件,然后再创建一个 QNetworkDatagram 对象,用来发送数据,该数据包必须包含服务器的 IP 地址和 端口号。
而接收服务器发送的数据就直接显示在 listWidget 上。
可以看到成功的发送和接收到数据了。
TCP
TCP是一个可靠、面向连接、面向字节流、全双工的传输层协议,它的API稍微比UDP复杂,但是也很简单。
其核心类有两个:QTcpServer 和 QTcpSocket。
QTcpServer
该类专门用于监听端口,和获取客户端连接。
listen(const QHostAddress&,quint16) | 方法 | 绑定指定的端口号和IP地址,并且启动监听 | 类似linx中的bind和listen的结合体 |
nextPendingConnection() | 方法 | 从系统中获取一个已经建立好的tcp连接,返回一个 QTcpSocket 对象。 通过该对象可以进行通信 | 类似linux 中的 accept |
newConnection | 信号 | 有新的客户端建立好连接后即触发 | 在linux中类似多路转接的通知机制 |
QTcpSocket
和 QUdpSocket 类似,该类用于进程间通信。
readAll() | 方法 | 读取接收缓冲区的所有数据 返回一个 QByteArray 对象 | 类似linux中的read |
write(const QByteArray & ) | 方法 | 把数据写入 socket 对象中 | 类似linux的write |
deleteLater | 方法 | 将该 socket 对象设为无效,在下一次事件循环中析构该对象 | 类似 java 中的自动回收机制,不过我们这里是半自动的 |
connectToHost(const QHostAddress&,quint16) | 方法 | 向服务器发送连接请求,进行三次握手操作 | 类似 linux 中的 connect 函数 |
readyRead | 信号 | 有数据到达并准备时触发 | 类似多路复用 |
disconnected | 信号 | 当断开连接时触发 | 类似多路复用 |
通过上面两个类可以实现 TCP 的回显服务器。
TCP回显服务器
和 UDP 的服务器一样,直接用一个 listWidget 用来显示所有数据。
在头文件上设置好 QTcpServer 对象和槽函数。
将 QTcpServer 的 newConnection 信号和对应的槽函数绑定,然后启动监听。
当一个客户端发起链接时,分未三个部分。
- 获取客户端的连接,并显示有客户端上线
- 绑定 readyRead 和 对应的槽函数(此处用的 lambda 表达式)
- 绑定 disconnected 和 对应的槽函数
无论是客户端上线、还是接收到数据、又或者是断开连接,都会在 listWidget 上显示出来。
而 process 函数则是单纯的返回一个相同的QString罢了。
TCP回显客户端
和 UDP 回显客户端一样,采用 listWidget、lineEdit、pushButton 三个控件。
头文件中添加 QTcpSocket 类和 pushButton 对应的槽函数。
而在构造函数中,除了初始化 socket 之外,还需要将 socket 的 readyRead 信号和槽函数绑定在一起,用来显示在 listWidget 中。此外还需要通过 connectToHost 来和服务器建立连接 。
此外由于客户端还需要接收服务器发送的数据,因此也需要绑定函数。
而pushButton的槽函数则直接读取 lineEdit 上的文本,并且发送和服务器。
能够发现确实进行了互相通信。
不过让我们回忆下 linux 中我们是如何使用 TCP 进行通信的?
当一个客户端进行请求时,就为该客户端创建一个线程,在线程中进行管理,但是我们这里的QT明显不是这样的,这是为什么呢?实际上那是因为 代码缺陷, linux中采用的是双层循环方式,当一个客户端建立连接,那么直到客户端断开连接之前,服务器一直都会在内层循环中阻塞,无法到达 accept 中去,只能通过多线程的方式进行。
而 QT 自带信号槽机制就很好的解决了这个问题,当有信号发生时才去处理,而 linux 除非采用 epoll 来将处理之外,基本只能通过多线程解决。
HTTP Client
实际上一个服务器不太可能采用图形化界面的,因此实际上 TCP 和 UDP 都使用的很少。
QT 中使用最多的是 HTTP。
QT 提供三个类用于和 HTTP 进行请求和获取操作。
- QNetworkAccessManager
- QNetworkRequest
- QNetworkReply
QNetworkAccessManager
该类提供 HTTP 的核心操作。
方法 | 说明 |
get(const QNetworkRequest&) | 以get 的方式发送一个请求 |
post(const QNetworkRequest&,const QByteArray&) | 以 post 的方式发送一个请求 |
当然,HTTP 有很多操作,比如delete、head之类的,不过这里就不一一赘述了。
QNetworkRequest
该类表示一个 Http 请求,不包含body
如果使用 post 发送一个带有 body 的请求,则需要另外单独的参数来传入 body。
方法 | 说明 |
QNetworkRequest(const QUrl&) | 通过 URL 构造一个 HTTP 请求 |
setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value) | 设置请求头 |
其中,这个 setHeader 的 KnownHeader 是一个枚举类型,包括以下类型。
值 | 说明 |
ContentTypeHeader | 描述body 类型 |
ContentLengthHeader | 描述 body 长度 |
LocationHeader | 用于重定向报文中指定重定向地址(响应才会用到) |
CookieHeader | 设置 cookie |
UserAgentHeader | 设置 User-Agent |
选择了某一个枚举类型后, value 就必须是枚举类型对应的说明,比如如果选择的ContentTypeHeader,那么 value 就需要在 http Body 的四种格式:form-data
,x-www-from-urlencoded
,raw
,binary 中选一个了。
QNetworkReply
这是 Http 的响应,它是 QIODevice 的子类
名称 | 类型 | 说明 |
error() | 方法 | 获取出错状态 |
errorString() | 方法 | 获取出错原因的文本 |
readAll() | 方法 | 读取响应文本 |
header(QNetworkRequest::KnownHeaders header) | 方法 | 读取响应指定header 的值 |
finished | 信号 | 客户端收到完整的响应数据后触发 |
接着我们来试着使用这些API 来获取一个响应。
首先采用 plainTextEdit、lineEdit、pushButton 三个控件。
头文件中新增 QNetworkAccessManager 对象。
然后在构造函数中初始化 manager。
而pushButton的槽函数中,我们需要按顺序来发送请求。
首先是通过获取在 lineEdit 中的 文本,并且构建一个 url。
然后通过 url 来创建一个请求,最后通过 get 方法来发送请求,并且获取一个响应。
如果该响应已经接收完所有数据后,就进入响应绑定的槽函数中,将响应显示在 plainTextEdit 上
可以看到确实是获取到了响应。