记录学习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客户端进行通信