文章目录
- 核心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_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
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);
//手动释放clientSocket
clientSocket->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里面的信号槽机制,就无需写这些循环,比较方便。