Qt Socket网络编程

网络编程在现在互联网时代是非常重要的,所以这篇博客将带你了解QT语言是如何使用Socket进行TCP和UPD通讯的!

图文超详细,一看就会!不会评论区找我!



一、概述

先来简单了解一下TPC与UDP通讯的知识点!

  1. TCP
    tcp传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议;tcp通讯一定要经过三次握手才可以连接成功进行通讯;且,tcp通讯只能一对一进行连接;现在大多数通讯都是使用tcp协议,例如用于发送文件等。

    QTcpSocket继承自QAbstractSocket,与QUdpSocket传输的数据报不同的是,QTcpSocket传输的是连续的数据流,尤其适合连续的数据传输,TCP一般分为客户端和服务端,即C/S (Client/Server模型)。

    QTcpSocket代表了两个独立的数据流,一个用来读取数据,一个用来写入数据,分别采用QTcpSocket::read()及QTcpSocket::write()操作,读取数据前先调用QTcpSocket::bytesAvailable来确定已有足够的数据可用。

    QTcpServer处理客户端的连接,可通过QTcpServer::listen()监听客户端发来的连接请求,每当有客户端连接时会发射newConnection()信号,QTcpSocket可用于读取客户端发来的数据报,亦可发送数据报。

  2. UDP
    upd协议集支持一个无连接的传输协议,该协议称为用户数据报协议。udp为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。也就是说,udp通讯无需经过三次握手就可以实现通信;但它是一种无脑式通讯,他关心接收方是否接收到了数据;所以udp通讯效率是非常高的,他一般被用于发送文字、视频通话等。

    UDP是一个轻量级、不可靠、面向数据报的、无连接的协议,多用于可靠性要求不严格,不是非常重要的传输。
    QUdpSocket类继承自QAbstractSocket,用来发送和接收UDP数据报,”Socket”即套接字,套接字即IP地址+端口号。其中IP地址指定了网络中的一台主机,二端口号则指定了该主机上的一个网络程序,使用套接字即可实现网络上的两个应用程序之间的通信。

    QUdpSocket支持IPv4广播,要广播数据报,则只需发送到一个特殊的地QHostAddress::Broadcast(即255.255.255.255),数据报一般建议发送字节数小于512字节。端口号选择1024-65535(1024以下的常用作保留端口号,如FTP常用端口号21,Telnet常用端口号23,DNS域名服务器常用端口53等)。

新建项目时,QT编译工具需要在.pro文件中添加代码:QT += network

VS编译工具需要勾选network项:
在这里插入图片描述

注意
QTcpSocket被称为通信套接字;
QTcpServer被称为监听套接字。


二、TCP通信

1. 发送文字

首先你得先了解QtTCP通信的流程,下面将根据下图进行讲解:
在这里插入图片描述

  1. 服务端调用listen函数进行监听,是否有客户端与其进行连接;
  2. 客户端需要进行主动与客户端连接,调用connectToHost进行连接;
  3. 服务端:如果与客户端连接成功,服务器会触发newConnection信号;
  4. 客户端:如果与服务器连接成功,客户端会触发connected信号;
  5. 当断开连接,客户端的通信套接字会触发disconnected信号,服务器的通信套接字也会触发disconnected信号;
  6. 服务器:触发newConnection信号后,必须在槽函数中实例化QTcpSocket对象;
  7. 服务端与客户端都是通过自己的通信套接字使用wirte函数进行发送信息;
  8. 服务端与客户端接收到对方发过来的消息时,都会触发readyRead信号,然后就可以在对应槽函数做接受处理;
  9. 服务端与客户端的断开都是使用通信套接字调用disconnectFromHost函数进行断开处理。

好了,了解清楚这些内容后,写代码就好理解了!

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


1). 服务端

简单写下操作过程,详细请看下方全部代码!

服务端用到的信号:
newConnection、readyRead、disconnected

  1. 实例化监听套接字 与 进行监听

    // 监听套接字,指定父对象,让其自动回收空间
    tcpServer = new QTcpServer(this);
    
    // 监听
    tcpServer->listen(QHostAddress::AnyIPv4, 8888);
    

    参数一:指定连接的地址类型;如:QHostAddress::AnyIPv4 或者 QHostAddress::AnyIPv6 或者 QHostAddress::Any等
    参数二:端口号。此指定的端口号作为客户端连接的依据之一。

  2. 连接成功后,实例化通信套接字

    // 取出建立好链接的套接字
    tcpSocket = tcpServer->nextPendingConnection();	// 实例化服务端的通信套接字
    
    // 获取对方的IP和端口
    QString ip = tcpSocket->peerAddress().toString();
    qint16 port = tcpSocket->peerPort();
    
  3. 给客户端发送数据

    // 给对方发送数据
    QString str = "你好呀!";
    tcpSocket->write(str.toUtf8().data());
    

    因为有中文,所以需要utf8进行修饰,然后转换为const char *类型进行发送!也可以说是QByteArray 类型。

  4. 接收数据

    // 从通信套接字中读取内容
    QByteArray arrays = tcpSocket->readAll();	// 读取所有内容
    

    最简单的就是使用readAll方法一下子读取客户端发来的全部内容。

  5. 与客户端断开连接

    // 主动和客户端断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
    
  6. 服务器取消监听

    tcpServer ->close();
    
  7. 信号与槽的连接

    // 当有客户端连接过来后会触发newConnection信号
    connect(tcpServer, &QTcpServer::newConnection, this, &_QTcpServer::ConnectSucceed);
    
    // 当有消息发送过来,对方的通信套接字会触发readyRead信号
    connect(tcpSocket, &QTcpSocket::readyRead, this, &_QTcpServer::ReceiveInformation);
    
    // 当服务器主动断开连接会触发disconnected信号
    connect(tcpSocket, &QTcpSocket::disconnected, this, &_QTcpServer::DisConnect);
    

ui界面:
在这里插入图片描述
全部代码实现(VS代码):
_QTcpServer.h文件

#pragma once

#include <QtWidgets/QWidget>
#include "ui_QTcpServer.h"

#include <QTcpSocket>	// 通信套接字
#include <QTcpServer>	// 监听套接字

#pragma execution_character_set("utf-8") // qt支持显示中文

class _QTcpServer : public QWidget {
	Q_OBJECT

public:
	_QTcpServer(QWidget *parent = Q_NULLPTR);


private slots:
	void ConnectSucceed();
	void ReceiveInformation();
	void DisConnect();
	void on_btnSend_clicked();
	void on_btnClose_clicked();

private:
	Ui::QTcpServerClass ui;

	QTcpServer *tcpServer;		// 监听套接字
	QTcpSocket *tcpSocket;		// 通信套接字
};

_QTcpServer.cpp文件

#include "_QTcpServer.h"

_QTcpServer::_QTcpServer(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);

	tcpServer = Q_NULLPTR;
	tcpSocket = Q_NULLPTR;


	// 监听套接字,指定父对象,让其自动回收空间
	tcpServer = new QTcpServer(this);

	tcpServer->listen(QHostAddress::AnyIPv4, 8888);

	setWindowTitle("服务器   端口号:8888");

	// 当有客户端连接过来后会触发newConnection信号
	connect(tcpServer, &QTcpServer::newConnection, this, &_QTcpServer::ConnectSucceed);
} 



void _QTcpServer::ConnectSucceed() {
	// 取出建立好链接的套接字
	tcpSocket = tcpServer->nextPendingConnection();

	// 获取对方的IP和端口
	QString ip = tcpSocket->peerAddress().toString();
	qint16 port = tcpSocket->peerPort();

	QString temp = QString("[%1:%2]:成功连接...").arg(ip).arg(port);
	ui.txtEditRead->setText(temp);


	// 当有消息发送过来,对方的通信套接字会触发readyRead信号
	connect(tcpSocket, &QTcpSocket::readyRead, this, &_QTcpServer::ReceiveInformation);

	// 当服务器主动断开连接会触发disconnected信号
	connect(tcpSocket, &QTcpSocket::disconnected, this, &_QTcpServer::DisConnect);
}


void _QTcpServer::ReceiveInformation() {
	// 从通信套接字中读取内容
	QByteArray arrays = tcpSocket->readAll();

	// 追加设置内容
	ui.txtEditRead->append(arrays);
}

void _QTcpServer::DisConnect() {
	//ui.txtEditRead->append("客户端主动断开连接...");
}

void _QTcpServer::on_btnSend_clicked() {
	if (tcpSocket == Q_NULLPTR) {
		// 追加设置内容
		ui.txtEditRead->append("未与客户端连接...");
		return;
	}


	// 获取编辑区内容
	QString str = ui.txtEditWirte->toPlainText();
	ui.txtEditWirte->clear();

	// 给对方发送数据
	tcpSocket->write(str.toUtf8().data());
}

void _QTcpServer::on_btnClose_clicked() {
	if (tcpSocket == Q_NULLPTR) {
		// 追加设置内容
		ui.txtEditRead->append("未与客户端连接...");
		return;
	}

	// 主动和客户端断开连接
	tcpSocket->disconnectFromHost();
	tcpSocket->close();
	tcpSocket = Q_NULLPTR;
	ui.txtEditRead->append("与客户端断开连接...");
}

2). 客户端

简单写下操作过程,详细请看下方全部代码!

客户端用到的信号:
connected、readyRead、disconnected

  1. 实例化通信套接字

    // 分配控件指定父对象
    tcpSocket = new QTcpSocket(this);
    
  2. 与服务器进行连接

    // 获取服务器IP和端口
    qint16 port = 8888;
    QString ip = "127.0.0.1";
    
    // 主动和服务器建立连接
    tcpSocket->connectToHost(QHostAddress(ip), port);
    

    记得,参数一,需要使用QHostAddress进行修饰!

  3. 发送数据

    // 发送信息
    QString str = "你也好!";
    tcpSocket->write(str.toUtf8().data());
    
  4. 读取服务器发送过来的数据

    // 读取服务器发送过来的消息
    QByteArray arrays = tcpSocket->readAll();
    
  5. 主动与服务器断开连接

    // 主动与服务器断开连接
    tcpSocket->disconnectFromHost();
    tcpSocket->close();
    
  6. 信号与槽的连接

    // 成功与服务器连接会触发connected信号
    connect(tcpSocket, &QTcpSocket::connected, this, &_QTcpSocket::SuccessfulConnection);
    
    // 当有消息发送过来,通信套接字会触发readyRead信号
    connect(tcpSocket, &QTcpSocket::readyRead, this, &_QTcpSocket::ReceiveInformation);
    
    // 当服务器主动断开连接会触发disconnected信号
    connect(tcpSocket, &QTcpSocket::disconnected, this, &_QTcpSocket::DisConnect);
    

ui界面:
在这里插入图片描述
全部代码实现(VS代码):

_QTcpSocket.h文件

#pragma once

#include <QWidget>
#include "ui__QTcpSocket.h"

#include <QTcpSocket>	// 通信套接字

#pragma execution_character_set("utf-8") // qt支持显示中文

class _QTcpSocket : public QWidget {
	Q_OBJECT

public:
	_QTcpSocket(QWidget *parent = Q_NULLPTR);
	~_QTcpSocket();


private slots:
	void on_btnConnect_clicked();
	void on_btnSend_clicked();
	void on_btnClose_clicked();
	void SuccessfulConnection();
	void ReceiveInformation();
	void DisConnect();

private:
	Ui::_QTcpSocket ui;

	QTcpSocket *tcpSocket;
};

_QTcpSocket.cpp文件

#include "_QTcpSocket.h"

#include <QHostAddress>

_QTcpSocket::_QTcpSocket(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);

	tcpSocket = Q_NULLPTR;

	setWindowTitle("客户端");

	// 分配控件指定父对象
	tcpSocket = new QTcpSocket(this);

	// 成功与服务器连接会触发connected信号
	connect(tcpSocket, &QTcpSocket::connected, this, &_QTcpSocket::SuccessfulConnection);
	
	// 当有消息发送过来,通信套接字会触发readyRead信号
	connect(tcpSocket, &QTcpSocket::readyRead, this, &_QTcpSocket::ReceiveInformation);

	// 当服务器主动断开连接会触发disconnected信号
	connect(tcpSocket, &QTcpSocket::disconnected, this, &_QTcpSocket::DisConnect);

}

_QTcpSocket::~_QTcpSocket() {}

void _QTcpSocket::on_btnSend_clicked() {
	// 获取输入框中的内容
	QString str = ui.txtWirte->toPlainText();
	ui.txtWirte->clear();


	// 发送信息
	tcpSocket->write(str.toUtf8().data());
}

void _QTcpSocket::on_btnClose_clicked() {
	// 主动与服务器断开连接
	tcpSocket->disconnectFromHost();
	tcpSocket->close();

	ui.txtRead->append("与服务器断开连接...");
}

void _QTcpSocket::SuccessfulConnection() {
	ui.txtRead->append("成功与服务器连接...");
}

void _QTcpSocket::ReceiveInformation() {
	// 读取服务器发送过来的消息
	QByteArray arrays = tcpSocket->readAll();

	// 追加显示
	ui.txtRead->append(arrays);
}

void _QTcpSocket::DisConnect() {
	if (ui.txtRead == Q_NULLPTR) {
		return;
	}
	//ui.txtRead->append("服务器与你断开连接...");
}

void _QTcpSocket::on_btnConnect_clicked() {
	// 获取服务器IP和端口
	qint16 port = ui.lineEditPort->text().toInt();
	QString ip = ui.lineEditIP->text();

	// 主动和服务器建立连接
	tcpSocket->connectToHost(QHostAddress(ip), port);
}

2. 发送文件

发送文件,通信流程与发送文字差不多,差别就是一些执行步骤需要注意的。

由于TCP固有的毛病,粘包,所以这里列举了两种方式解决粘包的问题!

1). 定时器方式解决粘包问题

看发送文件流程图
在这里插入图片描述

  1. 发送方选择一个文件,获取文件的大小和文件名;

    QString filePath = QFileDialog::getOpenFileName(this, "open", "../");
    // 获取文件信息
    QFileInfo info(filePath);
    fileName = info.fileName();
    fileSize = info.size();
    
  2. 发送方将文件大小和文件名组成一个包发送给接收方;

    // 先发送文件头信息  文件名##文件大小
    QString head = QString("%1##%2").arg(fileName).arg(fileSize);
    // 发送头部信息
    qint64 len = tcpSocket->write(head.toUtf8().data());
    
  3. 接收方接收文件信息包,解析获得文件大小和文件名字信息;在本地创建一个名字一样的文件;

    // 取出接收的内容
    QByteArray buf = tcpSocket->readAll();
    
    // 解析头部信息
    fileName = QString(buf).section("##", 0, 0);
    fileSize = QString(buf).section("##", 1, 1).toInt();
    
    // 创建一个文件
    file.setFileName(fileName);
    bool isOk = file.open(QIODevice::WriteOnly);
    
  4. 发送方使用定时器延时20毫秒;

    // 防止TCP粘包文件
    // 需要通过定时器延时20毫秒
    timer->start(20);	/* 解决粘包,方式一,使用定时器 */
    
  5. 发送发读取文件数据,发送给接收方,读多少发多少;

    // 每次发送数据的大小
    char buf[4 * 1024] = { 0 };
    len = 0;
    
    // 往文件中读数据
    len = file.read(buf, sizeof(buf));
    
    // 发送数据,读多少,发多少
    len = tcpSocket->write(buf, len);		
    
  6. 接收方接收文件数据,对方发多少,接收多少;

    // 取出接收的内容
    QByteArray buf = tcpSocket->readAll();
    
  7. 接收方将接收到的文件数据写入文件中;

    // 接收的文件数据写入文件
    qint64 len = file.write(buf);
    
  8. 发送方判断是否发送文件完毕;

    // 是否发送文件完毕
    if (sendSize == fileSize) {
    	QMessageBox::information(this, "提示", QString("发送文件完毕! 文件大小:%1").arg(sendSize));
    	file.close();
    
    	// 把客户端断开
    	tcpSocket->disconnectFromHost();
    	tcpSocket->close();
    }
    
  9. 接收方判断是否接收文件完毕。

    // 当两个值相等,说明文件接收完毕
    if (recvSize == fileSize) {
    	file.close();
    	QMessageBox::information(this, "提示", "文件接收完毕!");
    	isStart = true;
    
    	tcpSocket->disconnectFromHost();
    	tcpSocket->close();
    }
    

好了,了解清楚这些内容后,写代码就好理解了!

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

全部代码实现(VS代码):

服务端:QTcpSendFile.h

#pragma once

#include <QtWidgets/QWidget>
#include "ui_QTcpSendFile.h"

#include <QTcpServer>	// 监听套接字
#include <QTcpSocket>	// 通信套接字

#include <QFile>
#include <QTimer>

#pragma execution_character_set("utf-8") // qt支持显示中文

class QTcpSendFile : public QWidget {
	Q_OBJECT

public:
	QTcpSendFile(QWidget *parent = Q_NULLPTR);

private slots:
	void on_btnFile_clicked();
	void on_btnSend_clicked();
	//void on_sendFile();

private:
	void sendData();	// 发送文件数据

private:
	Ui::QTcpSendFileClass ui;

	QTcpServer *tcpServer;
	QTcpSocket *tcpSocket;

	QFile file;		// 文件对象
	QString fileName;	// 文件名字
	qint64 fileSize;	// 文件大小
	qint64 sendSize;	// 已经发送文件的大小
	QTimer *timer;		// 定时器
};

服务端:QTcpSendFile.cpp

#include "QTcpSendFile.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFileInfo>

QTcpSendFile::QTcpSendFile(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);


	ui.btnFile->setEnabled(false);
	ui.btnSend->setEnabled(false);

	// 监听套接字实例化对象
	tcpServer = new QTcpServer(this);

	// 监听
	tcpServer->listen(QHostAddress::AnyIPv4, 8888);
	setWindowTitle("服务器端口号为:8888");

	// 客户端成功和服务器连接,tcpServer会自动触发 newConnection()
	connect(tcpServer, &QTcpServer::newConnection, [=]() {
		// 取出建立好连接的套接字
		tcpSocket = tcpServer->nextPendingConnection();

		// 获取对方的ip和端口号
		QString ip = tcpSocket->peerAddress().toString();
		quint16 port = tcpSocket->peerPort();

		QString str = QString("[%1:%2] 成功连接!").arg(ip).arg(port);
		ui.textEdit->setText(str);	// 显示到编辑区

		ui.btnFile->setEnabled(true);

		//connect(tcpSocket, &QTcpSocket::readyRead, this, &QTcpSendFile::on_sendFile);
	});


	timer = new QTimer(this);

	// 定时器方式解决TCP粘包问题
	connect(timer, &QTimer::timeout, [=]() {
		// 关闭定时器
		timer->stop();
		
		// 发送文件
		sendData();
	});
}


void QTcpSendFile::on_btnFile_clicked() {
	QString filePath = QFileDialog::getOpenFileName(this, "open", "../");
	if (filePath.isEmpty() == false) {
		fileName.clear();
		fileSize = 0;

		// 获取文件信息
		QFileInfo info(filePath);
		fileName = info.fileName();
		fileSize = info.size();

		sendSize = 0;	// 发送文件的大小

		// 只读方式打开文件
		// 指定文件的名字
		file.setFileName(filePath);

		// 打开文件
		bool isOK = file.open(QIODevice::ReadOnly);
		if (isOK == false) {
			QMessageBox::information(this, "提示", "只读方式打开文件失败!");
			return;
		}

		// 提示打开文件的路径
		ui.textEdit->append(filePath);

		// 发送按钮放开
		ui.btnSend->setEnabled(true);

	} else {
		QMessageBox::information(this, "提示", "选择文件路径出错!");
	}


}


// 发送文件按钮
void QTcpSendFile::on_btnSend_clicked() {
	// 先发送文件头信息  文件名##文件大小
	QString head = QString("%1##%2").arg(fileName).arg(fileSize);
	// 发送头部信息
	qint64 len = tcpSocket->write(head.toUtf8().data());
	// 如果大于零,头部信息发送成功
	if (len > 0) {
		// 发送真正的文件信息
		// 防止TCP粘包文件
		// 需要通过定时器延时20毫秒
		timer->start(20);	/* 解决粘包,方式一,使用定时器 */


	} else {
		QMessageBox::information(this, "提示", "头部信息发送失败!");
		file.close();
		ui.btnSend->setEnabled(false);

		return;
	}
}

/* 解决粘包,方式二,当收到对方发送过来的特殊标志时,进行发送文件数据 */
//void QTcpSendFile::on_sendFile() {
//	QByteArray arrays = tcpSocket->readAll();
//
//	if (arrays == "send file") {
//		sendData();
//	}
//
//}

void QTcpSendFile::sendData() {
	qint64 len = 0;

	do {
		// 每次发送数据的大小
		char buf[4 * 1024] = { 0 };
		len = 0;

		// 往文件中读数据
		len = file.read(buf, sizeof(buf));

		// 发送数据,读多少,发多少
		len = tcpSocket->write(buf, len);
		
		// 发送的数据需要累计
		sendSize += len;

	} while (len > 0);

	// 是否发送文件完毕
	if (sendSize == fileSize) {
		QMessageBox::information(this, "提示", QString("发送文件完毕! 文件大小:%1").arg(sendSize));
		file.close();

		// 把客户端断开
		tcpSocket->disconnectFromHost();
		tcpSocket->close();
	}
}

客户端:ClientWidget.h

#pragma once

#include <QWidget>
#include "ui_ClientWidget.h"

#include <QTcpSocket>
#include <QFile>

#pragma execution_character_set("utf-8") // qt支持显示中文

class ClientWidget : public QWidget {
	Q_OBJECT

public:
	ClientWidget(QWidget *parent = Q_NULLPTR);
	~ClientWidget();

private slots:
	void on_recvData();

	void on_btnConnect_clicked();

private:
	Ui::ClientWidget ui;

	QTcpSocket *tcpSocket;
	QFile file;			// 文件对象
	QString fileName;	// 文件名字
	qint64 fileSize;	// 文件大小
	qint64 recvSize;	// 已经接收文件的大小

	bool isStart;
};

客户端:ClientWidget.cpp

#include "ClientWidget.h"
#include <QMessageBox>
#include <QHostAddress>

ClientWidget::ClientWidget(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);

	isStart = true;
	tcpSocket = new QTcpSocket(this);

	connect(tcpSocket, &QTcpSocket::readyRead, this, &ClientWidget::on_recvData);
}

ClientWidget::~ClientWidget() {}

void ClientWidget::on_btnConnect_clicked() {
	// 获取ip和端口号
	QString ip = ui.lineEditIP->text();
	qint16 port = ui.lineEditPort->text().toInt();

	tcpSocket->connectToHost(QHostAddress(ip), port);
}


void ClientWidget::on_recvData() {
	// 取出接收的内容
	QByteArray buf = tcpSocket->readAll();


	if (isStart == true) {
		// 接收头
		isStart = false;

		// 解析头部信息
		fileName = QString(buf).section("##", 0, 0);
		fileSize = QString(buf).section("##", 1, 1).toInt();
		recvSize = 0;


		// 打开文件
		file.setFileName(fileName);

		bool isOk = file.open(QIODevice::WriteOnly);
		if (isOk == false) {
			QMessageBox::information(this, "提示", "打开文件失败!");
			return;
		}

		// 给服务器发给送特定字符串,服务器收到后会发送文件数据过来
		//tcpSocket->write("send file");

		ui.progressBar->setMinimum(0);	// 最小值
		ui.progressBar->setMaximum(fileSize / 1024);	// 最大值
		ui.progressBar->setValue(0);	// 当前值


	} else {
		// 接收的文件数据写入文件
		qint64 len = file.write(buf);
		if (len > 0) {
			recvSize += len;
		}

		// 给进度条设置值
		ui.progressBar->setValue(recvSize / 1024);

		// 当两个值相等,说明文件接收完毕
		if (recvSize == fileSize) {
			file.close();
			QMessageBox::information(this, "提示", "文件接收完毕!");
			isStart = true;

			tcpSocket->disconnectFromHost();
			tcpSocket->close();
		}
	}
}

2). 回复信息方式解决粘包问题

看发送文件流程图
在这里插入图片描述

  1. 发送方选择一个文件,获取文件的大小和文件名;

    QString filePath = QFileDialog::getOpenFileName(this, "open", "../");
    // 获取文件信息
    QFileInfo info(filePath);
    fileName = info.fileName();
    fileSize = info.size();
    
  2. 发送方将文件大小和文件名组成一个包发送给接收方;

    // 先发送文件头信息  文件名##文件大小
    QString head = QString("%1##%2").arg(fileName).arg(fileSize);
    // 发送头部信息
    qint64 len = tcpSocket->write(head.toUtf8().data());
    
  3. 接收方接收文件信息包,解析获得文件大小和文件名字信息;在本地创建一个名字一样的文件;

    // 取出接收的内容
    QByteArray buf = tcpSocket->readAll();
    
    // 解析头部信息
    fileName = QString(buf).section("##", 0, 0);
    fileSize = QString(buf).section("##", 1, 1).toInt();
    
    // 创建一个文件
    file.setFileName(fileName);
    bool isOk = file.open(QIODevice::WriteOnly);
    
  4. 接收方给发送方发送一条特定的信息;

    // 给服务器发给送特定字符串,服务器收到后会发送文件数据过来
    tcpSocket->write("send file");
    
  5. 发送方接收信息,判断是否是特定信息;

    QByteArray arrays = tcpSocket->readAll();
    
    if (arrays == "send file") {
    	sendData();	// 发送文件数据
    }
    
  6. 如果是,发送发读取文件数据,发送给接收方,读多少发多少;

    // 每次发送数据的大小
    char buf[4 * 1024] = { 0 };
    len = 0;
    
    // 往文件中读数据
    len = file.read(buf, sizeof(buf));
    
    // 发送数据,读多少,发多少
    len = tcpSocket->write(buf, len);		
    
  7. 接收方接收文件数据,对方发多少,接收多少;

    // 取出接收的内容
    QByteArray buf = tcpSocket->readAll();
    
  8. 接收方将接收到的文件数据写入文件中;

    // 接收的文件数据写入文件
    qint64 len = file.write(buf);
    
  9. 发送方判断是否发送文件完毕;

    // 是否发送文件完毕
    if (sendSize == fileSize) {
    	QMessageBox::information(this, "提示", QString("发送文件完毕! 文件大小:%1").arg(sendSize));
    	file.close();
    
    	// 把客户端断开
    	tcpSocket->disconnectFromHost();
    	tcpSocket->close();
    }
    
  10. 接收方判断是否接收文件完毕。

    // 当两个值相等,说明文件接收完毕
    if (recvSize == fileSize) {
    	file.close();
    	QMessageBox::information(this, "提示", "文件接收完毕!");
    	isStart = true;
    
    	tcpSocket->disconnectFromHost();
    	tcpSocket->close();
    }
    

好了,了解清楚这些内容后,写代码就好理解了!

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

全部代码实现(VS代码):

服务端:QTcpSendFile.h

#pragma once

#include <QtWidgets/QWidget>
#include "ui_QTcpSendFile.h"

#include <QTcpServer>	// 监听套接字
#include <QTcpSocket>	// 通信套接字

#include <QFile>
#include <QTimer>

#pragma execution_character_set("utf-8") // qt支持显示中文

class QTcpSendFile : public QWidget {
	Q_OBJECT

public:
	QTcpSendFile(QWidget *parent = Q_NULLPTR);

private slots:
	void on_btnFile_clicked();
	void on_btnSend_clicked();
	void on_sendFile();

private:
	void sendData();	// 发送文件数据

private:
	Ui::QTcpSendFileClass ui;

	QTcpServer *tcpServer;
	QTcpSocket *tcpSocket;

	QFile file;		// 文件对象
	QString fileName;	// 文件名字
	qint64 fileSize;	// 文件大小
	qint64 sendSize;	// 已经发送文件的大小
	//QTimer *timer;		// 定时器
};

服务端:QTcpSendFile.cpp

#include "QTcpSendFile.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFileInfo>

QTcpSendFile::QTcpSendFile(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);


	ui.btnFile->setEnabled(false);
	ui.btnSend->setEnabled(false);

	// 监听套接字实例化对象
	tcpServer = new QTcpServer(this);

	// 监听
	tcpServer->listen(QHostAddress::AnyIPv4, 8888);
	setWindowTitle("服务器端口号为:8888");

	// 客户端成功和服务器连接,tcpServer会自动触发 newConnection()
	connect(tcpServer, &QTcpServer::newConnection, [=]() {
		// 取出建立好连接的套接字
		tcpSocket = tcpServer->nextPendingConnection();

		// 获取对方的ip和端口号
		QString ip = tcpSocket->peerAddress().toString();
		quint16 port = tcpSocket->peerPort();

		QString str = QString("[%1:%2] 成功连接!").arg(ip).arg(port);
		ui.textEdit->setText(str);	// 显示到编辑区

		ui.btnFile->setEnabled(true);

		connect(tcpSocket, &QTcpSocket::readyRead, this, &QTcpSendFile::on_sendFile);
	});


	//timer = new QTimer(this);

	// 定时器方式解决TCP粘包问题
	//connect(timer, &QTimer::timeout, [=]() {
	//	// 关闭定时器
	//	timer->stop();
	//	
	//	// 发送文件
	//	sendData();
	//});
}


void QTcpSendFile::on_btnFile_clicked() {
	QString filePath = QFileDialog::getOpenFileName(this, "open", "../");
	if (filePath.isEmpty() == false) {
		fileName.clear();
		fileSize = 0;

		// 获取文件信息
		QFileInfo info(filePath);
		fileName = info.fileName();
		fileSize = info.size();

		sendSize = 0;	// 发送文件的大小

		// 只读方式打开文件
		// 指定文件的名字
		file.setFileName(filePath);

		// 打开文件
		bool isOK = file.open(QIODevice::ReadOnly);
		if (isOK == false) {
			QMessageBox::information(this, "提示", "只读方式打开文件失败!");
			return;
		}

		// 提示打开文件的路径
		ui.textEdit->append(filePath);

		// 发送按钮放开
		ui.btnSend->setEnabled(true);

	} else {
		QMessageBox::information(this, "提示", "选择文件路径出错!");
	}


}


// 发送文件按钮
void QTcpSendFile::on_btnSend_clicked() {
	// 先发送文件头信息  文件名##文件大小
	QString head = QString("%1##%2").arg(fileName).arg(fileSize);
	// 发送头部信息
	qint64 len = tcpSocket->write(head.toUtf8().data());
	// 如果大于零,头部信息发送成功
	if (len > 0) {
		// 发送真正的文件信息
		// 防止TCP粘包文件
		// 需要通过定时器延时20毫秒
		//timer->start(20);	/* 解决粘包,方式一,使用定时器 */


	} else {
		QMessageBox::information(this, "提示", "头部信息发送失败!");
		file.close();
		ui.btnSend->setEnabled(false);

		return;
	}
}

/* 解决粘包,方式二,当收到对方发送过来的特殊标志时,进行发送文件数据 */
void QTcpSendFile::on_sendFile() {
	QByteArray arrays = tcpSocket->readAll();	

	if (arrays == "send file") {
		sendData();	// 发送文件数据
	}

}

void QTcpSendFile::sendData() {
	qint64 len = 0;

	do {
		// 每次发送数据的大小
		char buf[4 * 1024] = { 0 };
		len = 0;

		// 往文件中读数据
		len = file.read(buf, sizeof(buf));

		// 发送数据,读多少,发多少
		len = tcpSocket->write(buf, len);
		
		// 发送的数据需要累计
		sendSize += len;

	} while (len > 0);

	// 是否发送文件完毕
	if (sendSize == fileSize) {
		QMessageBox::information(this, "提示", QString("发送文件完毕! 文件大小:%1").arg(sendSize));
		file.close();

		// 把客户端断开
		tcpSocket->disconnectFromHost();
		tcpSocket->close();
	}
}

客户端:ClientWidget.h

#pragma once

#include <QWidget>
#include "ui_ClientWidget.h"

#include <QTcpSocket>
#include <QFile>

#pragma execution_character_set("utf-8") // qt支持显示中文

class ClientWidget : public QWidget {
	Q_OBJECT

public:
	ClientWidget(QWidget *parent = Q_NULLPTR);
	~ClientWidget();

private slots:
	void on_recvData();

	void on_btnConnect_clicked();

private:
	Ui::ClientWidget ui;

	QTcpSocket *tcpSocket;
	QFile file;			// 文件对象
	QString fileName;	// 文件名字
	qint64 fileSize;	// 文件大小
	qint64 recvSize;	// 已经接收文件的大小

	bool isStart;	// 判断接收的头部信息还是文件数据
};

客户端:ClientWidget.cpp

#include "ClientWidget.h"
#include <QMessageBox>
#include <QHostAddress>

ClientWidget::ClientWidget(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);

	isStart = true;
	tcpSocket = new QTcpSocket(this);

	connect(tcpSocket, &QTcpSocket::readyRead, this, &ClientWidget::on_recvData);
}

ClientWidget::~ClientWidget() {}

void ClientWidget::on_btnConnect_clicked() {
	// 获取ip和端口号
	QString ip = ui.lineEditIP->text();
	qint16 port = ui.lineEditPort->text().toInt();

	tcpSocket->connectToHost(QHostAddress(ip), port);
}


void ClientWidget::on_recvData() {
	// 取出接收的内容
	QByteArray buf = tcpSocket->readAll();


	if (isStart == true) {
		// 接收头
		isStart = false;

		// 解析头部信息
		fileName = QString(buf).section("##", 0, 0);
		fileSize = QString(buf).section("##", 1, 1).toInt();
		recvSize = 0;


		// 打开文件
		file.setFileName(fileName);

		bool isOk = file.open(QIODevice::WriteOnly);
		if (isOk == false) {
			QMessageBox::information(this, "提示", "打开文件失败!");
			return;
		}

		// 给服务器发给送特定字符串,服务器收到后会发送文件数据过来
		tcpSocket->write("send file");

		ui.progressBar->setMinimum(0);	// 最小值
		ui.progressBar->setMaximum(fileSize / 1024);	// 最大值
		ui.progressBar->setValue(0);	// 当前值


	} else {
		// 接收的文件数据写入文件
		qint64 len = file.write(buf);
		if (len > 0) {
			recvSize += len;
		}

		// 给进度条设置值
		ui.progressBar->setValue(recvSize / 1024);

		// 当两个值相等,说明文件接收完毕
		if (recvSize == fileSize) {
			file.close();
			QMessageBox::information(this, "提示", "文件接收完毕!");
			isStart = true;

			tcpSocket->disconnectFromHost();
			tcpSocket->close();
		}
	}
}

三、UDP通信

发送文字

首先你得先了解QtUDP通信的流程,下面将根据下图进行讲解:
在这里插入图片描述

  1. 无论是服务端还是客户端都无需进行连接;
  2. 使用通信套接字进行绑定ip地址类型与端口号;
  3. 使用writeDatagram给对方放松数据;
  4. 当接收到对方发送过来的数据时,会自动触发readyRead()信号;因此可以在槽函数中做接收处理;
  5. 使用readDatagram读取对方发送过来的数据;
  6. UDP通信无需断开连接;
  7. 当需要多播组播时,使用joinMulticastGroup进行加入某个组播中,参数是D类地址;
  8. 当需要离开多播组播时,使用leaveMulticastGroup进行离开操作,参数是自己的IP地址。

好了,了解清楚这些内容后,写代码就好理解了!

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

服务端和客户端都是一样的代码,一样的界面

所以下面只是展示注释比较详细一的一版代码

简单写下操作过程,详细请看下方全部代码!

所用到的信号:
readyRead

  1. 实例化通信套接字

    udpSocket = new QUdpSocket(this);
    
  2. 绑定IP类型与端口

    udpSocket->bind(QHostAddress::AnyIPv4, 8888);
    
  3. 如需加入某个组播则加入,无需则跳过,根据自己项目需求

    /* 加入某个组播,组播地址是D类地址 */
    udpSocket->joinMulticastGroup(QHostAddress("224.0.0.2"));
    //udpSocket->leaveMulticastGroup(QHostAddress("127.0.0.1"));	// 离开组播,填写自己的ip地址
    
  4. 发送消息

    // 获取ip和端口
    QString ip = ui.lineEditIP->text();
    qint16 port = ui.lineEditPort->text().toInt();
    
    // 发送数据
    udpSocket->writeDatagram(str.toUtf8().data(), QHostAddress(ip), port);
    

    注意:发送消息,需要指定对方ip和端口号!

  5. 接收消息

    // 读取对方发送过来的内容
    char buf[1024] = { 0 };
    QHostAddress cliAddr;	// 对方地址
    quint16 port;	// 对方端口
    
    qint64 len = udpSocket->readDatagram(buf, sizeof(buf), &cliAddr, &port);
    

ui界面:
在这里插入图片描述
全部代码实现(VS代码):

_QUdpSocket.h文件

#pragma once

#include <QtWidgets/QWidget>
#include "ui__QUdpSocket.h"

#include <QUdpSocket>	// UDP通信套接字

#pragma execution_character_set("utf-8")	// vs中qt支持中文

class _QUdpSocket : public QWidget {
	Q_OBJECT

public:
	_QUdpSocket(QWidget *parent = Q_NULLPTR);

private slots:
	void on_btnSend_clicked();
	void on_btnClose_clicked();
	void on_dealMsg();		// 处理对方发送过来的数据

private:
	Ui::_QUdpSocketClass ui;

	QUdpSocket *udpSocket;

};

_QUdpSocket.cpp文件

#include "_QUdpSocket.h"

#include <qhostaddress.h>


_QUdpSocket::_QUdpSocket(QWidget *parent)
	: QWidget(parent) {
	ui.setupUi(this);


	// 分配空间
	udpSocket = new QUdpSocket(this);


	// 绑定
	udpSocket->bind(QHostAddress::AnyIPv4, 8888);


	/* 加入某个组播,组播地址是D类地址 */
	udpSocket->joinMulticastGroup(QHostAddress("224.0.0.2"));
	//udpSocket->leaveMulticastGroup(QHostAddress("127.0.0.1"));	// 离开组播,填写自己的ip地址


	// 当对方成功发送数据过来
	// 自动触发 readyRead()信号
	connect(udpSocket, &QUdpSocket::readyRead, this, &_QUdpSocket::on_dealMsg);



	setWindowTitle("端口为:8888");

}

void _QUdpSocket::on_btnSend_clicked() {
	// 获取发送内容
	QString str = ui.txtWrite->toPlainText();
	// 内容为空,没必要发送
	if (str.isEmpty()) {
		return;
	}

	// 获取ip和端口
	QString ip = ui.lineEditIP->text();
	qint16 port = ui.lineEditPort->text().toInt();

	// 发送数据
	udpSocket->writeDatagram(str.toUtf8().data(), QHostAddress(ip), port);
}

void _QUdpSocket::on_btnClose_clicked() {
	this->close();
}

void _QUdpSocket::on_dealMsg() {
	// 读取对方发送过来的内容
	char buf[1024] = { 0 };
	QHostAddress cliAddr;	// 对方地址
	quint16 port;	// 对方端口
	
	// 读取对方发送过来的数据
	qint64 len = udpSocket->readDatagram(buf, sizeof(buf), &cliAddr, &port);
	if (len > 0) {
		// 格式化 [地址:端口]内容
		QString str = QString("[%1:%2]:%3").arg(cliAddr.toString()).arg(port).arg(buf);

		// 给显示区设置内容
		ui.txtRead->append(str);
	}
}

服务端代码一样,这里就不展示了!


四、总结

Socket编程到此就结束了,首先看我画的图,将图中的流程搞懂了,再研究代码就很轻松了!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

cpp_learners

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

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

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

打赏作者

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

抵扣说明:

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

余额充值