Qt基于文本协议的网络应用开发
文章目錄
- 1 文本協議的設計與實現
- 1.1 文本協議設計介紹
- 1.2 文本協議設計示例
- 2 從字節流裝配文本協議對象
- 3 文本協議中的中文處理
- 4 文本協議的網絡應用
1 文本協議的設計與實現
1.1 文本協議設計介紹
我們首先來看一下TCP應用編程中的問題:
- 數據接收端無法知道數據的發送方式!
網絡程序設計中的期望:
- 每次發送一條完整的消息,每次接收一條完整的消息。
- 即使接收緩沖區中有多條消息,也不會消息粘連。
- 消息中涵蓋了數據類型和數據長度等信息。
應用層協議設計:
- 什么是協議?
- 協議是通信雙方為數據交換而建立的規則、標準和約定的集合。
- 協議對數據傳輸的作用:
- 通信雙方根據協議能夠正確收發數據。
- 通信雙方根據協議能夠解釋數據的意義。
1.2 文本協議設計示例
協議設計示例:
- 目標:基于TCP設計可用于文本傳輸的協議。
- 完整消息包含:
- 數據頭:數據類型(即:數據區用途,固定長度)。
- 數據長度:數據區長度(固定長度)。
- 數據區:字符數據(變長區域)。
實現代碼如下:
TextMessage.h:
#ifndef TEXTMESSAGE_H #define TEXTMESSAGE_H#include <QObject>class TextMessage : public QObject {QString m_type;QString m_data;public:TextMessage(QObject* parent = NULL);TextMessage(QString type, QString data, QObject* parent = NULL);QString type();int length();QString data();QString serialize();bool unserialize(QString s); };#endif // TEXTMESSAGE_HTextMessage.cpp:
#include "TextMessage.h"TextMessage::TextMessage(QObject* parent) : QObject(parent) {m_type = "";m_data = ""; }TextMessage::TextMessage(QString type, QString data, QObject* parent) : QObject(parent) {m_type = type.trimmed();m_type.resize(4, ' ');m_data = data.mid(0, 0xFFFF); }QString TextMessage::type() {return m_type.trimmed(); }int TextMessage::length() {return m_data.length(); }QString TextMessage::data() {return m_data; }QString TextMessage::serialize() {QString len = QString::asprintf("%X", m_data.length());len.resize(4, ' ');return m_type + len + m_data; }bool TextMessage::unserialize(QString s) {bool ret = (s.length() >= 8);if( ret ){QString type = s.mid(0, 4);QString len = s.mid(4, 4).trimmed();int l = len.toInt(&ret, 16);ret = ret && (l == (s.length() - 8));if( ret ){m_type = type;m_data = s.mid(8, l);}}return ret; }main.cpp:
#include <QCoreApplication> #include <QDebug> #include "TextMessage.h"int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);TextMessage tm("AB", "1234567890");QString s = tm.serialize();qDebug() << s;TextMessage tmt;tmt.unserialize(s);qDebug() << tmt.type();qDebug() << tmt.length();qDebug() << tmt.data();return a.exec(); }2 從字節流裝配文本協議對象
問題:
- 如何將緩沖區中的數據裝配成為協議對象?
深度思考:數據是否能夠裝配成協議對象?
- 數據量足夠:
- 如果數據量足夠,是否能夠裝配不止一個對象?
- 如何處理剩余數據(屬于下一個協議對象)?
- 數據量不足:
- 是否達到協議最小長度(8字節)?
- 如何處理數據量超過最小長度,但不足以產生一個對象的情況?
初步的解決方案:
- 定義一個類用于接收字節流并裝配協議對象。
- 類中提供容器(隊列)暫存字節流。
- 當容器中至少存在8個字節時開始狀態:
- 首先裝配協議中的類型(type)和數據區長度(length)。
- 根據數據區長度從容易中取數據裝配協議數據(data)。
- 當協議數據裝配完成時,創建協議對象并返回,否則,返回NULL。
協議對象裝配類的初步設計:
assemble()函數的實現流程:
assemble()函數的注意事項:
- 以m_type作為標志決定是否解析類型和長度。
- m_length是接收后續數據的基礎。
- 當m_data的長度與m_length相同時創建協議對象。
- 否則,返回NULL。
makeTypeAndLength()實現要點:
makeMessage()實現要點:
代碼實現如下:
TxtMsgAssembler.h:
TxtMsgAssembler.cpp:
#include "TxtMsgAssembler.h"TxtMsgAssembler::TxtMsgAssembler(QObject* parent) : QObject(parent) {}void TxtMsgAssembler::clear() {m_type = "";m_length = 0;m_data = ""; }QString TxtMsgAssembler::fetch(int n) {QString ret = "";for(int i=0; i<n; i++){ret += m_queue.dequeue();}return ret; }void TxtMsgAssembler::prepare(const char* data, int len) {if( data != NULL ){for(int i=0; i<len; i++){m_queue.enqueue(data[i]);}} }QSharedPointer<TextMessage> TxtMsgAssembler::assemble() {TextMessage* ret = NULL;bool tryMakeMsg = false;if( m_type == "" ){tryMakeMsg = makeTypeAndLength();}else{tryMakeMsg = true;}if( tryMakeMsg ){ret = makeMessage();}if( ret != NULL ){clear();}return QSharedPointer<TextMessage>(ret); }QSharedPointer<TextMessage> TxtMsgAssembler::assemble(const char* data, int len) {prepare(data, len);return assemble(); }bool TxtMsgAssembler::makeTypeAndLength() {bool ret = (m_queue.length() >= 8);if( ret ){QString len = "";m_type = fetch(4);len = fetch(4);m_length = len.trimmed().toInt(&ret, 16);if( !ret ){clear();}}return ret; }TextMessage* TxtMsgAssembler::makeMessage() {TextMessage* ret = NULL;if( m_type != "" ){int needed = m_length - m_data.length();int n = (needed <= m_queue.length()) ? needed : m_queue.length();m_data += fetch(n);if( m_length == m_data.length() ){ret = new TextMessage(m_type, m_data);}}return ret; }void TxtMsgAssembler::reset() {clear();m_queue.clear(); }main.cpp:
#include <QCoreApplication> #include <QDebug> #include "TextMessage.h" #include "TxtMsgAssembler.h"int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);TextMessage tm("AB", "1234567890");QString s = tm.serialize();qDebug() << s;TxtMsgAssembler as;QSharedPointer<TextMessage> pt;pt = as.assemble(s.toStdString().c_str(), s.length());if( pt != NULL ){qDebug() << "assemble successfully";qDebug() << pt->type();qDebug() << pt->length();qDebug() << pt->data();}return a.exec(); }總結一下:
- 從連續字節流裝配協議對象是應用自定義協議的基礎。
- 裝配類(TxtMsgAssembler)用于解析自定義協議。
- 裝配類的實現的關鍵是如何處理字節數據不夠的情況。
- 自定義協議類和裝配類能夠有效解決數據粘連問題。
3 文本協議中的中文處理
問題:
- 文本協議的設計與實現能夠支持中文嗎?
回顧協議設計:
我們看一下下面的代碼會有相同的輸出嗎?
第一種方式正常輸出,第二種方式則不行。
深度分析:
- 文本協議的實現只考慮了ASCII碼的情況,對于中文類型的寬字符編碼情況并未考慮(寬字符>=2字符)。
協議設計的微小改動:
- Type:4個ASCII字符。
- Length:4個ASCII字符(存儲數據區字節數)。
- 數據區:使用UTF-8方式進行編碼。
編碼小知識:
- ASCII:
- 最早的統一編碼標準,規定了128個字符編碼(0-127)。
- Unicode
- 一個很大的字符集,規定了字符的二進制代碼(編碼標準)。
- UTF-8
- 使用最廣泛的一種Unicode編碼標準的實現。
UTF-8編碼的特點:
UTF-8編碼示例:
協議實現的改動:
首先看一下相對上面的代碼都做了什么修改:
4 文本協議的網絡應用
將TextMessage對象作為網絡傳輸的基本單位:
架構設計:
實現概要:
- 客戶端提供發送TextMessage對象的成員函數。
- 客戶端和服務端均內置TxtMsgAssembler對象:
- 用于從網絡字節流裝配TextMessage對象。
- 當成功收到TextMessage對象:
- 使用TxtMsgHandler接口進行異步通知。
為了使得服務端可以正確接收多個客戶端的TextMessage消息,我們需要為每一個與客戶端通信的TcpSocket對象分配一個專用的裝配對象:
簡單看一下Qt中的QMap容器:
- QMap是一種基于鍵值對(Key-Value)的字典數據結構。
- QMap是通過模板定義的,鍵和值都可以是自定義數據類型。
- QMap使用示例:
解決方案如下:
- 客戶端連接時,動態創建裝配對象,插入字典中。
- 網絡字節流達到時,在字典中查找裝配對象。
- 通過查找到的裝配對象處理字節流。
- 客戶端斷開時,銷毀對應的裝配對象。
代碼組織結構如下:
TxtMsgHandler.h:
TextMessage.h:
#ifndef TEXTMESSAGE_H #define TEXTMESSAGE_H#include <QObject> #include <QByteArray>class TextMessage : public QObject {QString m_type;QString m_data;public:TextMessage(QObject* parent = NULL);TextMessage(QString type, QString data, QObject* parent = NULL);QString type();int length();QString data();QByteArray serialize();bool unserialize(QByteArray ba); };#endif // TEXTMESSAGE_HTextMessage.cpp:
#include "TextMessage.h"TextMessage::TextMessage(QObject* parent) : QObject(parent) {m_type = "";m_data = ""; }TextMessage::TextMessage(QString type, QString data, QObject* parent) : QObject(parent) {m_type = type.trimmed();m_type.resize(4, ' ');m_data = data.mid(0, 15000); }QString TextMessage::type() {return m_type.trimmed(); }int TextMessage::length() {return m_data.length(); }QString TextMessage::data() {return m_data; }QByteArray TextMessage::serialize() {QByteArray ret;QByteArray dba = m_data.toUtf8();QString len = QString::asprintf("%X", dba.length());len.resize(4, ' ');ret.append(m_type.toStdString().c_str(), 4);ret.append(len.toStdString().c_str(), 4);ret.append(dba);return ret; }bool TextMessage::unserialize(QByteArray ba) {bool ret = (ba.length() >= 8);if( ret ){QString type = QString(ba.mid(0, 4));QString len = QString(ba.mid(4, 4)).trimmed();int l = len.toInt(&ret, 16);ret = ret && (l == (ba.length() - 8));if( ret ){m_type = type;m_data = QString(ba.mid(8));}}return ret; }TxtMsgAssembler.h:
#ifndef TXTMSGASSEMBLER_H #define TXTMSGASSEMBLER_H#include <QObject> #include <QQueue> #include <QSharedPointer> #include "TextMessage.h"class TxtMsgAssembler : public QObject {QQueue<char> m_queue;QString m_type;int m_length;QByteArray m_data;void clear();QByteArray fetch(int n);bool makeTypeAndLength();TextMessage* makeMessage(); public:TxtMsgAssembler(QObject* parent = NULL);void prepare(const char* data, int len);QSharedPointer<TextMessage> assemble(const char* data, int len);QSharedPointer<TextMessage> assemble();void reset(); };#endif // TXTMSGASSEMBLER_HTxtMsgAssembler.cpp:
#include "TxtMsgAssembler.h"TxtMsgAssembler::TxtMsgAssembler(QObject* parent) : QObject(parent) {}void TxtMsgAssembler::clear() {m_type = "";m_length = 0;m_data.clear(); }QByteArray TxtMsgAssembler::fetch(int n) {QByteArray ret;for(int i=0; i<n; i++){ret.append(m_queue.dequeue());}return ret; }void TxtMsgAssembler::prepare(const char* data, int len) {if( data != NULL ){for(int i=0; i<len; i++){m_queue.enqueue(data[i]);}} }QSharedPointer<TextMessage> TxtMsgAssembler::assemble() {TextMessage* ret = NULL;bool tryMakeMsg = false;if( m_type == "" ){tryMakeMsg = makeTypeAndLength();}else{tryMakeMsg = true;}if( tryMakeMsg ){ret = makeMessage();}if( ret != NULL ){clear();}return QSharedPointer<TextMessage>(ret); }QSharedPointer<TextMessage> TxtMsgAssembler::assemble(const char* data, int len) {prepare(data, len);return assemble(); }bool TxtMsgAssembler::makeTypeAndLength() {bool ret = (m_queue.length() >= 8);if( ret ){QString len = "";m_type = QString(fetch(4));len = QString(fetch(4));m_length = len.trimmed().toInt(&ret, 16);if( !ret ){clear();}}return ret; }TextMessage* TxtMsgAssembler::makeMessage() {TextMessage* ret = NULL;if( m_type != "" ){int needed = m_length - m_data.length();int n = (needed <= m_queue.length()) ? needed : m_queue.length();m_data.append(fetch(n));if( m_length == m_data.length() ){ret = new TextMessage(m_type, QString(m_data));}}return ret; }void TxtMsgAssembler::reset() {clear();m_queue.clear(); }ClientDemo.h:
#ifndef CLIENTDEMO_H #define CLIENTDEMO_H#include <QObject> #include <QTcpSocket> #include "TextMessage.h" #include "TxtMsgAssembler.h" #include "TxtMsgHandler.h"class ClientDemo : public QObject {Q_OBJECTQTcpSocket m_client;TxtMsgAssembler m_assembler;TxtMsgHandler* m_handler; protected slots:void onConnected();void onDisconnected();void onDataReady();void onBytesWritten(qint64 bytes);public:ClientDemo(QObject* parent = NULL);bool connectTo(QString ip, int port);qint64 send(TextMessage& message);qint64 available();void setHandler(TxtMsgHandler* handler);void close(); };#endif // CLIENTDEMO_HClientDemo.cpp:
#include "ClientDemo.h" #include <QHostAddress> #include <QDebug>ClientDemo::ClientDemo(QObject* parent) : QObject(parent), m_handler(NULL) {connect(&m_client, SIGNAL(connected()), this, SLOT(onConnected()));connect(&m_client, SIGNAL(disconnected()), this, SLOT(onDisconnected()));connect(&m_client, SIGNAL(readyRead()), this, SLOT(onDataReady()));connect(&m_client, SIGNAL(bytesWritten(qint64)), this, SLOT(onBytesWritten(qint64))); }void ClientDemo::onConnected() {}void ClientDemo::onDisconnected() {m_assembler.reset(); }void ClientDemo::onDataReady() {char buf[256] = {0};int len = 0;while( (len = m_client.read(buf, sizeof(buf))) > 0 ){QSharedPointer<TextMessage> ptm = m_assembler.assemble(buf, len);if( (ptm != NULL) && (m_handler != NULL) ){m_handler->handle(m_client, *ptm);}} }void ClientDemo::onBytesWritten(qint64 bytes) {(void)bytes; }bool ClientDemo::connectTo(QString ip, int port) {m_client.connectToHost(ip, port);return m_client.waitForConnected(); }qint64 ClientDemo::send(TextMessage& message) {QByteArray ba = message.serialize();return m_client.write(ba.data(), ba.length()); }qint64 ClientDemo::available() {return m_client.bytesAvailable(); }void ClientDemo::close() {m_client.close(); }void ClientDemo::setHandler(TxtMsgHandler* handler) {m_handler = handler; }ServerDemo.h:
#ifndef SERVERDEMO_H #define SERVERDEMO_H#include <QObject> #include <QTcpServer> #include <QMap> #include "TextMessage.h" #include "TxtMsgAssembler.h" #include "TxtMsgHandler.h"class ServerDemo : public QObject {Q_OBJECTQTcpServer m_server;QMap<QTcpSocket*, TxtMsgAssembler*> m_map;TxtMsgHandler* m_handler; public:ServerDemo(QObject* parent = NULL);bool start(int port);void stop();void setHandler(TxtMsgHandler* handler);~ServerDemo();protected slots:void onNewConnection();void onConnected();void onDisconnected();void onDataReady();void onBytesWritten(qint64 bytes); };#endif // SERVERDEMO_HServerDemo.cpp:
#include "ServerDemo.h" #include <QHostAddress> #include <QTcpSocket> #include <QObjectList> #include <QDebug>ServerDemo::ServerDemo(QObject* parent) : QObject(parent), m_handler(NULL) {connect(&m_server, SIGNAL(newConnection()), this, SLOT(onNewConnection())); }void ServerDemo::onNewConnection() {QTcpSocket* tcp = m_server.nextPendingConnection();TxtMsgAssembler* assembler = new TxtMsgAssembler();m_map.insert(tcp, assembler);connect(tcp, SIGNAL(connected()), this, SLOT(onConnected()));connect(tcp, SIGNAL(disconnected()), this, SLOT(onDisconnected()));connect(tcp, SIGNAL(readyRead()), this, SLOT(onDataReady()));connect(tcp, SIGNAL(bytesWritten(qint64)), this, SLOT(onBytesWritten(qint64))); }void ServerDemo::onConnected() {}void ServerDemo::onDisconnected() {QTcpSocket* tcp = dynamic_cast<QTcpSocket*>(sender());if( tcp != NULL ){delete m_map.take(tcp);} }void ServerDemo::onDataReady() {QTcpSocket* tcp = dynamic_cast<QTcpSocket*>(sender());char buf[256] = {0};int len = 0;if( tcp != NULL ){TxtMsgAssembler* assembler = m_map.value(tcp);while( (len = tcp->read(buf, sizeof(buf))) > 0 ){QSharedPointer<TextMessage> ptm = (assembler != NULL) ? assembler->assemble(buf, len) : NULL;if( (ptm != NULL) && (m_handler != NULL) ){m_handler->handle(*tcp, *ptm);}}} }void ServerDemo::onBytesWritten(qint64 bytes) {(void)bytes; }bool ServerDemo::start(int port) {bool ret = true;if( !m_server.isListening() ){ret = m_server.listen(QHostAddress("127.0.0.1"), port);}return ret; }void ServerDemo::stop() {if( m_server.isListening() ){m_server.close();} }ServerDemo::~ServerDemo() {const QObjectList& list = m_server.children();for(int i=0; i<list.length(); i++){QTcpSocket* tcp = dynamic_cast<QTcpSocket*>(list[i]);if( tcp != NULL ){tcp->close();}}const QList<TxtMsgAssembler*>& al = m_map.values();for(int i=0; i<al.length(); i++){delete al.at(i);} }void ServerDemo::setHandler(TxtMsgHandler* handler) {m_handler = handler; }main.cpp:
#include <QCoreApplication> #include <QDebug> #include "TextMessage.h" #include "TxtMsgAssembler.h" #include "TxtMsgHandler.h" #include "ClientDemo.h" #include "ServerDemo.h"class Handler : public TxtMsgHandler { public:void handle(QTcpSocket& socket, TextMessage& message){qDebug() << &socket;qDebug() << message.type();qDebug() << message.length();qDebug() << message.data();} };int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);TextMessage message("Demo", "Delphi Tang 狄泰軟件學院!");Handler handler;ServerDemo server;ClientDemo client;server.setHandler(&handler);server.start(8890);client.setHandler(&handler);client.connectTo("127.0.0.1", 8890);client.send(message);return a.exec(); }參考資料:
總結
以上是生活随笔為你收集整理的Qt基于文本协议的网络应用开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华谊兄弟被强制执行3亿 到底发生了什么
- 下一篇: 民生香港卡办卡条件