Qt5 基於TCP傳輸的傳送/接收檔案伺服器(支援多客戶端)
一、實現功能
1、伺服器端選擇待發送的檔案,可以是多個
2、開啟伺服器,支援多客戶端接入,能夠實時顯示每個客戶端接入狀態
3、等待所有客戶端都處於已連線狀態時,依次傳送檔案集給每個客戶端,顯示每個客戶端傳送進度
4、傳送完成後等待接收客戶端發回的檔案,顯示接收進度
5、關閉伺服器
二、實現要點
先講一下實現上述功能的幾個關鍵點,明白的這幾個要點,功能的大框架就搭好了,細節在下一節再講
1、新建伺服器類testServer,繼承自QTcpServer
功能:用於接收客戶端TCP請求,儲存所有客戶端資訊,向主視窗傳送資訊
在這個類中例項化QTcpServer的虛擬函式:
void incomingConnection(int socketDescriptor); //虛擬函式,有tcp請求時會觸發
引數為描述socket ID的int變數
此函式在QTcpServer檢測到外來TCP請求時,會自動呼叫。
注:若要接收區域網內的TCP請求,連線newConnection訊號到自定義槽函式
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(acceptConnection()));
但是這種方法無法處理多客戶端
2、新建客戶端類testClient,繼承自QTcpSocket
功能:儲存每個接入的客戶端資訊,控制傳送/接收檔案,向testServer傳送資訊
在testServer的incomingConnection函式中,通過設定socketDescriptor,初始化一個testClient例項
testClient *socket = new testClient(this);
if (!socket->setSocketDescriptor(socketDescriptor))
{
emit error(socket->error());
return;
}
3、傳送資料
例項化testClient類,通過void writeData(const char * data, qint64 size)函式將資料寫入TCP流
testClient *a;
a->writeData("message",8);
一般來說,使用QByteArray和QDataStream進行格式化的資料寫入,設定流化資料格式型別為QDataStream::Qt_5_0,與客戶端一致
testClient *a; QByteArray outBlock; //快取一次傳送的資料 QDataStream sendOut(&outBlock, QIODevice::WriteOnly); //資料流 sendOut.setVersion(QDataStream::Qt_5_0); sendOut << "message"; a->writeData(outBlock,outBlock.size()); outBlock.resize(0);
注意outBlock如果為全域性變數,最後需要resize,否則下一次寫入時會出錯
在testClient建構函式中連線資料成功傳送後產生的bytesWritten()訊號
connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));
引數為成功寫入TCP流的位元組數
通過捕獲這個訊號,可以實現資料的連續傳送。因為傳送檔案時,資料是一塊一塊傳送的,比如設定一塊的大小為20位元組,那麼傳送完20位元組,怎麼繼續傳送呢?就靠的是這個函式,再用全域性變數記錄下總共傳送的位元組數,便可以控制傳送的結束。
4、傳送檔案
先把檔案讀到QFile變數中,再通過QBytesArray傳送
testClient *a; QFile sendFile = new QFile(sendFilePath);//讀取傳送檔案路徑 if (!sendFile->open(QFile::ReadOnly )) //讀取傳送檔案 { return;} QByteArray outBlock; outBlock = sendFile->read(sendFile.size()); a->writeData(outBlock,outBlock.size());
5、接收資料
依舊是testClient類,連線有readyRead()訊號,readyRead()訊號為新連線中有可讀資料時發出
connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));
首先將testClient連線例項(如testClient *a)封裝到QDataStream變數中,設定流化資料格式型別為QDataStream::Qt_5_0,與客戶端一致。
在讀入資料前,用a->bytesAvailable()檢測資料流中可讀入的位元組數,然後就可以通過"<<"操作符讀入資料了。
QDataStream in(a); //本地資料流
in.setVersion(QDataStream::Qt_5_0); //設定流版本,以防資料格式錯誤
quint8 receiveClass;
if(this->bytesAvailable() >= (sizeof(quint8)))
{
in >> receiveClass;
}
6、接收檔案
使用readAll讀入TCP流中所有資料,再寫入到QFile變數中
QByteArray inBlock;
inBlock = a->readAll(); //讀入所有資料
QFile receivedFile = new QFile(receiveFilePath); //開啟接收檔案
if (!receivedFile->open(QFile::WriteOnly ))
{ return;}
QFile receivedFile->write(inBlock); //寫入檔案
inBlock.resize(0);
三、具體實現
1、全域性定義
需要定義一些全域性變數和常量,定義如下
//儲存檔案路徑和檔名的結構體
struct openFileStruct
{
QString filePath;
QString fileName;
};
struct clientInfo //客戶端資訊
{
QString ip; //ip
int state; //狀態
QString id; //id
};
const quint8 sendtype_file = 0; //傳送型別是檔案
const quint8 sendtype_start_test = 10; //傳送型別是開始測試
const quint8 sendtype_msg = 20; //傳送型別是訊息
const quint16 filetype_list = 0; //檔案型別為列表
const quint16 filetype_wavfile = 1; //檔案型別為wav檔案
const QString clientStatus[7] = //客戶端狀態
{QObject::tr("Unconnected"),QObject::tr("HostLookup"),QObject::tr("Connecting"),
QObject::tr("Connected"),QObject::tr("Bound"),QObject::tr("Listening"),QObject::tr("Closing")};
openFileStruct 用於主視窗,儲存傳送檔案的路徑和檔名clientInfo 用於主視窗,儲存每個客戶端的資訊
規定傳輸協議為先發送一個quint8的標誌位,規定傳輸型別,即為sendtype定義的部分
sendtype為0時,傳送檔案。filetype為檔案型別
傳送檔案協議為:傳送型別 | 資料總大小 | 檔案型別 | 檔名大小 | 檔名 | 檔案內容
傳送檔案時,先發送一個檔案列表,每行一個檔名,再逐個傳送檔案
clientStatus為客戶端狀態,與Qt內部的socket status定義相同
2、testClient類
一個testClient類例項為一個socket客戶端描述
(1)類宣告:
class testClient : public QTcpSocket
{
Q_OBJECT
public:
testClient(QObject *parent = 0);
int num; //客戶端序號
void prepareTransfer(std::vector<openFileStruct>& openFileList,int testtype_t); //準備傳輸檔案
signals:
void newClient(QString, int, int); //客戶端資訊變更訊號
void outputlog(QString); //輸出日誌資訊
void updateProgressSignal(qint64,qint64,int); //更新發送進度訊號
void newClientInfo(QString,int,int); //更新客戶端資訊
private slots:
void recvData(); //接收資料
void clientConnected(); //已連線
void clientDisconnected(); //斷開連線
void newState(QAbstractSocket::SocketState); //新狀態
void startTransfer(const quint16); //開始傳輸檔案
void updateClientProgress(qint64 numBytes); //傳送檔案內容
void getSendFileList(QString path); //在指定路徑生成傳送檔案列表
quint64 getSendTotalSize(); //獲取sendFilePath對應檔案加上頭的大小
private:
//傳送檔案所需變數
qint64 loadSize; //每次接收的資料塊大小
qint64 TotalSendBytes; //總共需傳送的位元組數
qint64 bytesWritten; //已傳送位元組數
qint64 bytesToWrite; //待發送位元組數
QString sendFileName; //待發送的檔案的檔名
QString sendFilePath; //待發送的檔案的檔案路徑
QFile *sendFile; //待發送的檔案
QByteArray outBlock; //快取一次傳送的資料
int sendNum; //記錄當前傳送到第幾個檔案
int sendFileNum; //記錄傳送檔案個數
int totalSendSize; //記錄傳送檔案總大小
quint64 proBarMax; //傳送進度條最大值
quint64 proBarValue; //進度條當前值
std::vector<openFileStruct> openList; //傳送檔案列表
//接收檔案用變數
quint8 receiveClass; //0:檔案,1:開始測試,20:客戶端接收到檔案反饋
quint16 fileClass; //待接收檔案型別
qint64 TotalRecvBytes; //總共需接收的位元組數
qint64 bytesReceived; //已接收位元組數
qint64 fileNameSize; //待接收檔名位元組數
QString receivedFileName; //待接收檔案的檔名
QFile *receivedFile; //待接收檔案
QByteArray inBlock; //接收臨時儲存塊
qint64 totalBytesReceived; //接收的總大小
qint32 recvNum; //現在接收的是第幾個
quint64 recvFileNum; //接收檔案個數
quint64 totalRecvSize; //接收檔案總大小
bool isReceived[3]; //是否收到客戶端的接收檔案反饋
};
public為需要上層訪問的公有變數和函式
signals為向上層傳送的訊號,保證主視窗實時顯示資訊
private slots包括底層實現傳送、接收訊息的函式,以及訊號處理的槽
private為私有變數,包括髮送檔案和接收檔案所需的變數
(2)類定義:
建構函式:
testClient::testClient(QObject *parent) :
QTcpSocket(parent)
{
//傳送變數初始化
num = -1;
loadSize = 100*1024;
openList.clear();
//接收變數初始化
TotalRecvBytes = 0;
bytesReceived = 0;
fileNameSize = 0;
recvFileNum = 0;
totalRecvSize = 0;
totalBytesReceived = 0;
recvNum = 0;
receiveClass = 255;
fileClass = 0;
for(int i=0; i<3; i++)
isReceived[i] = false;
//連線訊號和槽
connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));
connect(this, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
connect(this, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this, SLOT(newState(QAbstractSocket::SocketState)));
connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));
}
初始化變數
連線qt的tcp訊號和自定義槽,包括:
readyRead() 接收訊息訊號
disconnected() 斷開連線訊號
stateChanged(QAbstractSocket::SocketState) 連線狀態變更訊號
bytesWritten(qint64) 已寫入傳送訊息流訊號
接收資料槽,設定伺服器只接收一個檔案:
void testClient::recvData() //接收資料,伺服器只接收客戶端的一個結果檔案
{
QDataStream in(this); //本地資料流
in.setVersion(QDataStream::Qt_5_0); //設定流版本,以防資料格式錯誤
QString unit;
qint32 msg;
if(bytesReceived <= (sizeof(quint8)))
{
if(this->bytesAvailable() >= (sizeof(quint8)))
{
in >> receiveClass;
}
switch(receiveClass)
{
case sendtype_file: //接收檔案
bytesReceived += sizeof(quint8);
qDebug() << "bytesReceived: " << bytesReceived;
break;
case sendtype_msg:
in >> msg;
if(msg == 0) //接收檔案列表
{
emit outputlog(tr("client %1 have received list file")
.arg(this->peerAddress().toString()));
}
else if(msg == 1) //接收檔案
{
emit outputlog(tr("client %1 have received file(s)")
.arg(this->peerAddress().toString()));
}
return;
default:
return;
}
}
if(bytesReceived >= (sizeof(quint8)) && bytesReceived <= (sizeof(quint8) + sizeof(qint64)*2 + sizeof(quint16))) //開始接收檔案,先接受報頭
{
//收3個int型資料,分別儲存總長度、檔案型別和檔名長度
if( ( this->bytesAvailable() >= (sizeof(qint64)*2 + sizeof(quint16)) ) && (fileNameSize == 0) )
{
in >> TotalRecvBytes >> fileClass >> fileNameSize;
bytesReceived += sizeof(qint64)*2; //收到多少位元組
bytesReceived += sizeof(quint16);
if(fileClass == filetype_result)
{
recvNum = 1;
recvFileNum = 1;
totalRecvSize = TotalRecvBytes; //只有一個檔案,檔案總大小為該檔案傳送大小
totalBytesReceived += sizeof(qint64)*2;
totalBytesReceived += sizeof(quint16);
totalBytesReceived += sizeof(quint8);
emit newClientInfo(tr("receiving result"),num,4);
}
else
{
QMessageBox::warning(NULL,tr("WARNING"),tr("client %1 send wrong type of file to server")
.arg(this->peerAddress().toString()));
return;
}
}
//接著收檔名並建立檔案
if((this->bytesAvailable() >= fileNameSize)&&(fileNameSize != 0))
{
in >> receivedFileName;
bytesReceived += fileNameSize;
totalBytesReceived += fileNameSize;
QString receiveFilePath = receive_path + "/" + receivedFileName; //接收檔案路徑
emit outputlog(tr("receive from client %1\nsave as:%2")
.arg(this->peerAddress().toString())
.arg(receiveFilePath));
//建立檔案
receivedFile = new QFile(receiveFilePath);
if (!receivedFile->open(QFile::WriteOnly ))
{
QMessageBox::warning(NULL, tr("WARNING"),
tr("cannot open file %1:\n%2.").arg(receivedFileName).arg(receivedFile->errorString()));
return;
}
}
else
{
return;
}
}
//一個檔案沒有接受完
if (bytesReceived < TotalRecvBytes)
{
//可用內容比整個檔案長度-已接收長度短,則全部接收並寫入檔案
qint64 tmp_Abailable = this->bytesAvailable();
if(tmp_Abailable <= (TotalRecvBytes - bytesReceived))
{
bytesReceived += tmp_Abailable;
totalBytesReceived += tmp_Abailable;
inBlock = this->readAll();
receivedFile->write(inBlock);
inBlock.resize(0);
tmp_Abailable = 0;
}
//可用內容比整個檔案長度-已接收長度長,則接收所需內容,並寫入檔案
else
{
inBlock = this->read(TotalRecvBytes - bytesReceived);
if(fileClass == filetype_wavfile)
{
totalBytesReceived += (TotalRecvBytes - bytesReceived);
}
bytesReceived = TotalRecvBytes;
receivedFile->write(inBlock);
inBlock.resize(0);
tmp_Abailable = 0;
}
}
emit updateProgressSignal(totalBytesReceived,totalRecvSize,num); //更新發送進度訊號
//善後:一個檔案全部收完則重置變數關閉檔案流,刪除指標
if (bytesReceived == TotalRecvBytes)
{
//變數重置
TotalRecvBytes = 0;
bytesReceived = 0;
fileNameSize = 0;
receiveClass = 255;
receivedFile->close();
delete receivedFile;
//輸出資訊
emit outputlog(tr("Have received file: %1 from client %2")
.arg(receivedFileName)
.arg(this->peerAddress().toString())); //log information
//全部檔案接收完成
if(recvNum == recvFileNum)
{
//變數重置
recvFileNum = 0;
recvNum = 0;
totalBytesReceived = 0;
totalRecvSize = 0;
emit outputlog(tr("Receive all done!"));
}
if(fileClass == filetype_result)
{
emit newClientInfo(tr("Evaluating"),num,4);
}
}
}
接收檔案時,需要一步一步判斷接收位元組是否大於協議的下一項,若大於則再判斷其值接收的檔案型別必須是filetype_result
未接收完記錄接收進度
接收完檔案進行善後,關閉檔案流刪除指標等
每進行完一步,向上層傳送訊號,包括客戶端資訊和接收進度
更新客戶端狀態函式,向上層傳送訊號
void testClient::newState(QAbstractSocket::SocketState state) //新狀態
{
emit newClient(this->peerAddress().toString(), (int)state, num);
}
傳送的訊號引數為:該客戶端IP,狀態序號,客戶端編號
開始傳輸檔案函式(傳送包含檔案資訊的檔案頭)
void testClient::startTransfer(const quint16 type) //開始傳輸檔案
{
TotalSendBytes = 0; //總共需傳送的位元組數
bytesWritten = 0; //已傳送位元組數
bytesToWrite = 0; //待發送位元組數
//開始傳輸檔案訊號
emit outputlog(tr("start sending file to client: %1\n filename: %2")
.arg(this->peerAddress().toString())
.arg(sendFileName));
sendFile = new QFile(sendFilePath);
if (!sendFile->open(QFile::ReadOnly )) //讀取傳送檔案
{
QMessageBox::warning(NULL, tr("WARNING"),
tr("can not read file %1:\n%2.")
.arg(sendFilePath)
.arg(sendFile->errorString()));
return;
}
TotalSendBytes = sendFile->size();
QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_5_0);
//寫入傳送型別,資料大小,檔案型別,檔名大小,檔名
sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;
TotalSendBytes += outBlock.size();
sendOut.device()->seek(0);
sendOut << quint8(sendtype_file)<< TotalSendBytes << quint16(type)
<< qint64((outBlock.size() - sizeof(qint64) * 2) - sizeof(quint16) - sizeof(quint8));
this->writeData(outBlock,outBlock.size());
//this->flush();
bytesToWrite = TotalSendBytes - outBlock.size();
outBlock.resize(0);
}
讀取傳送檔案建立傳送檔案頭
用writeData將檔案頭寫入TCP傳送流,記錄已傳送位元組數
傳送檔案內容:
void testClient::updateClientProgress(qint64 numBytes) //傳送檔案內容
{
if(TotalSendBytes == 0)
return;
bytesWritten += (int)numBytes;
proBarValue += (int)numBytes;
emit updateProgressSignal(proBarValue,proBarMax,num); //更新發送進度訊號
if (bytesToWrite > 0)
{
outBlock = sendFile->read(qMin(bytesToWrite, loadSize));
bytesToWrite -= (int)this->writeData(outBlock,outBlock.size());
outBlock.resize(0);
}
else
{
sendFile->close();
//結束傳輸檔案訊號
if(TotalSendBytes < 1024)
{
emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3B")
.arg(this->peerAddress().toString())
.arg(sendFileName)
.arg(TotalSendBytes));
}
else if(TotalSendBytes < 1024*1024)
{
emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3KB")
.arg(this->peerAddress().toString())
.arg(sendFileName)
.arg(TotalSendBytes / 1024.0));
}
else
{
emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3MB")
.arg(this->peerAddress().toString())
.arg(sendFileName)
.arg(TotalSendBytes / (1024.0*1024.0)));
}
if(sendNum < openList.size()) //還有檔案需要傳送
{
if(sendNum == 0)
{
//QFile::remove(sendFilePath); //刪除列表檔案
proBarMax = totalSendSize;
proBarValue = 0;
}
sendFilePath = openList[sendNum].filePath;
sendFileName = openList[sendNum].fileName;
sendNum++;
startTransfer(filetype_wavfile);
}
else //傳送結束
{
emit newClientInfo(tr("send complete"),num,4);
TotalSendBytes = 0; //總共需傳送的位元組數
bytesWritten = 0; //已傳送位元組數
bytesToWrite = 0; //待發送位元組數
}
}
}
檔案未傳送完:記錄傳送位元組數,writeData繼續傳送,writeData一旦寫入傳送流,自動又進入updateClientProgress函式檔案已傳送完:發出訊號,檢測是否還有檔案需要傳送,若有則呼叫startTransfer繼續傳送,若沒有則發出訊號,更新客戶端資訊
準備傳輸檔案函式,被上層呼叫,引數為傳送檔案列表:
void testClient::prepareTransfer(std::vector<openFileStruct>& openFileList) //準備傳輸檔案
{
if(openFileList.size() == 0) //沒有檔案
{
return;
}
testtype_now = testtype_t;
isSendKeyword = false;
for(int i=0; i<2; i++)
isReceived[i] = false;
openList.clear();
openList.assign(openFileList.begin(),openFileList.end()); //拷貝檔案列表
QString sendFileListName = "sendFileList.txt";
QString sendFileListPath = temp_Path + "/" + sendFileListName;
getSendFileList(sendFileListPath); //在指定路徑生成傳送檔案列表
emit newClientInfo(tr("sending test files"),num,4); //更新主視窗測試階段
sendFilePath = sendFileListPath;
sendFileName = sendFileListName;
sendNum = 0; //傳送到第幾個檔案
proBarMax = getSendTotalSize();
proBarValue = 0;
startTransfer(filetype_list); //開始傳輸檔案
}
拷貝檔案列表生成傳送檔案列表檔案
更新主視窗資訊
開始傳輸列表檔案
上面呼叫的生成列表檔案函式如下:
void testClient::getSendFileList(QString path) //在指定路徑生成傳送檔案列表
{
sendFileNum = openList.size(); //記錄傳送檔案個數
totalSendSize = 0; //記錄傳送檔案總大小
for(int i = 0; i < sendFileNum; i++)
{
sendFileName = openList[i].fileName;
sendFilePath = openList[i].filePath;
totalSendSize += getSendTotalSize();
}
FILE *fp;
fp = fopen(path.toLocal8Bit().data(),"w");
fprintf(fp,"%d\n",sendFileNum);
fprintf(fp,"%d\n",totalSendSize);
for(int i = 0; i < sendFileNum; i++)
{
fprintf(fp,"%s\n",openList[i].fileName.toLocal8Bit().data());
}
fclose(fp);
}
被上面呼叫getSendTotalSize函式如下:
quint64 testClient::getSendTotalSize() //獲取sendFilePath對應檔案加上頭的大小
{
int totalsize;
//計算列表檔案及檔案頭總大小
QFile *file = new QFile(sendFilePath);
if (!file->open(QFile::ReadOnly )) //讀取傳送檔案
{
QMessageBox::warning(NULL, tr("WARNING"),
tr("can not read file %1:\n%2.")
.arg(sendFilePath)
.arg(file->errorString()));
return 0;
}
totalsize = file->size(); //檔案內容大小
QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_5_0);
//寫入傳送型別,資料大小,檔案型別,檔名大小,檔名
sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;
totalsize += outBlock.size(); //檔案頭大小
file->close();
outBlock.resize(0);
return totalsize;
}
3、testServer類
一個testClient類例項為一個socket伺服器端描述
(1)類宣告:
class testServer : public QTcpServer
{
Q_OBJECT
public:
testServer(QObject *parent = 0);
std::vector<testClientp> clientList; //客戶端tcp連線
std::vector<QString> ipList; //客戶端ip
int totalClient; //客戶端數
protected:
void incomingConnection(int socketDescriptor); //虛擬函式,有tcp請求時會觸發
signals:
void error(QTcpSocket::SocketError socketError); //錯誤訊號
void newClientSignal(QString clientIP,int state,int threadNum); //將新客戶端資訊發給主視窗
void updateProgressSignal(qint64,qint64,int); //更新發送進度訊號
void outputlogSignal(QString); //傳送日誌訊息訊號
void newClientInfoSignal(QString,int,int); //更新客戶端資訊
public slots:
void newClientSlot(QString clientIP,int state,int threadNum); //將新客戶端資訊發給主視窗
void updateProgressSlot(qint64,qint64,int); //更新發送進度槽
void outputlogSlot(QString); //傳送日誌訊息槽
void newClientInfoSlot(QString,int,int); //更新客戶端資訊
private:
int getClientNum(testClientp socket); //檢測使用者,若存在,返回下標,若不存在,返回使用者數
};
public:需要主視窗訪問的變數incomingConnection:接收tcp請求
signals:傳送客戶端資訊的訊號
public slots:接收下層testClient訊號的槽,並向上層主視窗傳送訊號
private:檢測使用者是否存在的輔助函式
(2)類定義:
建構函式:
testServer::testServer(QObject *parent) :
QTcpServer(parent)
{
totalClient = 0;
clientList.clear();
ipList.clear();
}
接收tcp請求函式:
void testServer::incomingConnection(int socketDescriptor)
{
testClient *socket = new testClient(this);
if (!socket->setSocketDescriptor(socketDescriptor))
{
QMessageBox::warning(NULL,"ERROR",socket->errorString());
emit error(socket->error());
return;
}
int num = getClientNum(socket); //檢測使用者,若存在,返回下標,若不存在,返回使用者數
socket->num = num; //記錄序號
emit newClientSignal(socket->peerAddress().toString(),(int)socket->state(),num); //將新客戶端資訊發給主視窗
//連線訊號和槽
connect(socket, SIGNAL(newClient(QString,int,int)), this, SLOT(newClientSlot(QString,int,int)));
connect(socket, SIGNAL(outputlog(QString)), this, SLOT(outputlogSlot(QString)));
connect(socket, SIGNAL(updateProgressSignal(qint64,qint64,int)),
this, SLOT(updateProgressSlot(qint64,qint64,int)));
connect(socket, SIGNAL(newClientInfo(QString,int,int)),
this, SLOT(newClientInfoSlot(QString,int,int)));
connect(socket, SIGNAL(readyToTest(int,int)), this, SLOT(readyToTestSlot(int,int)));
connect(socket, SIGNAL(startEvaluate(int,QString)), this, SLOT(startEvaluateSlot(int,QString)));
connect(socket, SIGNAL(evaluateComplete(int,QString,int)),
this, SLOT(evaluateCompleteSlot(int,QString,int)));
if(num == totalClient)
totalClient++;
}
通過setSocketDescriptor獲取當前接入的socket描述符檢測使用者是否存在,記錄序號
向主視窗傳送訊號,連線下層testClient訊號和接收槽
更新客戶端總數
檢測使用者是否存在的函式
int testServer::getClientNum(testClientp socket) //檢測使用者,若存在,返回下標,若不存在,加入列表
{
for(int i = 0; i < ipList.size(); i++)
{
qDebug() << "No." << i << "ip: " << ipList[i];
if(ipList[i] == socket->peerAddress().toString())
{
clientList[i] = socket;
return i;
}
}
clientList.push_back(socket); //存入客戶列表
ipList.push_back(socket->peerAddress().toString()); //存入ip列表
return totalClient;
}
其他傳送訊號的函式均為連線下層和上層的中轉,由於比較簡單就不贅述了4、主視窗
在主視窗(繼承自QMainWindow)繪製所需控制元件:
lineEdit_ipaddress:顯示伺服器IP
lineEdit_port:設定伺服器埠
listWidget_sendwav:傳送檔案列表
tableWidget_clientlist:客戶端資訊列表,顯示所有客戶端IP,接入狀態,傳送/接收進度條
textEdit_status:狀態列
pushButton_addwav:添加發送檔案按鈕
pushButton_deletewav:刪除傳送檔案按鈕
pushButton_startserver:開啟/關閉伺服器按鈕
pushButton_senddata:傳送資料按鈕
pushButton_deleteclient:刪除客戶端按鈕
主視窗需定義的變數
testServer *tcpServer; //tcp伺服器指標
bool isServerOn; //伺服器是否開啟
std::vector<openFileStruct> openFileList; //儲存目錄和檔名對
開啟/關閉伺服器按鈕
void testwhynot::on_pushButton_startserver_clicked() //開啟伺服器
{
if(isServerOn == false)
{
tcpServer = new testServer(this);
ui->comboBox_testtype->setEnabled(false); //開啟伺服器後不能更改測試型別
ui->pushButton_addwav->setEnabled(false); //不能更改wav列表
ui->pushButton_deletewav->setEnabled(false);
//監聽本地主機埠,如果出錯就輸出錯誤資訊,並關閉
if(!tcpServer->listen(QHostAddress::Any,ui->lineEdit_port->text().toInt()))
{
QMessageBox::warning(NULL,"ERROR",tcpServer->errorString());
return;
}
isServerOn = true;
ui->pushButton_startserver->setText(tr("close server"));
//顯示到狀態列
ui->textEdit_status->append(tr("%1 start server: %2:%3")
.arg(getcurrenttime())
.arg(ipaddress)
.arg(ui->lineEdit_port->text()));
//連結錯誤處理訊號和槽
//connect(this,SIGNAL(error(int,QString)),this,SLOT(displayError(int,QString)));
//處理多客戶端,連線從客戶端執行緒發出的訊號和主視窗的槽
//連線客戶端資訊更改訊號和槽
connect(tcpServer,SIGNAL(newClientSignal(QString,int,int)),this,SLOT(acceptNewClient(QString,int,int)));
//連線日誌訊息訊號和槽
connect(tcpServer,SIGNAL(outputlogSignal(QString)),
this,SLOT(acceptOutputlog(QString)));
//連線更新發送進度訊號和槽
connect(tcpServer, SIGNAL(updateProgressSignal(qint64,qint64,int)),
this, SLOT(acceptUpdateProgress(qint64,qint64,int)));
//連線更新客戶端資訊列表訊號和槽
connect(tcpServer, SIGNAL(newClientInfoSignal(QString,int,int)),
this, SLOT(acceptNewClientInfo(QString,int,int)));
//顯示到狀態列
ui->textEdit_status->append(tr("%1 wait for client...")
.arg(getcurrenttime()));
}
else
{
isServerOn = false;
ui->pushButton_startserver->setText(tr("start server"));
//斷開所有客戶端連線,發出disconnected()訊號
for(int i=0; i<tcpServer->clientList.size(); i++)
{
if(ui->tableWidget_clientlist->item(i,2)->text() == clientStatus[3]) //處於連線狀態才斷開,否則無法訪問testClientp指標
{
testClientp p = tcpServer->clientList[i];
p->close();
}
}
//清空列表
tcpServer->clientList.clear();
tcpServer->ipList.clear();
clientInfoList.clear();
for(int i=0; i<ui->tableWidget_clientlist->rowCount(); )
ui->tableWidget_clientlist->removeRow(i);
tcpServer->close(); //關閉伺服器
ui->textEdit_status->append(tr("%1 clost server.").arg(getcurrenttime()));
ui->comboBox_testtype->setEnabled(true); //可以重新選擇測試型別
ui->pushButton_addwav->setEnabled(true); //能更改wav列表
ui->pushButton_deletewav->setEnabled(true);
//按鈕無效化
ui->pushButton_senddata->setEnabled(false);
ui->pushButton_starttest->setEnabled(false);
ui->pushButton_deleteclient->setEnabled(false);
}
}
注意:1、listen(“監聽型別”,“埠號”) 用於開啟伺服器,在指定埠監聽客戶端連線
2、connect連線了客戶端更新資訊的訊號,來自下層testServer類
傳送資料按鈕
void testwhynot::on_pushButton_senddata_clicked() //傳送資料
{
//多客戶端傳送資料
if(ui->comboBox_testtype->currentIndex() == testtype_keyword)
{
if(!check_file(keyword_filepath))
return;
}
vector<clientInfo>::iterator it;
//遍歷所有客戶端資訊,確保都處於“已連線”狀態
for(it=clientInfoList.begin(); it!=clientInfoList.end(); it++)
{
if((*it).state != 3) //有客戶端未處於“已連線”狀態
{
QMessageBox::warning(this,tr("WARNING"),tr("client %1 is not connected.").arg((*it).ip));
return;
}
}
//沒有檔案
if(openFileList.size() == 0)
{
QMessageBox::warning(NULL,tr("WARNING"),tr("no file! can not send!"));
return;
}
for(int i = 0; i < tcpServer->clientList.size(); i++)
{
tcpServer->clientList[i]->prepareTransfer(openFileList,ui->comboBox_testtype->currentIndex());
}
//按鈕無效化
ui->pushButton_senddata->setEnabled(false);
}
呼叫底層prepareTransfer函式開始傳輸刪除客戶端按鈕
void testwhynot::on_pushButton_deleteclient_clicked()
{
QList<QTableWidgetItem*> list = ui->tableWidget_clientlist->selectedItems(); //讀取所有被選中的item
if(list.size() == 0) //沒有被選中的就返回
{
QMessageBox::warning(this,QObject::tr("warning"),QObject::tr("please select a test client"));
return;
}
std::set<int> del_row; //記錄要刪除的行號,用set防止重複
for(int i=0; i<list.size(); i++) //刪除選中的項
{
QTableWidgetItem* sel = list[i]; //指向選中的item的指標
if (sel)
{
int row = ui->tableWidget_clientlist->row(sel); //獲取行號
del_row.insert(row);
}
}
std::vector<int> del_list; //賦值給del_list,set本身為有序
for(std::set<int>::iterator it=del_row.begin(); it!=del_row.end(); it++)
{
del_list.push_back(*it);
}
for(int i=del_list.size()-1; i>=0; i--) //逆序遍歷
{
testClientp p = tcpServer->clientList[del_list[i]];
p->close();
ui->tableWidget_clientlist->removeRow(del_list[i]); //從顯示列表中刪除行
clientInfoList.erase(clientInfoList.begin() + del_list[i]); //從內部列表刪除
tcpServer->ipList.erase(tcpServer->ipList.begin() + del_list[i]);
tcpServer->clientList.erase(tcpServer->clientList.begin() + del_list[i]);
tcpServer->totalClient--;
}
for(int i=0; i<tcpServer->clientList.size(); i++)
{
tcpServer->clientList[i]->num = i;
}
if(clientInfoList.empty()) //沒有客戶端,刪除按鈕無效化
{
ui->pushButton_deleteclient->setEnabled(false);
}
}
刪除客戶端較麻煩,由於支援多選刪除,需要先將選中的行號排序、去重、從大到小遍歷刪除(防止刪除後剩餘行號變化)不僅要關閉客戶端,還要將其從顯示列表和內部列表刪除,保持顯示和實際列表同步
上述便是基於tcp傳輸的傳送/接收伺服器端的搭建,客戶端只需遵從上述的傳送協議搭建即可,客戶端傳送與接收與伺服器基本相同,也不贅述了。
本文偏重於工程實現,Qt的TCP傳輸原理敘述不多,若要深入瞭解qt套接字程式設計,請參考:http://cool.worm.blog.163.com/blog/static/64339006200842922851118/
相關推薦
Qt5 基於TCP傳輸的傳送/接收檔案伺服器(支援多客戶端)
一、實現功能 1、伺服器端選擇待發送的檔案,可以是多個 2、開啟伺服器,支援多客戶端接入,能夠實時顯示每個客戶端接入狀態 3、等待所有客戶端都處於已連線狀態時,依次傳送檔案集給每個客戶端,顯示每個客戶端傳送進度 4、傳送完成後等待接收客戶端發回的檔案,顯示接收進度 5、關閉
基於Tcp穿越的Windows遠端桌面(遠端桌面管理工具)
基於Tcp穿越的Windows遠端桌面(遠端桌面管理工具) 距離上一篇文章《基於.NET環境,C#語言 實現 TCP NAT》已經很久沒有發隨筆了,而且文章閱讀量不高,可能很多人對Tcp穿越不感興趣。今天這篇文章是基於上一篇文章做的應用,一個遠端桌面管理
java多執行緒通訊(伺服器與多客戶端)
基於TCP的多執行緒通訊 伺服器執行緒: package com.netproject1; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOExc
delphi獲取音視訊媒體檔案資訊(支援D7,XE7)
福利來了,delphi獲取音視訊媒體檔案資訊,親測支援D7和XE7。支援音訊檔案(*.ACC;*.AC3;*.APE;*.DTS;*.FLAC;*.M4A;*.MKA;*.MP2;*.MP3;*.MPA;*.PMC;*.OFR;*.OGG;*.RA;*.TTA;*.WAV;
Unity 與C#伺服器 實現Socket的UDP通訊(多客戶端)
前言 上一篇簡單的介紹了下Unity客戶端和伺服器的Socket通訊,但是還不能實現多個客戶端與伺服器的通訊,所以今天在這邊把前面的工程完善一下(使用的是上篇講到的UdpClient類來實現),實現多個客戶端與伺服器的udp通訊。效果圖如下,兩個客戶端可以向伺服器傳送訊息,
C#.網路程式設計 Socket基礎(三) 基於WinForm系統Socket TCP協議 實現端到端(伺服器與客戶端).txt.word.png等不同型別檔案傳輸
一、簡介: 前面的兩篇介紹了字串傳輸、圖片傳輸: 其實,本文針對Socket基礎(二)進一步完成,以便可以進行多種檔案傳輸。 二、基於不同的流(檔案流、記憶體流、網路等)讀寫。 1、圖片傳輸 方法一:(在客戶端用檔案流傳送(即將圖片寫到檔案流去,以便傳送),
Windows下基於TCP協議的大檔案傳輸(流形式)
在TCP下進行大檔案傳輸,不像小檔案那樣直接打包個BUFFER傳送出去,因為檔案比較大可能是1G,2G或更大,第一效率問題,第二TCP粘包問題。針對服務端的設計來說就更需要嚴緊些。下面介紹簡單地實現大檔案在TCP的傳輸應用。 粘包出現原因:在流傳輸中出現,UDP不會出現粘包,因為它有訊息邊界(參考Wi
虛擬機器主機linux(unbuntu)和開發板使用串列埠連線以及傳送接收檔案
一、串列埠使用背景 基本上檔案都是用tftp、nfs協議上傳和接收,不過這個需要使用到網路,相當於佔用網線口,不過相對而言,檔案上傳速度較快,對於檔案小的檔案(<1M大小),建議使用minicom工具;對於大檔案,推薦使用tftp或者nfs工具。 二、minicom工具 1、linux
Python網路程式設計--通過fork、tcp併發完成ftp檔案伺服器
ftp檔案伺服器 一、專案功能 1.服務端和客戶端兩部分,要求啟動一個服務端可以同時處理多個客戶端請求 2.功能:1).可以檢視服務端檔案庫中所有的普通檔案 &nbs
網路程式設計--使用TCP協議傳送接收資料
package com.zhangxueliang.tcp; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; /*** * 使用
TCP檔案伺服器 (*****領卓教育******)
server int process_list(int newsockfd,char *buf) //設定要共享的目錄 { DIR * dirp; int ret; struct dirent * direntp; dirp = opendir("./");/
基於TCP傳輸的粘包問題
1我們都知道TCP傳輸,是基於位元組流傳輸的,所以流與流直接傳輸就會產生邊界問題,我個人對粘包的理解就是,TCP傳輸無法獲悉不同包與包之間的“界限”。 如果對等接受方彼此直接沒有約定好傳輸資料大小的話,就會出現解析資料不準確問題,而且傳輸資料小於約定大小空間的話,也會出現浪費空間問題,為了解
Android TCP/IP 傳送接收16進位制資料
幫朋友推薦,贏眾投理財,CEO是我朋友,全是真實可靠的農業專案,投資收益可達年化9.8%,且有多重安全保障! // 設定伺服器IP和埠private static final String SERVERIP_2 ="192.168.5.178"; p
java網路程式設計基於TCP的多客戶端連線伺服器
package com.test.net; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.S
Java網路程式設計TCP協議傳送接收資料
一、客戶端傳送,伺服器端接收 package net; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * TC
服務端接收到客戶端傳送的檔名,並把檔案的內容返回給客戶端
public class ScoketService {public static void server() {System.out.println("-------------服務已啟動-------------");ServerSocket serverSocket = null;try {server
System V訊息佇列實現的檔案伺服器(不跨網路)
可能是定時的部分有問題吧,導致客戶端無法接收資料,不過我感覺思想是沒錯的。。。先pull上吧,以後發現錯誤再改 參考資料:UNP卷二 message.h #ifndef _MESSAGE_H #define _MESSAGE_H #include<stdio.h> #i
[原始碼和文件分享]基於java語言的FTP伺服器(Ping測試工具軟體)
一 需求分析 已知引數:目的節點IP地址或主機名 設計要求:通過原始套接字程式設計,模擬Ping命令,實現其基本功能,即輸入一個IP地址或一段IP地址的範圍,分別測試其中每個IP地址所對應主機的可達性,並返回耗時、生存時間等引數,並統計成功傳送和回送的Ping報文
基於TCP的C/S模式模板(Winsock實現)
伺服器程式server.cpp: #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <tchar.h> #include <locale.h> #include <WinSo
C#.網路程式設計 Socket基礎(一)Socket TCP協議 實現端到端(伺服器與客戶端)簡單字串通訊
簡介: 本章節主要討論了Socket的入門知識,還未針對Socket的難點問題(比如TCP的無訊息邊界問題)展開討論,往後在其他章節中進行研究。 注意點: 伺服器(比如臺式電腦)的IP為1.1.1.2,那麼客戶端(其他裝置,比如手機,Ipad)連線的一定是