记录学习TCP通信的过程,包括理论知识、在Qt中建立TCP服务端和客户端,并附上源代码。由于最近的项目中也使用到了海康VisionMaster软件,可以将其作为服务端,用Qt写的TCP客户端和其进行通信测试,方便调试。
目录
- 1.关于TCP理论知识
- 1.1 TCP如何保证可靠性
- 1.2 简述下TCP建立连接和断开连接的过程
- 1.3 TCP的模型
- 1.4 HTTP和HTTPS的区别,以及HTTPS有什么优缺点
 
- 2.Qt中TCP通信
- 2.1 QTcpServer
- 2.2 QTcpSocket
- 2.3 使用多线程进行网络通信
- 2.4 源码:TCP服务端和TCP客户端
- TCP客户端
- TCP服务端
- 使用
 
 
- 3.使用海康VisionMaster和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客户端进行通信
 
 
 



















