Qt学习日记 Day13:网络编程(一)-TCP基础与Socket实战

今天学了什么

今天学习Qt的TCP部分。

TCP

定义:TCP(传输控制协议),是面向连接的字节流传输层协议。

TCP 五大特点:

  • 面向连接:通信前建立连接,通信结束断开连接;
  • 可靠性传输:数据不丢、不错、不乱序、不重复;
  • 字节流传输:无消息边界,数据是连续字节;
  • 全双工:双方可同时收发数据;
  • 自带流量控制、拥塞控制;

TCP 为什么可靠(6点):

  • 三次握手:建立连接,确认双方收发能力正常;
  • ACK应答机制:收到数据必须回复确认;
  • 超时重传:未收到ACK,自动重发数据;
  • 序号+确认号:实现数据排序、去重;
  • 流量控制(滑动窗口):防止接收缓冲区溢出;
  • 拥塞控制:防止网络拥堵瘫痪;

三次握手(建立连接):

  1. 客户端发 SYN:请求连接;
  2. 服务端回 SYN+ACK:同意连接;
  3. 客户端回 ACK:连接正式建立;

四次挥手(断开连接):

TCP全双工,收发通道分开关闭,所以需要四次:

  1. 主动方发 FIN:我不发数据了;
  2. 被动方回 ACK:收到,我还能继续发数据;
  3. 被动方发 FIN:我也发完了,也不发了;
  4. 主动方回 ACK:确认关闭(等待2MSL);

2MSL 概念:

  • MSL:报文在网络中的最大存活时间;
  • 2MSL:主动关闭方最后等待的时间;

作用:保证最后一次 ACK 报文对方一定收到;等待网络中残留的旧数据包全部消失,不干扰新连接。

粘包、拆包:

  1. 产生原因:TCP是无边界字节流 + 内核缓冲区 + Nagle算法 + 网络MTU分片。

  2. 定义:

  • 粘包:多条消息合并成一条接收;
  • 拆包:一条大消息被分成多次接收;
  1. 三种解决办法(从优到劣):
  • 头部长度法(项目最常用):前4字节存消息长度;
  • 特殊分隔符法:文本结尾加标记;
  • 定长数据包:固定每条消息长度;

Nagle 算法(粘包核心原因):

作用:攒小包、合并发送,减少网络数据包数量,优化网络效率。

副作用:攒包机制 导致 TCP 粘包;

Qt关闭方式:开启低延迟,小包立刻发送:

socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);

Qt TCP 核心信号:

  1. QTcpSocket 客户端/通信端:
  • connected:连接成功(只发数据,不读数据);
  • readyRead:有数据到达(只能在这里读数据);
  • disconnected:连接断开;
  • errorOccurred:通信出错;
  1. QTcpServer 服务端:
  • newConnection:监听到新客户端连接;

TCP客户端连接服务器步骤:

① 创建客户端对象;

② socket->connectToHost(Ip,port); //"127.0.0.1",8080

③ connect(socket,&QTcpSocket::connected,...

TCP服务器连接客户端步骤:

① 创建服务器对象;

② serve->listen(QHostAddress::Any,port);

③ connect(serve,&QTcpServer::newConnection,this,=(){

soc=serve->nextPendingConnection();

connect(soc,&QTcpSocket::readyRead,this,=(){

QByteArray text=soc->readAll();

...}); });

任务1:极简 TCP 服务端与客户端

服务端:

QTcpServer 监听端口,收到 newConnection() 后获取 QTcpSocket,收到数据打印出来。

客户端:

QTcpSocket 连接服务端,发送 "Hello Server"。

实现:

① 服务器端:

cpp 复制代码
   serve=new QTcpServer(this);
    serve->listen(QHostAddress::Any,8080);
    connect(serve,&QTcpServer::newConnection,this,[=](){
        soc=serve->nextPendingConnection();
        connect(soc,&QTcpSocket::readyRead,this,[=](){
            QByteArray text=soc->readAll();
            qDebug()<<"服务器接收到客户端发送的消息:"<<text;
            soc->write("hello Scoket");
        });
    });

② 客户端:

cpp 复制代码
  socket=new QTcpSocket(this);
    socket->connectToHost("127.0.0.1",8080);
    connect(socket,&QTcpSocket::connected,this,[=](){
        socket->write("Hello Server");
    });
    connect(socket,&QTcpSocket::readyRead,[=](){
        QByteArray text=socket->readAll();
        qDebug()<<"客户端接收到服务器发送的消息:"<<text;
    });

带界面的简易聊天室

要求:点击按钮,客户端发送消息给服务端;服务端收到后回复客户端。

实现:

① 设计UI界面:

② 编写代码

cpp 复制代码
#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    serve=new QTcpServer(this);
    socket=new QTcpSocket(this);

    //显示服务器Ip
    QString ip;
    for(auto &a:QNetworkInterface::allAddresses())
    {
        if(a.protocol()==QAbstractSocket::IPv4Protocol&&!a.isLoopback())
        {
            ip=a.toString();
            break;
        }
    }
    qDebug()<<"拿到的Ip:"<<ip;
    ui->server_Ip_line->setText(ip);

    connect(serve,&QTcpServer::newConnection,this,[=](){
        QTcpSocket *s=serve->nextPendingConnection();
        soc=s;
        connect(s,&QTcpSocket::readyRead,this,[=]()
                {
            //读取发送来的内容
            QByteArray text=s->readAll();
            qDebug()<<"原始字节:"<<text;
            ui->server_textEdit->append(QString("收到客户端发送信息:%1").arg(QString::fromUtf8(text)));
        });
    });

    connect(socket,&QTcpSocket::connected,this,[=](){
        ui->cus_textEdit->append("客户端连接服务器成功。");
    });
    connect(socket,&QTcpSocket::readyRead,this,[=](){
        QByteArray text=socket->readAll();
        ui->cus_textEdit->append("收到服务器发送信息:"+text);
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_server_con_btn_clicked()
{
    int port=ui->server_port_line->text().toUInt();
    bool flag=serve->listen(QHostAddress::Any,port);
    if(flag)
    {
        ui->server_textEdit->append("服务端监听成功,端口:"+QString::number(port));
    }
    else
    {
        ui->server_textEdit->append("监听失败!端口被占用");
    }
}


void MainWindow::on_server_sendmsg_btn_clicked()
{
    if(serve->isListening())
    {
        soc->write(ui->server_sendmsg_line->text().toUtf8());
    }
}


void MainWindow::on_cus_con_btn_clicked()
{
    QString ip=ui->cus_Ip_line->text();
    int port=ui->cus_port_line->text().toInt();
    socket->connectToHost(ip,port);
}


void MainWindow::on_cus_sendmsg_btn_clicked()
{
    if(socket->isOpen())
    {
        socket->write(ui->cus_sendmsg_line->text().toUtf8());
    }
}


void MainWindow::on_dele_clicked()
{
    ui->cus_textEdit->setText("客户端连接服务器成功。");
}

复盘:① 文本框勾选readOnly,防止运行后,输入干扰;

② 显示服务器的IP地址,以供客户端连接;

③ 文本格式用UTF-8,防止读出来乱码;

④ 代码只是练习用,正式项目里服务端、客户端一定是分开两个程序(两个工程);同一

个窗口里一边服务端一边客户端,只适合学习调试;

⑤ soc 指针没有进行空指针判断:

在 on_server_sendmsg_btn_clicked 里,如果还没有客户端连接,soc 可能是空指针或者野指针,

直接 soc->write() 会导致程序崩溃。应该先判断:

if(soc && soc->state() == QAbstractSocket::ConnectedState);

⑥ socket->write 的返回值:

TCP 是流式协议,write 不一定能一次性把所有数据发出去。虽然小数据通常没问题,但严谨的做

法是检查返回值或者写大块数据时处理发送缓冲区;

⑦ 客户端断线没有处理:没有监听 disconnected 信号。如果服务端关掉,客户端不知道,再发数

据就会报错。应该监听 disconnected 信号并在界面上提示"连接已断开"。

相关推荐
箓维1 小时前
TCP Socket的进阶实现
网络·笔记·tcp/ip
架构师伯子明2 小时前
Ros 学习十三:IK Demo
学习
小羊没烦恼!2 小时前
关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
java·服务器·网络·学习·负载均衡
小Ti客栈2 小时前
Netty 零拷贝原理与实战
运维·服务器·网络
国科安芯2 小时前
商业光学遥感载荷焦平面供电链的低噪声稳压器研究综述——以国科安芯ASP7A84AS型3A低压差线性稳压器为器件级参照
大数据·网络·嵌入式硬件·平面·卫星·商业航天·抗辐射
幼儿园蛋小黄2 小时前
从 STM32N647 的 AI 部署入门嵌入式:一条可落地的学习路线
人工智能·stm32·学习
静以修身zzz2 小时前
计算云服务
运维·网络·云计算
星恒随风2 小时前
Linux网络编程:应用层协议究竟是什么?从网络计算器到TCP Socket抽象基类完整实现
linux·网络·笔记·tcp/ip
万联WANFLOW3 小时前
异地组网速度忽快忽慢到底怎么回事?直连与中继的判断方法和处理步骤
运维·网络·网络协议·p2p