天天看點

從零開始學Qt(86):TCP伺服器端程式設計

作者:未來奇兵

作為示範TCP通信的執行個體,建立了一個TCPClient程式和一個TCPServer程式。

本文首先介紹TCPServer程式,運作時界面如圖所示。

從零開始學Qt(86):TCP伺服器端程式設計

TCPServer程式具有如下的功能:

  • 根據指定IP位址(本機位址)和端口打開網絡監聽,有用戶端連接配接時建立socket連接配接;
  • 采用基于行的資料通信協定,可以接收用戶端發來的消息,也可以向用戶端發送消息;
  • 在狀态欄顯示伺服器監聽狀态和socket的狀态。

1. 主視窗定義與構造函數

TCPServer是一個視窗基于QMainWindow的應用程式,界面由UI設計器設計,MainWindow類的定義如下(忽略了 UI設計器自動生成的actions和按鈕的槽函數):

class MainWindow : public QMainWindow
{
	Q_OBJECT
public:
  MainWindow(QWidget *parent = nullptr);
  ~MainWindow();
protected:
	void closeEvent(QCloseEvent *event);
private:
  Ui::MainWindow *ui;
  QLabel *LabListen;//狀态欄标簽
  QLabel *LabSocketState;//狀态欄标簽
  QTcpServer *tcpServer; //TCP 伺服器
  QTcpSocket *tcpSocket;//TCP 通信的 Socket
  QString getLocalIP();//擷取本機 IP 位址
private slots:
//自定義槽函數
  void onNewConnection();//QTcpServer的newConnection ()信号
  void onSocketStateChange(QAbstractSocket::SocketState socketState);
  void onClientConnected(); //Client Socket connected
  void onClientDisconnected();//Client Socket disconnected
  void onSocketReadyRead();//讀取 socket 傳入的資料
};           

MainWindow中定義了私有變量tcpServer用于建立TCP伺服器,定義了 tcpSocket用于與用戶端進行socket連接配接和通信。

定義了幾個槽函數,用于與QTcpServer和QTcpSocket的相關信号連接配接,實作相應的處理。

MainWindow構造函數代碼如下:

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  LabListen=new QLabel("監聽狀态:");
  LabListen->setMinimumWidth(150);
  ui->statusbar->addWidget(LabListen);
  LabSocketState=new QLabel ("Socket 狀态:");
  LabSocketState->setMinimumWidth(200);
  ui->statusbar->addWidget(LabSocketState);
  QString localIP=getLocalIP(); //本機 IP
  this->setWindowTitle(this->windowTitle()+" 本機IP: "+localIP);
  ui->comboIP->addItem(localIP);
  tcpServer=new QTcpServer(this);
  connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

QString MainWindow::getLocalIP()
{//擷取本機IPv4位址
  QString hostName=QHostInfo::localHostName(); //本地主機名
  QHostInfo hostInfo=QHostInfo::fromName(hostName);
  QString localIP="";
  QList<QHostAddress> addList=hostInfo.addresses();
  if(!addList.isEmpty()){
    for(int i=0;i<addList.count();i++){
      QHostAddress aHost=addList.at(i);
      if(QAbstractSocket::IPv4Protocol==aHost.protocol()){
      	localIP=aHost.toString();
      	break;
    	}
  	}
  	return localIP;
	}
}           

MainWindow的構造函數建立狀态欄上的标簽用于資訊顯示,調用自定義函數getLocalIP()擷取本機IP位址,并顯示到标題欄上。建立QTcpServer執行個體tcpServer,并将其newConnection()信号與onNewConnection()槽函數關聯。

2.網絡監聽與socket連接配接的建立

作為TCP伺服器,QTcpServer類需要調用listen()在本機某個IP位址和端口上開始TCP監聽,以等待TCP用戶端的接入。單擊主視窗上“開始監聽”按鈕可以開始網絡監聽,其代碼如下:

void MainWindow::on_actionStart_triggered()
{//開始監聽
  QString IP=ui->comboIP->currentText();//IP位址
  quint16 port=ui->spinPort->value();//端口
  QHostAddress addr(IP);
  tcpServer->listen(addr, port); //開始監聽
  ui->plainTextEdit->appendPlainText("**開始監聽...");
  ui->plainTextEdit->appendPlainText("**伺服器位址:"+tcpServer->serverAddress().toString());
  ui->plainTextEdit->appendPlainText ("伺服器端口:"+QString::number(tcpServer->serverPort()));
  ui->actionStart->setEnabled(false);
  ui->actionStop->setEnabled(true);
  LabListen->setText("監聽狀态:正在監聽");
}           

程式讀取視窗上設定的監聽位址和監聽端口,然後調用QTcpServer的listen()函數開始監聽。 TCP伺服器在本機上監聽,是以IP位址可以是表示本機的“127.0.0.1”,或是本機的實際IP,亦或是常量QHostAddress::LocalHost,即在本機上監聽某個端口也可以寫成:

tcpServer->listen(QHostAddress::LocalHost,port);           

tcpServer開始監聽後,TCPClient就可以通過IP位址和端口連接配接到此伺服器。當有用戶端接 入時,tcpServer會發射newConnection()信号,此信号關聯的槽函數onNewConnection()的代碼如下:

void MainWindow::onNewConnection()
{
  tcpSocket = tcpServer->nextPendingConnection() ; //擷取socket
  connect(tcpSocket, SIGNAL(connected()),
  		this, SLOT(onClientConnected()));
  onClientConnected();
  connect(tcpSocket, SIGNAL(disconnected()),
  		this, SLOT(onClientDisconnected()));
  connect(tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
  		this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
  onSocketStateChange(tcpSocket->state());
  connect(tcpSocket,SIGNAL(readyRead()),
  		this,SLOT(onSocketReadyRead()));
}           

程式首先通過nextPendingConnection()函數擷取與接入連接配接進行通信的QTcpSocket對象執行個體tcpSocket,然後将tcpSocket的幾個信号與相應的槽函數連接配接起來。

QTcpSocket的這幾個信号的作用是:

  • connected()信号,用戶端socket連接配接建立時發射此信号;
  • disconnected()信号,用戶端socket連接配接斷開時發射此信号;
  • stateChanged(),本程式的socket狀态變化時發射此信号;
  • readyRead(),本程式的socket的讀取緩沖區有新資料時發射此信号。

涉及狀态變化的幾個信号的槽函數代碼如下:

void MainWindow::onClientConnected()
{//用戶端接入時
  ui->plainTextEdit->appendPlainText("**client socket connected");
  ui->plainTextEdit->appendPlainText("**peer address:"
  		+tcpSocket->peerAddress().toString());
  ui->plainTextEdit->appendPlainText("**peer port: "
  		+QString::number(tcpSocket->peerPort()));
}

void MainWindow::onClientDisconnected()
{//用戶端斷開連接配接時
  ui->plainTextEdit->appendPlainText("**client socket disconnected");
  tcpSocket->deleteLater();
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket狀态變化時
  switch(socketState){
  case QAbstractSocket::UnconnectedState:
  	LabSocketState->setText("socket 狀态:UnconnectedState"); break;
  case QAbstractSocket::HostLookupState:
  	LabSocketState->setText("scoket狀态:HostLookupState"); break;
  case QAbstractSocket::ConnectingState:
  	LabSocketState->setText("scoket狀态:ConnectingState"); break;
  case QAbstractSocket::ConnectedState:
  	LabSocketState->setText("scoket狀态:ConnectedState"); break;
  case QAbstractSocket::BoundState:
  	LabSocketState->setText("scoket狀态:BoundState"); break;
  case QAbstractSocket::ClosingState:
  	LabSocketState->setText("scoket ClosingState"); break;
  case QAbstractSocket::ListeningState:
  	LabSocketState->setText("scoket 狀态:ListeningState");
  }
}           

TCP伺服器停止監聽,隻需調用QTcpServer的close()函數即可。視窗上的“停止監聽”響應代碼如下:

void MainWindow::on_actionStop_triggered()
{//停止監聽
  if(tcpServer->isListening()) //tcpServer 正在監聽
  tcpServer->close(); //停止監聽
  ui->actionStart->setEnabled(true);
  ui->actionStop->setEnabled(false);
  LabListen->setText("監聽狀态:己停止監聽");
}           

3.與TCPClient的資料通信

TCP伺服器端和用戶端之間通過QTcpSocket通信時,需要規定兩者之間的通信協定,即傳輸的資料内容如何解析。QTcpSocket間接繼承于QIODevice,是以支援流讀寫功能。

Socket之間的資料通信協定一般有兩種方式,基于行的或基于資料塊的。

基于行的資料通信協定一般用于純文字資料的通信,每一行資料以一個換行符結束。 canReadLine()函數判斷是否有新的一行資料需要讀取,再用readLine()函數讀取一行資料,例如:

while(tcpClient->canReadLine())
{
	ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}           

基于塊的資料通信協定用于一般的二進制資料的傳輸,需要自定義具體的格式。

執行個體程式TCPServer和TCPClient隻是進行字元串的資訊傳輸,類似于一個簡單的聊天程式, 程式采用基于行的資料通信協定。

單擊視窗上的“發送消息”,将文本框裡的字元串發送給用戶端,其實作代碼如下:

void MainWindow::on_btnSend_clicked()
{ //發送一行字元串,以換行符結束
  QString msg=ui->edtMsg->text();
  ui->plainTextEdit->appendPlainText("[out] "+msg);
  ui->edtMsg->clear();
  ui->edtMsg->setFocus();
  QByteArray str=msg.toUtf8();
  str.append('\n');//添加一個換行符
  tcpSocket->write(str);
}           

從上面的代碼中可以看到,讀取文本框中的字元串到msg後,先将其轉換為QByteArray類型位元組數組str,然後在str最後面添加一個換行符,用QIODevice的write()函數寫入緩沖區,這樣就向用戶端發送一行文字。

QTcpSocket接收到資料後,會發射readyRead()信号,在onNewConnection()槽函數中己經建 立了這個信号與槽函數onSocketReadyRead()的連接配接。

槽函數onSocketReadyRead()實作緩沖區資料的讀取,其代碼如下:

void MainWindow::onSocketReadyRead()
{ //讀取緩沖區行文本
	while(tcpSocket->canReadLine())
		ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}           

這樣,TCPServer就可以與TCPClient之間進行雙向通信了,且這個連接配接将一直存在,直到某一方的QTcpSocket對象調用disconnectFromHost()函數斷開socket連接配接。

————————————————

覺得有用的話請關注點贊,謝謝您的支援!

對于本系列文章相關示例完整代碼有需要的朋友,可關注并在評論區留言!