目录
- 前言
- 正文
- 一、TCP
- 二、UDP
- 1、基本流程
- 2、必备知识
- 三、代码层级
- 1、UDP服务端
- END、总结的知识与问题
- 1、如何获取QByteArray中某一字节的数据,并将其转为十进制?
- 2、如何以本年本月本日为基础,获取时间戳,而不以1970为基础?
- 3、如何将一个四个字节组成的数拆分成1个字节一个字节的?
- 4、如何对前面的所有字节进行异或校验?
- 5、如何将QByteArray中的某个字节转为十六进制?
- 参考
前言
恰好,有个项目需要用到UDP,之前使用比较多的是TCP,UDP的还是第一次搞,但是感觉流程也是差不多,甚至比TCP要更简单就行,这里就稍微做一下总结,写一下自己需要注意的点,以及从网上查找到的,比较有用的一些资料。本篇文章,主要偏向的还是UDP,TCP的话,大家可以稍微看下,当做过下眼。
正文
一、TCP
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的传输协议,它是OSI(Open System Interconnection,开放式系统互联)模型中的第四层协议,通常使用于网络中的应用层和传输层之间。
TCP建立连接主要就是三次握手、断开连接主要就是四次挥手。
三次握手的基本流程如下:
四次挥手的基本流程如下:
关于TCP Socket 的实现逻辑如下:
本篇文章关于TCP就点到为止了。下面就讲下UDP了。
二、UDP
1、基本流程
UDP(User Data Protocol),用户数据报协议,是一种简单轻量级、不可靠、面向数据报、无连接的传输层协议,可以应用在可靠性不是十分重要的场合,如短消息、广播信息等。
适用于以下几种情况:
A、网络数据大多为短消息。
B、拥有大量客户端
C、对数据安全性无特殊要求
D、网络负担非常重,但对响应速度要求高。
这部分内容,应该有一部分难点在于UDP的拆包上面,但由于目前解除的项目不需要拆包,就暂不考虑这个问题了。
UDP编程的流程:
2、必备知识
1、数据报的长度一般不少于512字节,每个数据报包含发送者和接收者的IP地址和端口等信息。
2、单播、广播、组播的知识
单播(unicast)模式:一个UDP客户端发出的数据报只发送到另一个指定地址和端口的UDP客户端,是一对一的数据传输。
广播(broadcast)模式:一个UDP客户端发出的数据报,在同一网络范围内其他所有的UDP客户端都可以收到。QUdpSocket支持IPv4广播。广播经常用于实现网络发现的协议。要获取广播数据只需在数据报中指定接收端地址为QHostAddress::Broadcast,一般的广播地址为255.255.255.255。
组播(multicast)模式:也称为多播。UDP客户端加入到另一组播IP地址指定的多播组,成员向多组播地址发送的数据报组内成员都可以接收到,类似于QQ群的功能。
-
单播(Unicast):单播是一种点对点的通信方式,其中一个发送方(源)向一个接收方(目标)发送数据。在单播通信中,数据从发送方经过网络传输到指定的接收方,其他设备不会接收到该数据。单播适用于需要将数据传输到特定设备或主机的场景,例如客户端-服务器通信。
-
广播(Broadcast):广播是一种一对多的通信方式,其中一个发送方向局域网中的所有设备发送数据。在广播通信中,数据从发送方通过网络传输到同一局域网中的所有设备。所有接收方都会接收到广播数据。广播适用于需要将数据传输到局域网中的所有设备的场景,例如局域网上的服务发现、网络广告等。
-
广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。
其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。
3、报文大小的限制与各系统的协议实现有关,但不得超过其下层 IP 协议规定的64KB
4、所谓客户端进行广播,是指客户端向一个广播地址255.255.255.255 + 一个指定的服务端监听的端口号 进行发送数据。只要端口号是对的,那么服务端就会接收到数据。
5、如何监听某个端口:netstat -ano | findstr 1024
6、
三、代码层级
1、UDP服务端
该代码是我实现的UDP服务端,只负责接收信息。
UDPServer.h
#ifndef UDPSERVER_H
#define UDPSERVER_H
#include <QObject>
#include <QUdpSocket>
class CUdpServer : public QObject
{
Q_OBJECT
public:
explicit CUdpServer(QObject *parent = nullptr);
~CUdpServer();
bool OpenUdpServer(const int &_iPort);
protected:
bool _WriteDatagram(const QString &_sIp, const int &_iPort, char *_pData, int _iLength);
bool _ResponseFrame(const QString &_sIp, const int &_iPort, const QString &_sCmd, const QByteArray &_oArray);
bool _HandleLoginFrame(const QString &_sIp, const int &_iPort, const QByteArray &_oArray);
private:
void _Init();
uint8_t _XorCheck(uint8_t *pbuf, uint32_t length);
qint64 _GetNowDateTimeStampMs();
public slots:
void on_readyRead();
private:
QUdpSocket* m_pUdpServer = nullptr;
bool m_bOpen = false;
bool m_bDoing = false;
int m_iElectricQuantity = 0;
};
#endif // UDPSERVER_H
UDPServer.cpp
#include "UdpServer.h"
#include <QNetworkDatagram>
#include <QDateTime>
#include <CommonFunc.h>
#include <KldLog.h>
CUdpServer::CUdpServer(QObject *parent):
QObject(parent)
{
_Init();
connect(m_pUdpServer, &QUdpSocket::readyRead, this, &CUdpServer::on_readyRead);
}
CUdpServer::~CUdpServer()
{
}
bool CUdpServer::OpenUdpServer(const int &_iPort)
{
bool bRet = false;
if (false == m_bOpen)
{
bRet = m_pUdpServer->bind(QHostAddress::Any, _iPort);
if (bRet)
{
m_bOpen = true;
}
}
return bRet;
}
bool CUdpServer::_WriteDatagram(const QString &_sIp, const int &_iPort, char *_pData, int _iLength)
{
bool bRet = false;
QByteArray oOutputArray = QByteArray::fromRawData((char*)_pData, _iLength);
qint64 iLen = m_pUdpServer->writeDatagram(oOutputArray, QHostAddress(_sIp), _iPort);
return bRet;
}
bool CUdpServer::_ResponseFrame(const QString &_sIp, const int &_iPort, const QString &_sCmd, const QByteArray &_oArray)
{
bool bRet = false;
int iElectricQuantity = (int)_oArray.at(3);
uchar cArray[16]= {0};
cArray[0] = 0xAF;//帧头
cArray[1] = 0x00;//设备类型
cArray[2] = 0x00;//设备编号
cArray[3] = 0x64;//电池电量
qint64 iTimeStamp = _GetNowDateTimeStampMs();
quint8 byte1 = (iTimeStamp >> 24) & 0xFF;
quint8 byte2 = (iTimeStamp >> 16) & 0xFF;
quint8 byte3 = (iTimeStamp >> 8) & 0xFF;
quint8 byte4 = iTimeStamp & 0xFF;
LOG_INFO << "--->z CUdpServer::_HandleHeartFrame byte1:"<< iTimeStamp<<"||"<<QString::number(iTimeStamp, 16).toStdString();
cArray[4] = (int)byte1;//时间戳
cArray[5] = (int)byte2;//时间戳
cArray[6] = (int)byte3;//时间戳
cArray[7] = (int)byte4;//时间戳
cArray[8] = _oArray.at(8);//唯一ID
cArray[9] = _oArray.at(9);//唯一ID
cArray[10] = _oArray.at(10);//唯一ID
cArray[11] = _oArray.at(11);//唯一ID
bool bOK = false;
cArray[12] = _sCmd.toInt(&bOK, 16);//命令类型
cArray[13] = 0x00;//包序列
cArray[14] = 0x00;//数据长度
cArray[15] = _XorCheck(cArray, sizeof(cArray) - 1);//校验字节
bRet = _WriteDatagram(_sIp, _iPort, (char*)cArray, sizeof(cArray));
return bRet;
}
bool CUdpServer::_HandleLoginFrame(const QString &_sIp, const int &_iPort, const QByteArray &_oArray)
{
bool bRet = false;
if (_oArray.length() < 16)
{
return bRet;
}
if (m_bDoing)
{
return false;
}
m_bDoing = true;
bRet = _ResponseFrame(_sIp, _iPort, "01", _oArray);
m_bDoing = false;
return bRet;
}
void CUdpServer::_Init()
{
m_pUdpServer = new QUdpSocket(this);
}
uint8_t CUdpServer::_XorCheck(uint8_t *pbuf, uint32_t length)
{
uint8_t temp = 0;
uint32_t i;
pbuf++;
pbuf++;
for (i = 0; i < length - 2; i++)
{
temp ^= *pbuf++;
}
return temp;
}
qint64 CUdpServer::_GetNowDateTimeStampMs()
{
// 获取当前日期和时间
QDateTime currentDateTime = QDateTime::currentDateTime();
// 获取当前年份
int iCurrentYear = currentDateTime.date().year();
int iCurrentMonth = currentDateTime.date().month();
int iCurrentDay = currentDateTime.date().day();
// 创建基准日期(以当前年份为基准)
QDateTime baseDateTime(QDate(iCurrentYear, iCurrentMonth, iCurrentDay), QTime(0, 0, 0));
// 计算当前时间相对于基准日期的毫秒数
qint64 timestamp = currentDateTime.toMSecsSinceEpoch() - baseDateTime.toMSecsSinceEpoch();
return timestamp;
}
void CUdpServer::on_readyRead()
{
//帧头(af) 设备类型 设备编号 电池电量 时间戳 唯一ID 命令类型 包序列 数据长度 校验字节 数据校验 数据
// 1 1 1 1 4 4 1 1 1 1 1 N
while (m_pUdpServer->hasPendingDatagrams()) // 判断是否有可读数据
{
QNetworkDatagram datagram = m_pUdpServer->receiveDatagram(); // 读取数据
QByteArray replyData = datagram.data();
if(replyData.count())
{
QString sIP = datagram.senderAddress().toString().remove("::ffff:");
int iPort = datagram.senderPort();
QString sCmd = QString("%1").arg((char)replyData.at(12), 2, 16, QChar('0'));
for (int i = 0; i< replyData.size(); i++)
{
char byte = replyData.at(i);
QString sByte = QString::number((int)byte, 16);
QString hexString = QString("%1").arg((int)byte, 2, 16, QChar('0'));
}
if (sCmd == "01")
{
_HandleLoginFrame(sIP, iPort, replyData);
}
}
}
}
调用的时候,就直接new一个这个UDPServer的对象就可以了。
如果大家需要直接可以运行起来的程序,大家可以参考这位兄弟的,这位兄弟的就写的比较完整了,客户端,服务端都有。
https://github.com/mahuifa/QMDemo/tree/master/QMNetwork
END、总结的知识与问题
1、如何获取QByteArray中某一字节的数据,并将其转为十进制?
int iElectricQuantity = (int)_oArray.at(3);
2、如何以本年本月本日为基础,获取时间戳,而不以1970为基础?
qint64 CUdpServer::_GetNowDateTimeStampMs()
{
// 获取当前日期和时间
QDateTime currentDateTime = QDateTime::currentDateTime();
// 获取当前年份
int iCurrentYear = currentDateTime.date().year();
int iCurrentMonth = currentDateTime.date().month();
int iCurrentDay = currentDateTime.date().day();
// 创建基准日期(以当前年份为基准)
QDateTime baseDateTime(QDate(iCurrentYear, iCurrentMonth, iCurrentDay), QTime(0, 0, 0));
// 计算当前时间相对于基准日期的毫秒数
qint64 timestamp = currentDateTime.toMSecsSinceEpoch() - baseDateTime.toMSecsSinceEpoch();
return timestamp;
}
3、如何将一个四个字节组成的数拆分成1个字节一个字节的?
qint64 iTimeStamp = _GetNowDateTimeStampMs();//这个实际获取到的是4个字节,但也没关系,我们根据下面的方式,取到的一定是最前面的四个字节
quint8 byte1 = (iTimeStamp >> 24) & 0xFF;
quint8 byte2 = (iTimeStamp >> 16) & 0xFF;
quint8 byte3 = (iTimeStamp >> 8) & 0xFF;
quint8 byte4 = iTimeStamp & 0xFF;
cArray[4] = (int)byte1;//时间戳
cArray[5] = (int)byte2;//时间戳
cArray[6] = (int)byte3;//时间戳
cArray[7] = (int)byte4;//时间戳
4、如何对前面的所有字节进行异或校验?
uint8_t CUdpServer::_XorCheck(uint8_t *pbuf, uint32_t length)
{
uint8_t temp = 0;
uint32_t i;
pbuf++;
pbuf++;
for (i = 0; i < length - 2; i++)
{
temp ^= *pbuf++;
}
return temp;
}
5、如何将QByteArray中的某个字节转为十六进制?
char byte = replyData.at(i);//replayData就是一个QByteArray
QString hexString = QString("%1").arg((int)byte, 2, 16, QChar('0'));
参考
参考:
1、UDP广播与多播
2、Qt 网络编程-UDP
3、Qt中实现UDP的分包和组包——参考“草上爬”的博客