網絡編程,OSI(開放式系統互聯參考模型)七層參考模型:應用層、表示層、會話層、傳輸層、網絡層、數據鏈路層、物理層。
套接字(Socket)是網絡通信的基本構建模塊,又分爲流式套接字(Stream Socket)和數據報套接字(Datagram Socket)兩種類型的套接字。
TCP:傳送控制協議(Transmission Control Protocol),這是一種提供給用戶的可靠的全雙工字節流面向連接的協議。
UDP:用戶數據報協議(User Datagram Protocol),這是提供給用戶進程的無連接協議,用於傳送數據而不執行正確性檢查。
當然TCP、UDP都歸屬於傳輸層協議。
對所用的網絡知識簡短的介紹,下面步入正題,開始Qt套接字編程~
在TCP/IP網絡中兩個進程間的相互作用的主要模式是客戶機/服務器模式(Client/Server model),是構造分佈式應用程序最常用的模式。
Qt中幾乎所有的QtNetwork類都是異步的,一般情況下沒有必要Socket使用在多線程中。
■、UDP
UDP是不可信賴的,它是基於包的協議。一些應用程序層的協議使用UDP是因爲它比TCP更加小巧,數據是從一個主機到另一個主機以包的形式發送的。這裏沒有連接到的概念,並且如果一個UDP包沒有被正確交付,它不會向系統報告任何錯誤。
下面寫一個簡單的廣播示例,由客戶端和服務器兩部分組成。
//客戶端發送數據
void Client::sendDatagram()
{
QByteArray datagram;
QDataStream out(&datagram, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_3);
out << QDateTime::currentDateTime() << "vic.MINg!" << 3.14;QUdpSocket udpSocket(this);
udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, 1981);
}
在QByteArray型局部變量datagram中構建待發送的數據包,然後通過QUdpSocket類的 writeDatagram ( const QByteArray & datagram, const QHostAddress & host, quint16 port );函數將數據包發出。值得注意的是,這裏的地址使用了QHostAddress::Broadcast值,它對應IPv4下的廣播地址,如果將該值更換成單機地址(如本機地址QHostAddress::LocalHost),將變成一個普通的點對點的UDP程序。
//服務器接收數據
void Server::initSocket()
{
udpSocket = new QUdpSocket(this);
udpSocket->bind(1981);connect(udpSocket, SIGNAL(readyRead()),
this, SLOT(readPendingDatagrams()));
}
初始化生成QUdpSocket實例,並綁定與客戶端約定的端口(1981)。這裏多說幾句,在編寫網絡程序時應該使用1024以上的端口號,1024以下的端口號通常被系統保留,緊密的綁定了一些服務(如80端口是http服務、21端口是ftp服務)。
void Server::readPendingDatagrams()
{
while (udpSocket->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;udpSocket->readDatagram(datagram.data(), datagram.size(),
&sender, &senderPort);
QDateTime dateTime;
QString name;
double data;
QDataStream in(&datagram, QIODevice::ReadOnly);
in.setVersion(QDataStream::Qt_4_3);
in >> dateTime >> name >> data;
}
}
接受數據函數首先調用QUdpSocket類的成員函數hasPendingDatagrams()以判斷是否有可供讀取的數據。如果有則通過pendingDatagramSize()獲取當前可供讀取的UDP報文大小,並據此大小分配接收緩衝區,最後讀取相應數據。
■、TCP
TCP是一個基於流的協議。對於應用程序,數據表現爲一個長長的流,而不是一個大大的平面文件。基於TCP的高層協議通常是基於行的或者基於塊的。
●、基於行的協議把數據作爲一行文本進行傳輸,每行都以一個換行符結尾。
●、基於塊的協議把數據作爲二進制塊進行傳輸,每塊是由一個size大小字段和緊跟它的一個size字節的數據組成。
QTcpSocket通過器父類QAbstractSocket繼承了QIODevice,因此他可以通過使用QTextStream和QDataStream來進行讀取和寫入。
QTcpServer類在服務器端處理來自TCP客戶端連接數據,需要注意的是,該類直接繼承於QObject基類,而不是QAbstractSocket抽象套接字類。
下面介紹一個TCP應用示例,示例來自《精通Qt4編程》,感覺十分不錯,它也是由客戶端和服務器兩部分組成,客戶端選擇本地文件,並通過TCP連接將它上傳到服務器端。
由於使用了TCP協議,所以可以輕鬆的傳遞大文件,而無需擔心傳輸過程造成文件損壞。
其中客戶端程序SendFile從本地文件系統中選中一個已有文件並在成功連接服務器後開始發送,服務器端程序ReceiveFile則將該文件保存在當前目錄下,兩端均以進度條和數據兩種形式分別顯示文件傳輸進度和詳細的數據傳輸字節數。
客戶端程序SendFile的用戶界面是一個簡單的對話框,上面佈置一個QProgressBar進度條,一個用於顯示狀態的QLabel,三個QPushButton按鈕,分別用來選擇文件、發送文件和退出程序。
Qt的QFileDialog類提供了一個文件選擇對話框,用戶使用它可以很容易的進行目錄或文件的選擇。
下面將Dialog類部分代碼陳列出來,它是QDialog的子類,實現客戶端的全部功能。
class Dialog : public QDialog
{
Q_OBJECTpublic:
Dialog(QWidget *parent = 0);public slots:
void start();
void startTransfer();
void updateClientProgress(qint64 numBytes);
void displayError(QAbstractSocket::SocketError socketError);
void openFile();private:
QProgressBar *clientProgressBar;
QLabel *clientStatusLabel;
QPushButton *startButton;
QPushButton *quitButton;
QPushButton *openButton;
QDialogButtonBox *buttonBox;QTcpSocket tcpClient; //客戶端套接字
qint64 TotalBytes; //總共需發送的字節數
qint64 bytesWritten; //已發送字節數
qint64 bytesToWrite; //待發送字節數
qint64 loadSize; //被初始化爲一個4Kb的常量
QString fileName; //待發送的文件的文件名
QFile *localFile; //待發送的文件
QByteArray outBlock; //緩存一次發送的數據
};
爲了發送較大的文件,變量使用了qint64類型,Qt保證該類型數據在所有其所支持的平臺下均爲64位大小,這幾乎可以表示一個無限大的文件了。
loadSize用來儘可能的將一個較大的文件分割,每次發送4Kb大小,餘下不足4Kb的按實際大小發送。
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
loadSize = 4*1024; // 4Kb
TotalBytes = 0;
bytesWritten = 0;
bytesToWrite = 0;
clientProgressBar = new QProgressBar;
clientStatusLabel = new QLabel(tr("客戶端就緒"));
startButton = new QPushButton(tr("開始"));
quitButton = new QPushButton(tr("退出"));
openButton = new QPushButton (tr("打開"));
startButton->setEnabled(false);
buttonBox = new QDialogButtonBox;
buttonBox->addButton(startButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(openButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);connect(startButton, SIGNAL(clicked()), this, SLOT(start()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
connect(openButton, SIGNAL(clicked()), this, SLOT(openFile()));
connect(&tcpClient, SIGNAL(connected()), this, SLOT(startTransfer()));
connect(&tcpClient, SIGNAL(bytesWritten(qint64)),
this, SLOT(updateClientProgress(qint64)));
connect(&tcpClient, SIGNAL(error(QAbstractSocket::SocketError)),
this, SLOT(displayError(QAbstractSocket::SocketError)));QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(clientProgressBar);
mainLayout->addWidget(clientStatusLabel);
mainLayout->addStretch(1);
mainLayout->addSpacing(10);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);
setWindowTitle(tr("發送文件"));
}
這裏關聯了QTcpSocket的三個重要信號,它們分別是成功與服務器建立連接後產生的connected()信號,數據成功發送後產生的bytesWritten()信號和產生錯誤的error()信號。
void Dialog::openFile()
{
fileName = QFileDialog::getOpenFileName(this);
if (!fileName.isEmpty())
startButton->setEnabled(true);
}
用戶在客戶端界面按下"打開"按鈕後,openFile()槽函數將被調用。該函數通過Qt文件選擇對畫框QFileDialog所提供的靜態函數getOpenFileName(),能夠很容易地返回用戶所選取的文件名,這裏將其保存在私有成員變量fileName中。如果選中返回的文件名非空,將激活"開始"按鈕。
void Dialog::start()
{
startButton->setEnabled(false);
QApplication::setOverrideCursor(Qt::WaitCursor);
bytesWritten = 0;
clientStatusLabel->setText(tr("連接中..."));
tcpClient.connectToHost(QHostAddress::LocalHost, 16689);
}
用戶在客戶端界面按下"開始"按鈕後,start()槽函數將被調用。該函數的主要功能是連接服務器,它使用了QTcpSocket類的connectToHost()函數,其中的兩個參數分別是服務器主機地址及其監聽端口,讀者可以根據實際應用需求進行修改。
void Dialog::startTransfer()
{
localFile = new QFile(fileName);
if (!localFile->open(QFile::ReadOnly )) {
QMessageBox::warning(this, tr("應用程序"),
tr("無法讀取文件 %1:\n%2.")
.arg(fileName)
.arg(localFile->errorString()));
return;
}
TotalBytes = localFile->size();
QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_4_3);
QString currentFile = fileName.right(fileName.size() - fileName.lastIndexOf('/') - 1);
sendOut << qint64(0) << qint64(0) << currentFile;
TotalBytes += outBlock.size();
sendOut.device()->seek(0);
sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64) * 2));
bytesToWrite = TotalBytes - tcpClient.write(outBlock);
clientStatusLabel->setText(tr("已連接"));
qDebug() << currentFile << TotalBytes;
outBlock.resize(0);
}
一旦連接建立成功,QTcpSocket類將發出connected()消息,繼而調用startTransfer()槽函數。該函數首先向服務器端發送一個文件頭結構。
文件頭結構由三個字段組成,分別是64位的總長度(包括文件數據長度和文件頭自身長度),64位的文件名長度和文件名。
函數startTransfer()首先以只讀方式打開選中的文件,然後通過QFile類的size()函數獲取待發送文件的大小,並將該值暫存於TotalBytes變量中。
接下來將發送緩衝區outBlock封裝在一個QDataStream類型的變量中,這樣做可以很方便的通過重載的"<<"操作符填寫文件頭結構。
設置文件頭結構的操作有些小技巧,這裏首先通過QString類的right()函數去掉文件的路徑部分,僅將文件部分保存在currentFile變量中,然後通過sendOut << qint64(0) << qint64(0) << currentFile操作構造一個臨時的文件頭,將該值追加到TotalBytes字段,從而完成實際需發送字節數的記錄。
接着通過sendOut.device()->seek(0)函數將讀寫操作指向從頭開始,並且調用類似操作sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64) * 2)),填寫實際的總長度和文件長度。
需要注意的是,不能錯誤地通過QString::size()函數獲取文件名的大小,該函數返回的是QString類型文件名所包含的字節數,而不是實際所佔存儲空間的大小,由於字節編碼和QString類存儲管理的原因,兩者往往並不相等。
完成了文件頭結構的填寫後,調用tcpClient.write(outBlock)函數將該文件頭髮出,同時修改待發送字節數bytesToWrite。最後,調用outBlock.resize(0)函數清空發送緩衝區以備下次使用。
void Dialog::updateClientProgress(qint64 numBytes)
{
bytesWritten += (int)numBytes;
if (bytesToWrite > 0) {
outBlock = localFile->read(qMin(bytesToWrite, loadSize));
bytesToWrite -= (int)tcpClient.write(outBlock);
outBlock.resize(0);
}
else{
localFile->close();
}
clientProgressBar->setMaximum(TotalBytes);
clientProgressBar->setValue(bytesWritten);
clientStatusLabel->setText(tr("已發送 %1MB").arg(bytesWritten / (1024 * 1024)));
}
一旦數據發出,QTcpSocket類將會產生bytesWritten()信號,繼而調用updateClientProgress(qint64)槽函數,參數表示實際已發出的字節數。如果待發送數據計數bytesToWritten大於0,將儘可能地從發送文件中讀取4Kb數據,並將其發送,否則發送完畢關閉文件。還需要在此更新亦發和待發數據計數,並以此更新發送進度條和狀態顯示。
void Dialog::displayError(QAbstractSocket::SocketError socketError)
{
if (socketError == QTcpSocket::RemoteHostClosedError)
return;QMessageBox::information(this, tr("網絡"),
tr("產生如下錯誤: %1.").arg(tcpClient.errorString()));tcpClient.close();
clientProgressBar->reset();
clientStatusLabel->setText(tr("客戶端就緒"));
startButton->setEnabled(true);
QApplication::restoreOverrideCursor();
}
如果連接或數據傳輸過程中的某次操作發生錯誤,QTcpSocket類發出error()信號,並觸發錯誤處理槽函數displayError()。該函數的錯誤處理方式比較簡單,僅是顯示出錯誤對話框並關閉連接。
main()函數實現與以前的例子類似,這裏不再敘述了。
服務器端程序ReceiveFile完成的功能與客戶端程序恰恰相反,它負責從TCP連接上接收數據,並將其寫入當前目錄下的指定文件中。
其界面也是一個簡單的對話框,上面佈置一個QProgressBar進度條,一個用來顯示狀態的QLabel,兩個QPushButton按鈕分別用來開啓監聽和退出程序。
該程序的主要功能也是在一個從QDialog類繼承而來的Dialog類中完成的。
class Dialog : public QDialog
{
Q_OBJECTpublic:
Dialog(QWidget *parent = 0);public slots:
void start();
void acceptConnection();
void updateServerProgress();
void displayError(QAbstractSocket::SocketError socketError);private:
QProgressBar *clientProgressBar;
QProgressBar *serverProgressBar;
QLabel *serverStatusLabel;
QPushButton *startButton;
QPushButton *quitButton;
QPushButton *openButton;
QDialogButtonBox *buttonBox;QTcpServer tcpServer; //服務器套接字
QTcpSocket *tcpServerConnection; //連接後服務器返回的套接字
qint64 TotalBytes; //總共需接收的字節數
qint64 bytesReceived; //已接收字節數
qint64 fileNameSize; //待接收文件名字節數
QString fileName; //待接收文件的文件名
QFile *localFile; //待接收文件
QByteArray inBlock;
};
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
TotalBytes = 0;
bytesReceived = 0;
fileNameSize = 0;
serverProgressBar = new QProgressBar;
serverStatusLabel = new QLabel(tr("服務端就緒"));startButton = new QPushButton(tr("接收"));
quitButton = new QPushButton(tr("退出"));buttonBox = new QDialogButtonBox;
buttonBox->addButton(startButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);connect(startButton, SIGNAL(clicked()), this, SLOT(start()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(serverProgressBar);
mainLayout->addWidget(serverStatusLabel);
mainLayout->addStretch(1);
mainLayout->addSpacing(10);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);setWindowTitle(tr("接收文件"));
}
構造函數負責初始化界面,並將開始和退出按鈕與各自的槽函數關聯。這裏還關聯了QTcpServer的newConnection()信號,該信號在有可用的TCP連接是發出。
void Dialog::start()
{
startButton->setEnabled(false);QApplication::setOverrideCursor(Qt::WaitCursor);
bytesReceived = 0;while (!tcpServer.isListening() && !tcpServer.listen(QHostAddress::LocalHost,16689)) {
QMessageBox::StandardButton ret = QMessageBox::critical(this,
tr("迴環"),
tr("無法開始測試: %1.").arg(tcpServer.errorString()),
QMessageBox::Retry | QMessageBox::Cancel);
if (ret == QMessageBox::Cancel)
return;
}
serverStatusLabel->setText(tr("監聽"));
}
當用戶按下"接收"按鈕後,start()函數開始執行,它調用QTcpServer的isListening()函數和listen()函數判斷當前服務器是否已處在監聽狀態以及在本地16689端口建立監聽是否成功。
如果一切正常,服務器端就已經成功監聽,隨時等待處理客戶端的TCP連接請求,否則彈出錯誤信息,報告錯誤後返回。
void Dialog::acceptConnection()
{
tcpServerConnection = tcpServer.nextPendingConnection();
connect(tcpServerConnection, SIGNAL(readyRead()),
this, SLOT(updateServerProgress()));
connect(tcpServerConnection, SIGNAL(error(QAbstractSocket::SocketError)),
this, SLOT(displayError(QAbstractSocket::SocketError)));serverStatusLabel->setText(tr("接受連接"));
tcpServer.close();
}
有客戶端請求到來時,QTcpSocket類將會發出newConnection()信號,從而觸發acceptConnection()函數。
QTcpServer類在接受了外來TCP連接請求後,可以通過nextPendingConnection()函數獲取一個新的已建立連接的子套接字,(該套接字封裝在QTcpSocket類中)並返回QTcpSocket類指針,將返回值保存在tcpServerConnection私有變量中。
接下來關聯QTcpSocket類的readyRead()信號和error()信號,其中readyRead()信號在新連接中有可讀數據時發出,而當新連接中產生錯誤是會發出error()信號。
由於本例只處理一個客戶端請求,因此在返回一個連接後,就調用QTcpSocket類的close()函數關閉服務器端的監聽,後面的工作均在新建的tcpServerConnection連接上完成。
void Dialog::updateServerProgress()
{
QDataStream in(tcpServerConnection);
in.setVersion(QDataStream::Qt_4_3);
if(bytesReceived <= sizeof(qint64)*2){
if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2)&&(fileNameSize ==0)){
in >> TotalBytes >> fileNameSize;
bytesReceived += sizeof(qint64)*2;
}
if((tcpServerConnection->bytesAvailable() >= fileNameSize)&&(fileNameSize !=0)){
in >> fileName;
bytesReceived += fileNameSize;
localFile = new QFile(fileName);
if (!localFile->open(QFile::WriteOnly )) {
QMessageBox::warning(this, tr("應用程序"),
tr("無法讀取文件 %1:\n%2.").arg(fileName).arg(localFile->errorString()));
return;
}
}else{
return;
}
}
if (bytesReceived < TotalBytes){
bytesReceived += tcpServerConnection->bytesAvailable();
inBlock = tcpServerConnection->readAll();
localFile->write(inBlock);
inBlock.resize(0);
}
serverProgressBar->setMaximum(TotalBytes);
serverProgressBar->setValue(bytesReceived);
qDebug()<<bytesReceived;
serverStatusLabel->setText(tr("已接收 %1MB").arg(bytesReceived / (1024 * 1024)));if (bytesReceived == TotalBytes) {
tcpServerConnection->close();
startButton->setEnabled(true);
QApplication::restoreOverrideCursor();
}
}
當建立的連接有新的可供讀取的數據時,QTcpSocket類會發出readyRead()信號,從而觸發updateServerProgress()函數。該函數完成數據的接收、存儲,並更新進度顯示。
首先將上面返回的TCP連接tcpServerConnection封裝的QDataStream類型變量in中,同時設置流化數據格式類型爲QDataStream::Qt_4_3,與客戶端保持一致。現在可以很方便的通過重載後的"<<"操作符讀取TCP連接上的數據了。
由於流數據是沒有結構的,爲了知道接收的文件名以及文件何時接收完畢,必須首先獲取文件頭結構,這裏還有個小問題,由於開始時所傳輸文件名的長度是未知的,導致文件頭結構的長度也是未知的,因此無法知道TCP數據流中前多少字節屬於文件頭結構部分。實際上文件頭結構的接收可分兩布完成:
1、從TCP數據流中接收前16個字節(兩個qint64結構長),用來確定總共需接收的字節數和文件名長度,並將這兩個值保存在私有成員TotalBytes和fileNameSize中,然後根據fileNameSize值接收文件名。值得注意的是,無法保證在上述接收文件頭結構過程中,TCP連接上總是有足夠的數據,因此在第一步中,需要通過tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize ==0)操作確保至少有16字節的可用數據且文件名長度爲0(表示未從TCP連接接收文件名長度字段,仍處於第一步操作),然後調用in
>> TotalBytes >> fileNameSize操作讀取總共需接收的數據和文件名長度。
2、類似的通過(tcpServerConnection->bytesAvailable() >= fileNameSize) && (fileNameSize !=0)操作確保連接上的數據已包含完整的文件名且文件名長度不爲0(表示已從TCP連接接收文件名長度字段,處於第二步操作中),然後調用in >> fileName操作讀取文件名,並根據該文件名在本地以只寫方式打開一個同名文件localFile,用來保存接收到的數據。
接下來的工作是讀取實際的文件數據並保存,以及更新進度顯示,直到接收到完全的數據。由於所發送的文件內容自身也是無格式的流,因此在接收文件內容時,只要TCP連接上有數據,就調用tcpServerConnection->readAll()操作將當前全部可讀數據讀入接收緩衝inBlock中,隨後再將該緩衝中的數據寫入文件localFile中。當已收到的數據bytesReceived等於TotalBytes時,接收完畢,這時通過tcpServerConnection->close()操作關閉連接。
最後,錯誤處理函數displayError()和主函數main()與客戶端程序類似,這裏不再多說了~
通常QTcpSocket類和QTcpServer類以異步方式工作,但可以通過調用其waitFor...()類型的函數實現同步操作,這類操作將阻塞調用線程直到某個信號發出。
例如:在調用了非阻塞的QTcpSocket::connectToHost()函數後緊接着調用QTcpSocket::waitForConnected()函數以阻塞調用線程,知道connected()信號發出。
一般而言,同步操作往往可以簡化代碼的控制流程,但也存在較大的缺點,調用waitFor...()函數將阻塞事件的處理,對於GUI線程會引起用戶界面的凍結。
因此,Qt建議在GUI線程中不使用同步套接字,此時QTcpSocket也不在需要事件循環。
已經寫了不少,累呀:( ,可是還有例子要舉...
下一個例子,其實是想講解一個Socket編程最爲典型的例子程序了,自己寫的聊天程序,這個例子主要講解的是單服務器、多客戶端進行的處理過程。
但是,由於一個字"懶"的原因,這裏就只對服務端如何實現進行多客戶端進行簡短的講解,其實在聊天程序的比較主要的知識點,在下面這個多線程網絡程序中也涉及到了~~
現在讓我們看看服務器包含的兩個類:QCharServer和QCharClient。
QCharServer類繼承了QServerSocker,QTcpServer類允許接受外來TCP連接,每當檢測到外來TCP連接請求時,會自動調用QTcpServer::incomingConnection()函數,參數爲標識socket ID的int型變量。
QCharServer* serverSocket = new QCharServer(this);
if (!serverSocket->listen(QHostAddress::Any, m_port))
{
QMessageBox::critical(this, tr("CharServer"),
tr("Unable To Start The Server: %1.")
.arg(serverSocket->errorString()));
serverSocket->close();
}
在主界面下創建和監聽,等待客戶端連接。
class QCharServer : public QTcpServer
{
Q_OBJECT
public:
QCharServer(QObject *parent = 0);
private:
void incomingConnection( int socketDescriptor );
signals:
void error(QTcpSocket::SocketError socketError);
};QCharServer::QCharServer(QObject *parent)
: QTcpServer(parent)
{
}void QCharServer::incomingConnection(int socketDescriptor)
{
QCharClient *socket = new QCharClient(this);
if (!socket->setSocketDescriptor(socketDescriptor))
{
emit error(socket->error());
return;
}
}
設置socketDescriptor並且將QCharClient保存到一個內部列表中,從而在任何時候,在內存中QCharClient對象的數量和正在服務的客戶端數量都是一樣的。
QCharClient繼承了QTcpSocket並且封裝了一個單獨的客戶端的狀態。
class QCharClient : public QTcpSocket
{
Q_OBJECT
public:
QCharClient(QObject *parent = 0);private slots:
void recvData();
void tryTest();
void clientDisconnected();private:
void sendData();
};QCharClient::QCharClient(QObject *parent)
: QTcpSocket(parent)
{
connect(this, SIGNAL(connected()), this, SLOT(clientConnected()));
connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));
connect(this, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
}void QCharClient::clientConnected()
{
...
}void QCharClient::recvData()
{
QDataStream in(this);
char buffer[MAX_RECV_BUFFER_SIZE];
memset(buffer, 0, MAX_RECV_BUFFER_SIZE);
unsigned int len = in.readRawData(buffer, MAX_RECV_BUFFER_SIZE);
}void QCharClient::clientDisconnected()
{
...
deleteLater();
}void QCharClient::sendData()
{
QDataStream out(this);
char *buffer;
buffer = "vic.MINg";
int len = strlen( buffer );
out.writeRawData(buffer, len);
}
這裏沒有什麼新內容,不做多廢話了~
一個多線程的網絡時間服務器,這個程序也是來自《精通Qt4編程》一書,每當由客戶請求到達時,這個服務器將啓動一個新線程爲它返回當前的時間,服務器完畢後這個線程將自動退出,同時用戶界面會顯示當前以接受請求的次數。
class TimeServer : public QTcpServer
{
Q_OBJECT
public:
TimeServer(QObject *parent = 0);protected:
void incomingConnection(int socketDescriptor);
private:
Dialog *dlg;
};
首先需要實現一個TCP服務端類TimeServer,這裏直接從QTcpServer類繼承,並重寫了其虛函數void incomingConnection( int socketDescriptor )。這個函數在TCP服務端有新的連接時被調用,參數這是界面指針,借用這個指針,將線程發出的消息關聯到界面的槽函數中。
TimeServer::TimeServer(QObject *parent)
: QTcpServer(parent)
{
dlg = (Dialog*)parent;
}
構造函數十分簡單,這裏用傳入的父類指針parent初始化私有變量dlg就可以了。
void TimeServer::incomingConnection(int socketDescriptor)
{
TimeThread *thread = new TimeThread(socketDescriptor,this);
connect(thread, SIGNAL(finished()), dlg, SLOT(showResult()),Qt::QueuedConnection);
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();
}
在重寫的虛函數incomingConnection()中,首先以返回的套接字描述符socketDescriptor創建一個工作線程TimeThread,然後將這個線程的結束消息finished()分別關聯到界面顯示類的槽函數showResult()用於顯示請求計數,以及線程自身的槽函數deleteLater()用於結束線程。
一切準備工作完成後啓動這個線程。需要注意的是,在第一個connect操作中,使用了排隊連接方式,第二個connect操作中使用了直接連接方式,原因在於前一個信號是跨線程的,後一個信號是在同一個線程中,當然也可以省略connect()函數的最後一個參數,而採用Qt的自動連接選擇方式。另一個需要注意的是,由於工作線程中存在網絡事件,因此不能被外界線程銷燬,這裏使用了延遲銷燬函數deleterLater()保證由工作線程自身銷燬。
class TimeThread : public QThread
{
Q_OBJECT
public:
TimeThread(int socketDescriptor, QObject *parent);
void run();signals:
void error(QTcpSocket::SocketError socketError);
private:
int socketDescriptor;
};
工作線程TimeThread由QThread類繼承而來,這裏將重寫重要的虛函數run()。此外,還定義了一個出錯信號void error(QTcpSocket::SocketError socketError)和一個私有的套接字描述符socketDescriptor。
TimeThread::TimeThread(int socketDescriptor,QObject *parent)
: QThread(parent), socketDescriptor(socketDescriptor)
{
}
構造函數十分簡單,這裏僅是初始化了私有套接字描述符。
void TimeThread::run()
{
QTcpSocket tcpSocket;
if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
emit error(tcpSocket.error());
return;
}QDateTime time;
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_3);
uint time2u = QDateTime::currentDateTime().toTime_t();
out << time2u;
tcpSocket.write(block);
tcpSocket.disconnectFromHost();
tcpSocket.waitForDisconnected();
}
虛函數run()是工作線程的實質所在,當在TimeServer::incomingConnection()函數中調用了start()函數後,這個虛函數開始執行。它首先創建一個QTcpSocket類並置以從構造函數中傳入的套接字描述符,用來向客戶端傳回服務器端的當前時間。如果出錯,發出error(tcpSocket.error())信號報告錯誤;否則,開始獲取當前時間並將它傳回客戶端,然後斷開連接等待返回。
這裏介紹以下時間數據的傳輸格式,Qt雖然可以很方便的通過QDateTime類的靜態函數currentDateTime()獲取一個時間對象,但類結構是無法直接在網絡間傳輸的,此時需要將它轉換成一個標準的數據類型後再傳輸。幸好的是QDateTime類提供了uint toTime_t() const函數,這個函數返回當前自1970-01-01 00:00:00經過了多少秒,爲一個uint類型,可以將這個值傳輸給客戶端。在客戶端方面,使用QDateTime類void setTime_t(uint seconds)將這個時間還原。
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = 0);
public slots:
void showResult();
private:
QLabel *statusLabel;
QLabel *reqStatusLable;
QPushButton *quitButton;
TimeServer *server;
int count;
};
界面類Dialog比較簡單,它實際上就是一個對話框。在此定義了一個用於顯示請求次數的槽函數void showResult(),以及用於顯示監聽端口的標籤statusLabel,用於顯示請求次數的標籤reqStatusLabel,退出按鈕quitButton,TCP服務器server和請求次數計數器count。
Dialog::Dialog(QWidget *parent)
: QDialog(parent),count(0)
{
server = new TimeServer(this);
statusLabel = new QLabel;
reqStatusLable = new QLabel;
quitButton = new QPushButton(tr("退出"));
quitButton->setAutoDefault(false);
if (!server->listen()) {
QMessageBox::critical(this, tr("多線程時間服務器"),
tr("無法啓動服務器: %1.").arg(server->errorString()));
close();
return;
}statusLabel->setText(tr("時間服務器運行在端口: %1.\n")
.arg(server->serverPort()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch(1);
buttonLayout->addWidget(quitButton);
buttonLayout->addStretch(1);QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(statusLabel);
mainLayout->addWidget(reqStatusLable);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
setWindowTitle(tr("多線程時間服務器"));
}
構造函數Dialog完成了兩件事,一件是初始化界面,另一件是啓動服務器端的網絡監聽。
void Dialog::showResult()
{
reqStatusLable->setText(tr("第%1次請求完畢.\n").arg(++count));
}
槽函數showResult()功能十分簡單,它在標籤reqStatusLable上顯示當前的請求次數,並將請求計數count加1。