1. 开发流程

1.1 Qtcp服务器的关键步骤
• 工程建立,需要在.pro****加入网络权限

• 创建一个基于 QTcpServer 的服务端涉及以下关键步骤:
- 创建并初始化QTcpServer 实例:
• 实例化 QTcpServer 。
• 调用 listen 方法在特定端口监听传入的连接。 - 处理新连接:
• 为 newConnection 信号连接一个槽函数。
• 在槽函数中,使用 nextPendingConnection 获取 QTcpSocket 以与客户端通信。 - 读取和发送数据:
• 通过连接 QTcpSocket 的 readyRead 信号来读取来自客户端的数据。
• 使用 write 方法发送数据回客户端。 - 关闭连接:
• 在适当的时候关闭 QTcpSocket 。
1.2 QTtcp客户端的关键开发步骤
• 工程建立,需要在.pro加入网络权限

• 创建一个基于 QTcpSocket 的Qt客户端涉及以下步骤:
- 创建QTcpSocket 实例:
• 实例化 QTcpSocket 。 - 连接到服务器:
• 使用 connectToHost 方法连接到服务器的IP地址和端口。 - 发送数据到服务器:
• 使用 write 方法发送数据。 - 接收来自服务器的数据:
• 为 readyRead 信号连接一个槽函数来接收数据。 - 关闭连接:
• 关闭 QTcpSocket 连接。
2. TCP协议
• TCP(传输控制协议)是一种广泛使用的网络通信协议,设计用于在网络中的计算机之间可靠地传输数据。它是互联网协议套件的核心部分,通常与IP(互联网协议)一起使用,合称为TCP/IP。以下是TCP协议的一些基本特点:
- **面向连接:**在数据传输之前,TCP 需要在发送方和接收方之间建立一个连接。这包括三次握手过
程,确保两端都准备好进行数据传输。 - **可靠传输:**TCP 提供可靠的数据传输服务,这意味着它保证数据包准确无误地到达目的地。如果发生数据丢失或错误,TCP 会重新发送数据包。
- **顺序控制:**TCP 保证数据包的传输顺序。即使数据包在网络中的传输顺序被打乱,接收方也能按照正确的顺序重组这些数据。
- **流量控制:**TCP 使用窗口机制来控制发送方的数据传输速率,以防止网络过载。这有助于防止接收方被发送方发送的数据所淹没。
- **拥塞控制:**TCP 还包括拥塞控制机制,用来检测并防止网络拥塞。当网络拥塞发生时,TCP 会减少其数据传输速率。
- **数据分段:**大块的数据在发送前会被分割成更小的段,以便于传输。这些段会被独立发送并在接收端重新组装。
- **确认和重传:**接收方对成功接收的数据包发送确认(ACK)信号。如果发送方没有收到确认,它会重传丢失的数据包。
- 终止连接:数据传输完成后,TCP 连接需要被正常关闭,这通常涉及到四次挥手过程。
• TCP 适用于需要高可靠性的应用,如网页浏览、文件传输、电子邮件等。然而,由于它的这些特性,TCP在处理速度上可能不如其他协议(如UDP)那么快速。
• TCP协议中的三次握手和四次挥手是建立和终止连接的重要过程。下面是它们的简要描述:
1.2 三次握手(建立连接)
• 三次握手的主要目的是在两台设备之间建立一个可靠的连接。它包括以下步骤:
- SYN:客户端向服务器发送一个SYN(同步序列编号)报文来开始一个新的连接。此时,客户端进入SYN-SENT状态。
- SYN-ACK:服务器接收到SYN报文后,回复一个SYN-ACK(同步和确认)报文。此时服务器进入SYN-RECEIVED状态。
- ACK:客户端接收到SYN-ACK后,发送一个ACK(确认)报文作为回应,并进入ESTABLISHED(已建立)状态。服务器在收到这个ACK报文后,也进入ESTABLISHED状态。这标志着连接已经建立。
• 如图:

1.3 四次挥手(断开连接)
• 四次挥手的目的是终止已经建立的连接。这个过程包括以下步骤:
- FIN:当通信的一方完成数据发送任务后,它会发送一个FIN(结束)报文来关闭连接。发送完FIN报文后,该方进入FIN-WAIT-1状态。
- ACK:另一方接收到FIN报文后,发送一个ACK报文作为回应,并进入CLOSE-WAIT状态。发送FIN报文的一方在收到ACK后,进入FIN-WAIT-2状态。
- FIN:在等待一段时间并完成所有数据的发送后,CLOSE-WAIT状态的一方也发送一个FIN报文来请求关闭连接。
- ACK:最初发送FIN报文的一方在收到这个FIN报文后,发送一个ACK报文作为最后的确认,并进入TIME-WAIT状态。经过一段时间后,确保对方接收到了最后的ACK报文,该方最终关闭连接。
• 如图:

• 在这两个过程中,三次握手主要确保双方都准备好进行通信,而四次挥手则确保双方都已经完成通信并同意关闭连接。
3. Socket
• Socket不是一个协议,而是一种编程接口(API)或机制,用于在网络中实现通信。Socket 通常在应用层和传输层之间提供一个端点,使得应用程序可以通过网络发送和接收数据。它支持多种协议,主要是 TCP 和 UDP。
以下是 Socket 的一些基本特点:
• 类型:有两种主要类型的 Sockets ------ TCP Socket(面向连接,可靠)和 UDP Socket(无连接, 不可靠)。
• 应用:在各种网络应用中广泛使用,如网页服务器、聊天应用、在线游戏等。
• 编程语言支持:大多数现代编程语言如 Python, Java, C++, 等都提供 Socket 编程的支持。
• 功能:提供了创建网络连接、监听传入的连接、发送和接收数据等功能。
• QT: 在QT组件中,QTcpSocket用来管理和实现TCP Socket通信,QUdpSocket用来管理和实现 UDP Socket通信
• 总之,Socket 是实现网络通信的基础工具之一,它抽象化了网络层的复杂性,为开发者提供了一种相对简单的方式来建立和管理网络连接。
4. 创建QTtcp服务端
4.1 ui

4.2 代码
• widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QAbstractSocket>
#include <QDebug>
#include <QMessageBox>
#include <QNetworkInterface>
#include <QTcpServer>
#include <QTcpSocket>
#include "mycombobox.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//界面初始化
this->setLayout(ui->verticalLayout);
this->setFixedSize(796,746);
//btnStopListen,btnLineout和btnSend这三个按键在未连接的时候是不能使用的
ui->btnStopListen->setEnabled(false);
ui->btnLineout->setEnabled(false);
ui->btnSend->setEnabled(false);
//创建TCP服务器
server = new QTcpServer(this);//在当前窗口
//创建信号与槽,当有客户端来连接时,就触发这个信号
//当有新的客户端连接到服务器时,就发送一个newConnection信号
connect(server,&QTcpServer::newConnection,this,&Widget::on_newClient_connect);
//QList<QHostAddress> QNetworkInterface::allAddresses()
//这个静态方法是用于获取主机上所有的IP地址
QList<QHostAddress> addresses = QNetworkInterface::allAddresses();
//然后一个一个刷上comboBoxAddr这里面
for(QHostAddress address : addresses){
//然后一个个放到ui的comboBoxAddr这里,只放IPV4的
//protocol() 是 "告诉我这个IP地址是IPv4还是IPv6"
if(address.protocol() == QAbstractSocket::IPv4Protocol)
ui->comboBoxAddr->addItem(address.toString());
}
//给on_ComboBox_clicked()创建信号与槽,刷新拉下列表
connect(ui->comboBoxChildren,SIGNAL(on_ComboBox_clicked()),this,SLOT(mComboBox_refresh()));
}
void Widget::mComboBox_refresh(){
//先清理一下comboBoxChildren
ui->comboBoxChildren->clear();
//获取所有的客户端连接
//获取server对象下所有的 QTcpSocket 客户端连接。
QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>();
//把所有端口号添加到comboBoxChildren
for(QTcpSocket* tmp : tcpSocketClients){
//断开的客户端不应该加入进去,但是断开的客户端以0端口号还是会存在在server
//如果不清理会影响重新连接的客户端
if(tmp != nullptr)
ui->comboBoxChildren->addItem(QString::number(tmp->peerPort()));
}
//在添加all
ui->comboBoxChildren->addItem("all");//选择全部
}
void Widget::on_readyRead_Handler(){
//sender()获得是那个socket通道发来的信号的。(可能连接了多个客户端)
//找到那个通道连接的那个对象QTcpSocket
QTcpSocket* tmpSocket = qobject_cast<QTcpSocket *>(sender());
//获取数据
// QString revDate = tmpSocket->readAll();
// revDate = revDate.toUtf8();
QString revDate = tmpSocket->readAll();
QString data = revDate.toUtf8();
//把数据放到textEditRev上面
ui->textEditRev->insertPlainText("客户端" + QString::number(tmpSocket->peerPort()) + ":" + data + "\n");
//让滚动条一直在下面,并且显示光标
ui->textEditRev->moveCursor(QTextCursor::End);
ui->textEditRev->ensureCursorVisible();
}
void Widget::msocketState(QAbstractSocket::SocketState socketState){
int tmpIndex;
qDebug() << "client out In state: " << socketState;
//找出是那个客户端点击了断开连接
QTcpSocket* tmpSocket = qobject_cast<QTcpSocket *>(sender());
switch(socketState){
case QAbstractSocket::UnconnectedState://这个表示客户端断开连接或者没有连接
//QComboBox::findText() 是在 QComboBox 中查找指定文本并返回其索引的函数。
tmpIndex = ui->comboBoxChildren->findText(QString :: number(tmpSocket->peerPort()));
//根据索引去掉comboBoxChildren里面的端口
ui->comboBoxChildren->removeItem(tmpIndex);
//然后清理该端口的资源
//deleteLater() 是 "请晚一点再删除我",告诉Qt:"等当前事件处理完了,在下一轮事件循环中安全地删除这个对象"
tmpSocket->deleteLater();
if(ui->comboBoxChildren->count() == 0){
//判断还有没有客户端在线
//如果没有的话,此时是不能发消息的
ui->btnSend->setEnabled(false);
}
ui->textEditRev->insertPlainText("客户端" + QString :: number(tmpSocket->peerPort())
+ ":" + "断开连接" + "\n");
break;
}
}
void Widget::on_newClient_connect(){
qDebug() << "newClient In";
//检查服务器的 待处理连接队列中是否有客户端在等待
//多个客户端连接的时候
if(server->hasPendingConnections()){
//获得QTcpSocket以与客户端通信
QTcpSocket* connection = server->nextPendingConnection();
//打印客户端的地址和端口号
qDebug() << "Client Addr: " << connection->peerAddress().toString()
<< "Port: " << connection->peerPort();
//将连接显示到textEditRev上面
ui->textEditRev->insertPlainText("客户端地址:" + connection->peerAddress().toString()
+ "\n客户端端口号:" + QString :: number(connection->peerPort()) + "\n");
//创建接收到客户端数据的信号与槽
//当连接成功的时候,就进行数据交互,本质还是IO操作
//readyRead() 信号是 "有新的数据可以读了" 的通知
connect(connection,SIGNAL(readyRead()),this,SLOT(on_readyRead_Handler()));
//创建客户端断开的信号与槽
//方式一,void QAbstractSocket::stateChanged(QAbstractSocket::SocketState socketState)
//当 QAbstractSocket 的状态发生变化时,会发出这个信号
connect(connection,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(msocketState(QAbstractSocket::SocketState)));
//处理连接的时候,把加进来的连接的端口放到comboBoxChildren
//但是仅仅只在ui上面
//setCurrentText() 是 "请在下拉框/组合框中显示这个文本"
//把连接进来的客户端端口号放进comboBoxChildren
ui->comboBoxChildren->addItem(QString::number(connection->peerPort()));
ui->comboBoxChildren->setCurrentText(QString::number(connection->peerPort()));
//有连接就恢复发送按键
//isChecked() 是用于查询按钮类控件是否被选中的函数,返回 true 表示选中/勾选,false 表示未选中。
if(!ui->btnSend->isChecked()){
ui->btnSend->setEnabled(true);
}
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btnListen_clicked()
{
//开始监听
//bool QTcpServer::listen
//(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
//QHostAddress要检查的IP地址,port就是端口
//重新构造,将字符串IP地址转化为QHostAddress这个类型
//QHostAddress addr();
//QHostAddress::Any 是自动检查并同时监听 IPv4 和 IPv6 接口。
//listen() 返回 true 表示监听成功,false 表示监听失败。
int port = ui->lineEditPort->text().toInt();//端口
if(!server->listen(QHostAddress(ui->comboBoxAddr->currentText()),port)){//监听不成功的时候
qDebug() << "listenError";
QMessageBox msgBox;
msgBox.setWindowTitle("监听失败");
msgBox.setText("端口被占用!");
msgBox.exec();
return;
}
//监听成功
ui->btnListen->setEnabled(false);
ui->btnStopListen->setEnabled(true);
ui->btnLineout->setEnabled(true);
}
void Widget::on_btnStopListen_clicked()
{
//关闭所有的QTcpSocket
// QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
//在当前对象的所有子对象中查找指定名称和类型的对象
//获取server对象下所有的 QTcpSocket 客户端连接。
QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>();//获得了所有的客户端
//关闭所有客户端与服务端的连接
for(QTcpSocket* tmp: tcpSocketClients){
tmp->close();
}
//关闭服务端
server->close();
//重置按钮
ui->btnListen->setEnabled(true);
ui->btnStopListen->setEnabled(false);
ui->btnLineout->setEnabled(false);
}
void Widget::on_btnLineout_clicked()
{
//程序直接退出
on_btnStopListen_clicked();
//删除服务端
delete server;
//退出程序
this->close();
}
void Widget::on_btnSend_clicked()
{
// QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
//在当前对象的所有子对象中查找指定名称和类型的对象
//获取server对象下所有的 QTcpSocket 客户端连接。
//找出所有与客户端的连接
QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>();
//判断是否有连接
if(tcpSocketClients.isEmpty()){
QMessageBox msgBox;
msgBox.setWindowTitle("发送失败");
msgBox.setText("当前无连接!");
msgBox.exec();
return;
}
//判断是私发还是广播
if(ui->comboBoxChildren->currentText() != "all"){//选择是那个端口号发送
QString currentName = ui->comboBoxChildren->currentText();
//查找是否有这个端口号
for(QTcpSocket* tmp : tcpSocketClients){
if(QString::number(tmp->peerPort()) == currentName){
tmp->write(ui->textEditSend->toPlainText().toStdString().c_str());
}
}
}else if(ui->comboBoxChildren->currentText() == "all"){
//广播
for(QTcpSocket* tmp : tcpSocketClients){
tmp->write(ui->textEditSend->toPlainText().toStdString().c_str());
}
}
}
• widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QAbstractSocket>
#include <QTcpServer>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void on_newClient_connect();
void on_readyRead_Handler();
void msocketState(QAbstractSocket::SocketState);
void mComboBox_refresh();
private slots:
void on_btnListen_clicked();
void on_btnStopListen_clicked();
void on_btnLineout_clicked();
void on_btnSend_clicked();
private:
Ui::Widget *ui;
QTcpServer *server;
};
#endif // WIDGET_H
• mycombobox.cpp
cpp
#include "mycombobox.h"
#include <QMouseEvent>
//QT中没有点击ComboBox就刷新的信号
//所有需要重写鼠标点击事件,然后发送一个信号
mycombobox::mycombobox(QWidget *parent) : QComboBox(parent)
{
}
void mycombobox::mousePressEvent(QMouseEvent *e)
{
//button() - 返回触发当前事件的特定按钮
if(e->button() == Qt::LeftButton){
emit on_ComboBox_clicked();
}
//继续传递下去,调用基类处理,确保正常弹出下拉列表
// 缺少 QComboBox::mousePressEvent(e); 可能导致 ComboBox无法正常弹出选项列表
QComboBox::mousePressEvent(e);
}
• mycombobox.h
cpp
#ifndef MYCOMBOBOX_H
#define MYCOMBOBOX_H
#include <QComboBox>
#include <QWidget>
class mycombobox : public QComboBox
{
Q_OBJECT
public:
mycombobox(QWidget* parent);
protected:
void mousePressEvent(QMouseEvent *e) override;
signals:
void on_ComboBox_clicked();
};
#endif // MYCOMBOBOX_H
5. 创建QTtcp客户端
5.1 ui

5.2 代码
• widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QTcpSocket>
#include <QTimer>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//UI的初始化
this->setLayout(ui->verticalLayout);
//断开连接和发送在还有连接到服务端是不能操作的
ui->btndisCon->setEnabled(false);
ui->btnSend->setEnabled(false);
//创建新的TcpSocket去连接服务端
client = new QTcpSocket(this);
//创建读取的信号与槽
connect(client,SIGNAL(readyRead()),this,SLOT(mRead_Data_From_Server()));
}
void Widget::mRead_Data_From_Server(){
//读取服务端发送过来的数据
setInsertColor(Qt::black,client->readAll());
//解决滚动条问题
ui->textEditRev->moveCursor(QTextCursor::End);
ui->textEditRev->ensureCursorVisible();
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btnConnect_clicked()
{
//这个函数用来启动一个到远程服务器的连接,连接结果通过信号通知。
//virtual void
//connectToHost(const QString &hostName, quint16 port,
// QIODevice::OpenMode openMode = ReadWrite, QAbstractSocket::NetworkLayerProtocol protocol = AnyIPProtocol)
//去连接服务端,根据IP和端口号
client->connectToHost(ui->lineEditIPAddr->text(),ui->lineEditPort->text().toInt());
//用于获取套接字的当前状态
//这里有一个bug,就是错误的服务端IP地址都能连接成功,这是由于client->state() == QAbstractSocket::ConnectingState
//这个条件造成的,因为正在连接也不知道能不能连接成功,只要是正在连接就过了这个条件
// if(client->state() == QAbstractSocket::ConnectingState ||
// client->state() == QAbstractSocket::ConnectedState){
// ui->textEditRev->append("连接成功!");
// ui->lineEditPort->setEnabled(false);
// ui->lineEditIPAddr->setEnabled(false);
// ui->btnConnect->setEnabled(false);
// ui->btndisCon->setEnabled(true);
// ui->btnSend->setEnabled(true);
// }
//解决方法,超时判负
timer = new QTimer(this);//创建定时器
timer->setSingleShot(true);//设置为单次定时器
timer->setInterval(5000);//间隔为5s
//如果超时的操作
connect(timer,SIGNAL(timeout()),this,SLOT(onTimeout()));
//所以解决方法还是用信号与槽,使用这个信号(判断客户端是否连接成功)
//QAbstractSocket::connected() 是 Qt 网络编程中表示连接成功建立的信号。
connect(client,SIGNAL(connected()),this,SLOT(onConnect()));
//而且还需要做连接不成功的信号与槽,
//QAbstractSocket::error() 是 Qt 网络编`程中表示发生错误的信号。
connect(client,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(onError(QAbstractSocket::SocketError)));
//不希望被按下一次连接之后,被多次按下,从而造成影响
this->setEnabled(false);
//开启定时器
timer->start();
}
void Widget::onError(QAbstractSocket::SocketError socketError){
qDebug() << "连接错误:" << socketError;//错误码打印出来
//errorString() 是 "告诉我刚才发生了什么错误" 的错误信息查询函数,返回人类可读的错误描述。
ui->textEditRev->append("连接出问题了:" + client->errorString());
this->setEnabled(true);
on_btndisCon_clicked();
}
void Widget::onConnect(){//连接成功就做这个事
//关闭定时器
timer->stop();
ui->textEditRev->append("连接成功!");
this->setEnabled(true);
ui->lineEditPort->setEnabled(false);
ui->lineEditIPAddr->setEnabled(false);
ui->btnConnect->setEnabled(false);
ui->btndisCon->setEnabled(true);
ui->btnSend->setEnabled(true);
}
void Widget::onTimeout(){
ui->textEditRev->append("连接超时!");
//还要清理之前不正确的IP,不然会对正确的IP造成影响的
client->abort();
this->setEnabled(true);
}
void Widget::on_btndisCon_clicked()
{
//disconnectFromHost() 是 "我要优雅地断开连接,把该发的数据都发完再走" 的礼貌断开函数。
client->disconnectFromHost();
client->close();
ui->textEditRev->append(QString::number(client->peerPort()) + "断开连接!");
ui->lineEditPort->setEnabled(true);
ui->lineEditIPAddr->setEnabled(true);
ui->btnConnect->setEnabled(true);
ui->btndisCon->setEnabled(false);
ui->btnSend->setEnabled(false);
}
void Widget::setInsertColor(Qt::GlobalColor color,QString str){
// //ui->textEditRev->setForegroundRole(),这个也可以设置颜色,
// //但是会统一设置的,没法做到颜色上区分,所以要使用光标级别的
// QTextCursor cursor = ui->textEditRev->textCursor();
// //void QTextCursor::setCharFormat(const QTextCharFormat &format)
// //用于设置文本字符格式。
// QTextCharFormat format;//QTextCharFormat 类是 Qt 中用于设置文本字符格式的类
// //用于设置前景画刷的函数。
// format.setForeground(QBrush(QColor(Qt::red)));
// cursor.setCharFormat(format);
// cursor.insertText(ui->textEditSend->toPlainText());
//获得当前光标
QTextCursor cursor = ui->textEditRev->textCursor();
//QTextCursor::setCharFormat(const QTextCharFormat &format)是QTextCursor类的一个方法,用于应用字符格式到文本。
QTextCharFormat format;
//设置前景色
format.setForeground(QBrush(QColor(color)));
//应用字符格式到文本。
cursor.setCharFormat(format);
//insertText()是QTextCursor类的一个方法,用于在光标位置插入文本。
cursor.insertText(str);
}
void Widget::on_btnSend_clicked()
{
//获取要发送的数据
QByteArray sendData = ui->textEditSend->toPlainText().toUtf8();
//发送出去
client->write(sendData);
//区别客户端发送的消息和服务端发送过来的消息(用颜色区分)
setInsertColor(Qt::red,sendData);//客户端发送,显示红色
}
• widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QTcpSocket>
#include <QTimer>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_btnConnect_clicked();
void onTimeout();
void onConnect();
void onError(QAbstractSocket::SocketError socketError);
void on_btndisCon_clicked();
void on_btnSend_clicked();
void mRead_Data_From_Server();
private:
Ui::Widget *ui;
QTcpSocket* client;
QTimer *timer;
void setInsertColor(Qt::GlobalColor color,QString str);//用来设置颜色
};
#endif // WIDGET_H