计算机系统应用教程网站

网站首页 > 技术文章 正文

从零开始学Qt(86):TCP服务器端程序设计

btikc 2024-10-24 09:33:21 技术文章 7 ℃ 0 评论

作为演示TCP通信的实例,创建了一个TCPClient程序和一个TCPServer程序。

本文首先介绍TCPServer程序,运行时界面如图所示。

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连接。

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

觉得有用的话请关注点赞,谢谢您的支持!

对于本系列文章相关示例完整代码有需要的朋友,可关注并在评论区留言!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表