如何使用QT5編寫一個利用TCP協議的聊天室 (一)
關于Tcp協議的知識點
TCP協議是一種基于傳輸層的協議,具有可靠性,需要連接,工作方式為全雙工,傳輸速度相較于UPD更慢的特點,一般用于傳輸大量的數據,傳輸過程不允許丟包的情況.一般情況下聊天類軟件均采用UDP協議,此處采用TCP是為了了解TCP的特點,以及保證實驗過程不因為丟包影響實驗結果.
實驗思路
我將服務器端分為三個部分,每個部分分別實現不同的功能.
第一部分:服務器端的外形設計,服務器端應有一個對話框顯示客戶端的登入登出以及在登入期間所發送的信息,一個文本框顯示端口號,一個按鈕來開啟服務器
第二部分:服務器功能,服務器需要監聽一個固定的端口,當有客戶端接入該端口時,創建一個TCP套接字對象來為該客戶服務(以便在服務端實現與客戶端的通信),服務器還需要能存儲每一個接入的客戶端的信息
第三部分:一個TCP套接字對象的功能,該對象需要能讀取套接字中的數據,將其傳輸給服務器,
具體實現
以下代碼為.cpp中的部分代碼,代碼參考<<Qt5開發及實例(第三版)>>,若內容有錯誤理解希望能提出指正
第一部分:
1 Tcpserver::Tcpserver(QWidget *parent,Qt::WindowFlags f)//構造函數 2 : QDialog(parent,f) 3 { 4 setWindowTitle(tr("TCP Server"));//更改名稱 5 ContentListWidget = new QListWidget;//初始化一個對話框,顯示相應的信息 6 PortLabel = new QLabel(tr("端口"));初始化一個標簽,讓其顯示相應的內容 7 PortLineEdit = new QLineEdit;//初始化一個對話框顯示固定的端口號 8 CreateBtn = new QPushButton(tr("create chat room"));//初始化一個按鈕,讓其顯示相應的內容 9 mainLayout = new QGridLayout(this);//初始化一個QGridLayout的布局,并將所有部件塞進布局10 mainLayout->addWidget(ContentListWidget,0,0,1,2);11 mainLayout->addWidget(PortLabel,1,0);12 mainLayout->addWidget(PortLineEdit,1,1);13 mainLayout->addWidget(CreateBtn,2,0,1,2);14 port = 8010;//監聽8010端口15 PortLineEdit->setText(QString::number(port));//將端口號顯示16 connect(CreateBtn,SIGNAL(clicked()),this,SLOT(slotCreateServer()));//按下按鈕發送信號,觸發槽函數,該槽函數功能為創建一個服務器17 }18 TcpServer::~Tcpserver()//析構函數19 {20 }21 void Tcpserver::slotCreateServer()//槽函數的實現22 {23 server = new Server(this,port);//創建一個Server對象,并將端口號傳給該對象,是其對該端口進行監聽24 connect(server,SIGNAL(updateServer(QString,int)),this,SLOT(updateServer(QString,int)));//若server發送updateServer信號,觸發tcpserver的槽函數updateServer25 //使其更新對話框內容26 CreateBtn->setEnabled(false);//創建服務器之后 無法再點擊該按鈕27 }28 void Tcpserver::updateServer(QString msg,int length)//更新內容的槽函數29 {30 ContentListWidget->addItem(msg.left(length));31 }
第二部分:
1 Server::Server(QObject *parent,int port) : QTcpServer(parent) 2 { 3 listen(QHostAddress::Any,port);//在指定的端口對任意地址監聽 4 } 5 void Server::incomingConnection(qintptr socketDescriptor)//當出現一個新的連接的時候,TcpServer便會觸發incomingConnection()函數,每有一個新的連接都會觸發一次,即都創建一個新的對象 6 { 7 TcpClientSocket *tcpClientSocket = new TcpClientSocket(this);//創建一個新的TcpClientSocket與客戶端通信 8 //出現一個新連接才會創建一個 TcpClientSocket 的對象,如果連接后有數據傳入,該對象將其讀下來 通過updateClient信號來發送 9 connect(tcpClientSocket,SIGNAL(updateClients(QString,int)),this,SLOT(updateClients(QString,int)));//連接TcpClientSocket的updateClients信號10 //當有新數據傳入后,TcpClientSocket便會發出updateClients的信號,此時觸發Server中的updateClients的槽函數,將信號中的msg與length傳入該槽函數11 connect(tcpClientSocket,SIGNAL(disconnected(int)),this,SLOT(slotDisconnected(int)));//連接TcpClientSocket中的disconnected信號12 //當TcpClientSocket的對象斷開連接后,發出disconnected的信號,此時觸發Server中的slotDisconnected的槽函數13 tcpClientSocket->setSocketDescriptor(socketDescriptor);//將新創建的TcpClientSocket的套接字描述符指定為參數socketDescriptor14 tcpClientSocketList.append(tcpClientSocket);//將tcpClientSocket這個對象添加入tcpClientSocketList這個列表中15 }16 void Server::updateClients(QString msg,int length)17 {18 emit updateServer(msg,length);//發出updateServer的信號,通知服務器對話框更新相應的顯示狀態19 for(int i = 0;i<tcpClientSocketList.count();i )//實現廣播,即將新發送到服務器中的數據進行廣播,發送到每一個連接的對象,進行同步更新對話框20 {21 QTcpSocket *item = tcpClientSocketList.at(i);//22 if(item->write(msg.toLatin1(),length)!=length)23 {24 continue;25 }26 }27 }28 void Server::slotDisconnected(int descriptor)//從tcpClientSocketList列表中將斷開連接的TcpClientSocket對象刪除29 {30 for (int i = 0;i<tcpClientSocketList.count();i ) {31 QTcpSocket *item = tcpClientSocketList.at(i);32 if(item->socketDescriptor()==descriptor)33 {34 tcpClientSocketList.removeAt(i);35 return;36 }37 }38 return;39 }
第三部分:
1 TcpClientSocket::TcpClientSocket(QObject *parent) 2 { 3 connect(this,SIGNAL(readyRead()),this,SLOT(dateReceived()));//readyRead()是QIODevice的一個信號函數,由QTcpSocket繼承而來,在有數據來時發出信號 4 connect(this,SIGNAL(disconnected()),this,SLOT(slotDisconnected()));//disconnected()是QIODevice的一個信號函數,由QTcpSocket,斷開連接時發出信號 5 } 6 void TcpClientSocket::dateReceived() 7 { 8 while(bytesAvailable()>0)//當有數據來時,bytesAvailable()從套接字中檢測所來的數據,返回等待讀取的傳入字節數 9 {10 int length = bytesAvailable();11 char buf[1024];12 read(buf,length);//將套接字中的數據讀取到buf中,讀取長度為length13 QString msg = buf;14 emit updateClients(msg,length);//發出信號15 }16 }17 void TcpClientSocket::slotDisconnected()18 {19 emit disconnected(this->socketDescriptor());//發出disconnected信號20 }
部分代碼詳細解析
1.監聽
監聽主要依托listen()函數實現,listen()函數由QTcpServer的對象調用,接受兩個參數,第一個參數為QHostAddress的枚舉,上文中的QHostAddress::Any為任意地址的意思,包括IPv4和IPv6.而第二個參數為整型的類型,用來代表端口,listen()函數返回一個布爾類型來表示監聽是否成功.
2.連接
當客戶端連接到端口時(如何連接將在客戶端實驗在闡述),由于已經建立了監聽的關系,我們便可以利用一個QTcpServer中一個虛函數–incomingConnection(),該函數當有服務器端連入時便會觸發,我們可以通過重寫該函數來完成客戶端接入的信息傳遞,該函數接受一個qintstr 類型的參數,該參數代表了接受連接的本機套接字描述符,該參數的作用將在讀取數據時體現
3.讀取數據
我們想要接受客戶端發送的信息,除了監聽相應的端口,與之建立連接還需要一個服務器端的套接字來接受客戶端發送的數據,創建一個套接字我們可以使用QTcpSocket 的對象來使用一個setSocketDescriptor()函數,該函數的作用為初始化套接字,接受一個qintstr的參數(就是在在上文中提到的那個)來指明該套接字接受哪個的數據,當成功創建了一個套接字時,我們便可以采用readyRead()函數來發出信號,該函數當有新的可讀數據在套接字時,便可以發出信號,為之我們建立相應的槽函數.在槽函數中可以使用byteAvailable()函數來獲取可讀取數據的字節數,而正式讀取數據時使用read()函數便可以了,read()函數有兩個參數,第一個參數為讀取數據的存儲位置,第二個參數為讀取多少數據,我們可以使用byteAvailable()返回的值來完成全部讀取.