关于TCP通信的学习和应用案例

记录学习TCP通信的过程,包括理论知识、在Qt中建立TCP服务端和客户端,并附上源代码。由于最近的项目中也使用到了海康VisionMaster软件,可以将其作为服务端,用Qt写的TCP客户端和其进行通信测试,方便调试。

1.关于TCP理论知识

参考:https://www.nowcoder.com/tutorial/93/e1b14ab2b40a4ef98d9e55830eb48d66

  socket(套接字):四元组:客户端ip+端口号port+服务端ip+端口号port,保证这是绝对唯一的连接(这是一个整体)。端口号最多有65535个。

1.1 TCP如何保证可靠性

  • (1)序列号、确认应答、超时重传
      数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值
  • (2)窗口控制与高速重发控制/快速重传(重复确认应答)
      TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
      使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒…
  • (3)拥塞控制
      如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵(大家都在用网,你在这狂发,吞吐量就那么大,当然会堵),甚至造成网络的瘫痪。所以TCP在为了防止这种情况而进行了拥塞控制
      慢启动:定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到确认应答(经过一个rtt),将拥塞窗口大小*2。
      拥塞避免:设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个rtt,拥塞窗口大小+1),以此来避免拥塞。
    将报文段的超时重传看做拥塞,则一旦发生超时重传,我们需要先将阈值设为当前窗口大小的一半,并且将窗口大小设为初值1,然后重新进入慢启动过程。
      快速重传:在遇到3次重复确认应答(高速重发控制)时,代表收到了3个报文段,但是这之前的1个段丢失了,便对它进行立即重传。
    然后,先将阈值设为当前窗口大小的一半,然后将拥塞窗口大小设为慢启动阈值+3的大小。
      这样可以达到:在TCP通信时,网络吞吐量呈现逐渐的上升,并且随着拥堵来降低吞吐量,再进入慢慢上升的过程,网络不会轻易的发生瘫痪。

1.2 简述下TCP建立连接和断开连接的过程

客户端包括:
①应用层
②传输控制层:TCP、UDP。TCP是面向连接的、可靠的传输。TCP包括:三次握手、数据传输、四次分手
③网络层
④链路层
⑤物理层

  • TCP建立连接和断开连接的过程
    在这里插入图片描述
  • 三次握手
       三次握手是建立连接,开启socket。
      1.Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
      2.Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
      3.Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
    在这里插入图片描述
  • 四次挥手
      四次分手是断开连接,进行资源的释放。
      由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

1.数据传输结束后,客户端的应用进程发出连接释放报文段,并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。
2.服务器接收到FIN后,发送一个ACK给客户端,确认序号为收到的序号+1,服务器进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。
3.当服务器没有数据要发送时,服务器发送一个FIN报文,此时服务器进入LAST_ACK状态,等待客户端的确认
4.客户端收到服务器的FIN报文后,给服务器发送一个ACK报文,确认序列号为收到的序号+1。此时客户端进入TIME_WAIT状态,等待2MSL(MSL:报文段最大生存时间),然后关闭连接。

1.3 TCP的模型

四层TCP/IP模型如下:
在这里插入图片描述

1.4 HTTP和HTTPS的区别,以及HTTPS有什么优缺点

  • HTTP协议和HTTPS协议区别如下
      1.HTTP协议是以明文的方式在网络中传输数据,而HTTPS协议传输的数据则是经过TLS加密后的,HTTPS具有更高的安全性
      2.HTTPS在TCP三次握手阶段之后,还需要进行SSL 的handshake,协商加密使用的对称加密密钥
      3.HTTPS协议需要服务端申请证书,浏览器端安装对应的根证书
      4.HTTP协议端口是80,HTTPS协议端口是443

  • HTTPS优点
      1.HTTPS传输数据过程中使用密钥进行加密,所以安全性更高
      2.HTTPS协议可以认证用户和服务器,确保数据发送到正确的用户和服务器

  • HTTPS缺点
      1.HTTPS握手阶段延时较高:由于在进行HTTP会话之前还要进行SSL握手,因此HTTPS协议握手阶段延时增加
      2.HTTPS部署成本高:一方面HTTPS协议需要使用证书来验证自身的安全性,所以需要购买CA证书;另一方面由于采用HTTPS协议需要进行加解密的计算,占用CPU资源较多,需要的服务器配置或数目高。


在这里插入图片描述
  三次握手、四次握手都是传输控制层的,属于内核,我们写代码时不需要考虑这些。
在这里插入图片描述
  什么是TCP?
TCP是面向连接的可靠的传输控制协议。
  使用过程:
通过程序从客户端C向服务端S发送请求syn,服务端S收到请求后对客户端C进行回应,发送syn+ack,客户端C向服务端S发送ack表示收到回应。由此建立客户端C和服务端S的联系,建立之后在服务端S建立一个内存空间buffer,在客户端C建立一个内存空间buffer,由程序控制两个内存空间之间的数据传输。

在这里插入图片描述
程序不能直接控制这两个内存空间,需要通过socket来处理。
什么是socket?
又称套接字、插座。
所谓套接字,即有套接和被套接,插座有插头和插座,即2组,IP+Port(IP地址+端口号)
2组组成:ip:port + ip:port —— 4元组
哪怕其中只有一个改变了,也是独立的一组


2.Qt中TCP通信

https://subingwen.cn/linux/socket/#4-TCP%E9%80%9A%E4%BF%A1%E6%B5%81%E7%A8%8B

2.1 QTcpServer

在这里插入图片描述
TCP服务器端:首先创建套接字socket(),然后绑定bind(),设置监听listen(),然后等待客户端的连接accept(),连接成功之后,接收数据rev()和发送数据send(),最后关闭套接字close()

TCP客户端:首先创建套接字socket(),然后连接服务器connect(),连接之后和服务器进行通信,发送数据send()和接收数据rev(),最后关闭套接字close()

在这里插入图片描述

2.2 QTcpSocket

在这里插入图片描述

2.3 使用多线程进行网络通信

示例场景:TCP客户端和服务端,通过子线程处理将客户端的文件发送给服务器

https://www.bilibili.com/video/BV1LB4y1F7P7/?p=10&spm_id_from=pageDriver&vd_source=858585879400a2acad4b4d9a0283f25d

2.4 源码:TCP服务端和TCP客户端

环境:Ubuntu16.04 + QT5.12.9 + CMake3.21

TCP客户端

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(tcp_client LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt5 COMPONENTS Widgets REQUIRED Network)

if(ANDROID)
  add_library(tcp_client SHARED
    main.cpp
    tcpclient.cpp
    tcpclient.h
    tcpclient.ui
    resources.qrc
  )
else()
  add_executable(tcp_client
    main.cpp
    tcpclient.cpp
    tcpclient.h
    tcpclient.ui
    resources.qrc
  )
endif()

target_link_libraries(tcp_client PRIVATE Qt5::Widgets Qt5::Network)

tcpclient.ui
在这里插入图片描述
main.cpp

#include "tcpclient.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    TcpClient w;
    w.show();
    w.setWindowTitle("TCP - 客户端");
    return a.exec();
}

tcpclient.h

#ifndef TCPCLIENT_H
#define TCPCLIENT_H

#include <QMainWindow>
#include <QTcpSocket>
#include <QHostAddress>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui {
class TcpClient;
}
QT_END_NAMESPACE

class TcpClient : public QMainWindow
{
    Q_OBJECT

public:
    TcpClient(QWidget *parent = nullptr);
    ~TcpClient();

private slots:
    void on_pbn_connect_clicked();

    void on_pbn_disconnect_clicked();

    void on_pbn_send_message_clicked();

private:
    Ui::TcpClient *ui;

    QTcpSocket *tcp_{ nullptr };
    QLabel *status_{ nullptr };
};
#endif // TCPCLIENT_H

tcpclient.cpp

#include "tcpclient.h"
#include "./ui_tcpclient.h"

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

    ui->le_port->setText("8000");
    ui->le_ip->setText("127.0.0.1");
    ui->pbn_disconnect->setEnabled(false);

    // 创建通信的套接字对象
    tcp_ = new QTcpSocket(this);

    // 检测服务器是否回复了数据
    connect(tcp_, &QTcpSocket::readyRead, [=] {
        // 接收服务器发送的数据
        QByteArray recv_msg = tcp_->readAll();
        ui->txe_message->append("服务器Say: " + recv_msg);
    });

    // 检测是否和服务器是否连接成功了
    connect(tcp_, &QTcpSocket::connected, this, [=]() {
        ui->txe_message->append("恭喜, 连接服务器成功!!!");
        status_->setPixmap(QPixmap(":/resources/connect.png").scaled(20, 20));
        ui->pbn_connect->setEnabled(false);
        ui->pbn_disconnect->setEnabled(true);
    });

    // 检测服务器是否和客户端断开了连接
    connect(tcp_, &QTcpSocket::disconnected, this, [=]() {
        ui->txe_message->append("服务器已经断开了连接...");
        ui->pbn_connect->setEnabled(true);
        ui->pbn_disconnect->setEnabled(false);
        status_->setPixmap(
            QPixmap(":/resources/disconnect.png").scaled(20, 20));
    });

    // 设置连接状态的状态栏
    status_ = new QLabel(this);
    status_->setPixmap(QPixmap(":/resources/disconnect.png").scaled(20, 20));
    ui->statusbar->addWidget(new QLabel("连接状态:"));
    ui->statusbar->addWidget(status_);
}

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

void TcpClient::on_pbn_connect_clicked()
{
    QString ip = ui->le_ip->text();
    unsigned short port = ui->le_port->text().toInt();
    // 连接服务器
    tcp_->connectToHost(QHostAddress(ip), port);
    ui->pbn_connect->setEnabled(false);
    ui->pbn_disconnect->setEnabled(true);
}

void TcpClient::on_pbn_disconnect_clicked()
{
    tcp_->close();
    ui->pbn_connect->setEnabled(true);
    ui->pbn_disconnect->setEnabled(false);
}

void TcpClient::on_pbn_send_message_clicked()
{
    QString send_msg = ui->txe_send_message->toPlainText();
    tcp_->write(send_msg.toUtf8());
    ui->txe_message->append("客户端Say: " + send_msg);
    ui->txe_send_message->clear();
}

运行效果:
在这里插入图片描述

TCP服务端

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)

project(tcp_server LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt5 COMPONENTS Widgets REQUIRED Network)

if(ANDROID)
  add_library(tcp_server SHARED
    main.cpp
    tcpserver.cpp
    tcpserver.h
    tcpserver.ui
    resources.qrc
  )
else()
  add_executable(tcp_server
    main.cpp
    tcpserver.cpp
    tcpserver.h
    tcpserver.ui
    resources.qrc
  )
endif()

target_link_libraries(tcp_server PRIVATE Qt5::Widgets Qt5::Network)

tcpserver.ui
在这里插入图片描述
main.cpp

#include "tcpserver.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    TcpServer w;
    w.show();
    w.setWindowTitle("TCP - 服务器");
    return a.exec();
}

tcpserver.h

#ifndef TCPSERVER_H
#define TCPSERVER_H

#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui {
class TcpServer;
}
QT_END_NAMESPACE

class TcpServer : public QMainWindow
{
    Q_OBJECT

public:
    TcpServer(QWidget *parent = nullptr);
    ~TcpServer();

private slots:
    void on_pbn_set_listen_clicked();

    void on_pbn_send_data_clicked();

private:
    Ui::TcpServer *ui;

    QTcpServer *server_{ nullptr };
    QTcpSocket *tcp_{ nullptr };
    QLabel *status_{ nullptr };
};
#endif // TCPSERVER_H

tcpserver.cpp

#include "tcpserver.h"
#include "./ui_tcpserver.h"

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

    ui->le_port->setText("8899");

    // 第一步:创建监听的服务对象
    server_ = new QTcpServer(
        this); // 指定实例化父类this,即QMainWindow,待页面析构时,server_也被析构

    // 第三步:通过 QTcpServer::newConnection()信号检测是否有新的客户端连接
    // 如果有新的客户端连接调用 QTcpSocket *QTcpServer::nextPendingConnection()
    // 得到通信的套接字对象
    connect(server_, &QTcpServer::newConnection, this, [=]() {
        tcp_ = server_->nextPendingConnection();
        ui->txe_record->append("成功和客户端建立了新的连接...");
        status_->setPixmap(QPixmap(":/resources/connect.png").scaled(20, 20));

        // 检测是否有客户端数据
        connect(tcp_, &QTcpSocket::readyRead, this, [=]() {
            // 接收数据
            QByteArray data = tcp_->readAll();
            ui->txe_record->append("客户端Say:" + data);
        });

        // 检测客户端是否断开了连接
        connect(tcp_, &QTcpSocket::disconnected, this, [=]() {
            ui->txe_record->append("客户端已经断开了连接...");
            tcp_->deleteLater();
            status_->setPixmap(
                QPixmap(":/resources/disconnect.png").scaled(20, 20));
        });
    });

    // 设置连接状态的状态栏
    status_ = new QLabel(this);
    status_->setPixmap(QPixmap(":/resources/disconnect.png").scaled(20, 20));
    ui->statusbar->addWidget(new QLabel("连接状态:"));
    ui->statusbar->addWidget(status_);
}

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

// 第二步:通过 QTcpServer 对象设置监听,即:QTcpServer::listen()
void TcpServer::on_pbn_set_listen_clicked()
{
    unsigned short port = ui->le_port->text().toUShort();
    // 设置服务器监听
    server_->listen(QHostAddress::Any, port);
    ui->pbn_set_listen->setEnabled(false);
}

void TcpServer::on_pbn_send_data_clicked()
{
    // 将txe_send_message中输入内容转为纯文本的QString形式
    QString msg = ui->txe_send_message->toPlainText();
    // 将QSting类型转为QByteArray类型
    tcp_->write(msg.toUtf8());
    ui->txe_record->append("服务端Say:" + msg);
    ui->txe_send_message->clear();
}

运行效果:
在这里插入图片描述

使用

1.打开TCP-服务端TCP-客户端
在这里插入图片描述
2.点击TCP-服务器启动监听服务器,打开端口8899的监听,修改TCP-客户端服务端端口,改为8899,点击连接服务器。完成客户端和服务端的连接。
在这里插入图片描述
3.客户端和服务端之间可进行通讯。
在这里插入图片描述

3.使用海康VisionMaster和TCP客户端进行通信

1.首先查看当前设备IP地址
在这里插入图片描述
2.打开海康VisionMaster中通信管理
在这里插入图片描述
修改为当前本机IP地址,设置本地端口,使能打开TCP服务端0
在这里插入图片描述
3.运行TCP-客户端,修改服务器地址IP和端口,和VisionMaster中一致,点击连接服务端,即可看到连接服务端成功。
在这里插入图片描述
4.测试海康VisionMaster和TCP客户端进行通信
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Qt是一种跨平台的GUI(图形用户界面)应用程序开发框架,提供了用于创建跨平台应用程序的一系列工具和功能。Modbus TCP是一种通讯协议,用于在多个设备之间传输数据。在工业自动化和远程控制广泛应用。当这两种技术结合使用时,就可以很容易地创建和开发自动化应用程序。 下面将介绍一个qt modbus tcp应用实例: 假设我们需要读取一个远程温度传感器的温度值,并将其显示在qt应用程序。我们可以使用Modbus TCP通信协议来读取传感器值,而Qt可以提供一个用户友好的GUI,用于实时显示温度值。 首先需要安装Qt Modbus库,它包含了在Qt应用程序使用Modbus协议的类和函数。 然后需要创建一个QT界面(GUI),该界面应包含从Modbus服务器读取值的按钮。当用户点击该按钮时,应用程序会与Modbus服务器通信,并读取温度值。读取到的温度值将在GUI实时显示出来。 在qt应用程序,可以使用以下代码进行Modbus通信: //创建modbus客户端 QModbusTcpClient *client = new QModbusTcpClient(this); //连接到modbus tcp服务器 client->setConnectionParameter(QModbusTcpClient::ConnectionParameterIp, "192.168.1.100"); client->setConnectionParameter(QModbusTcpClient::ConnectionParameterPort, 502); client->connectDevice(); //读取温度值 QModbusDataUnit request(QModbusDataUnit::InputRegisters, 0, 1); request.setValue(0, 1); //从设备地址1读取值 QModbusReply *reply = client->sendReadRequest(request, 1); while (!reply->isFinished()) ; //等待回复完成 //显示温度值 if (reply->error() == QModbusDevice::NoError) { const QModbusDataUnit unit = reply->result(); int temperature = unit.value(0); //温度值保存在第一个寄存器 ui->temperatureLabel->setText(QString::number(temperature) + "℃"); } else { ui->temperatureLabel->setText("Error: " + reply->errorString()); } 在这个例子,我们使用了QModbusTcpClient来创建一个Modbus客户端,并连接到Modbus服务器。然后,我们发送一个读取请求,从Modbus服务器读取温度值。一旦读取到值,我们可以将其显示在GUI。 这只是一个具有基本功能的简单示例,实际使用应用程序可能需要更多的功能和贡献,比如通过Modbus协议写值,以及对Modbus服务器和RTU通信的支持等。 总之,Qt Modbus TCP应用示例是多种多样的,从简单的应用程序到工业自动化和远程控制的复杂系统都可以使用这些技术实现。通过使用一个强大的GUI和Modbus协议,开发人员可以创建出易用性高、性能稳定的自动化应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

boss-dog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值