1. 开发流程
1.1 Qtcp服务器的关键步骤
1.2 QTtcp客户端的关键开发步骤
2. TCP协议
1.2三次握手(建立连接)
1.3四次挥手(断开连接)
3. Socket
4. 创建QTtcp服务端
4.1 ui
4.2 代码
• widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QAbstractSocket> #include <QDebug> #include <QMessageBox> #include <QNetworkInterface> #include <QTcpServer> #include <QTcpSocket> #include "mycombobox.h" Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); //界面初始化 this->setLayout(ui->verticalLayout); this->setFixedSize(796,746); //btnStopListen,btnLineout和btnSend这三个按键在未连接的时候是不能使用的 ui->btnStopListen->setEnabled(false); ui->btnLineout->setEnabled(false); ui->btnSend->setEnabled(false); //创建TCP服务器 server = new QTcpServer(this);//在当前窗口 //创建信号与槽,当有客户端来连接时,就触发这个信号 //当有新的客户端连接到服务器时,就发送一个newConnection信号 connect(server,&QTcpServer::newConnection,this,&Widget::on_newClient_connect); //QList<QHostAddress> QNetworkInterface::allAddresses() //这个静态方法是用于获取主机上所有的IP地址 QList<QHostAddress> addresses = QNetworkInterface::allAddresses(); //然后一个一个刷上comboBoxAddr这里面 for(QHostAddress address : addresses){ //然后一个个放到ui的comboBoxAddr这里,只放IPV4的 //protocol() 是 "告诉我这个IP地址是IPv4还是IPv6" if(address.protocol() == QAbstractSocket::IPv4Protocol) ui->comboBoxAddr->addItem(address.toString()); } //给on_ComboBox_clicked()创建信号与槽,刷新拉下列表 connect(ui->comboBoxChildren,SIGNAL(on_ComboBox_clicked()),this,SLOT(mComboBox_refresh())); } void Widget::mComboBox_refresh(){ //先清理一下comboBoxChildren ui->comboBoxChildren->clear(); //获取所有的客户端连接 //获取server对象下所有的 QTcpSocket 客户端连接。 QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>(); //把所有端口号添加到comboBoxChildren for(QTcpSocket* tmp : tcpSocketClients){ //断开的客户端不应该加入进去,但是断开的客户端以0端口号还是会存在在server //如果不清理会影响重新连接的客户端 if(tmp != nullptr) ui->comboBoxChildren->addItem(QString::number(tmp->peerPort())); } //在添加all ui->comboBoxChildren->addItem("all");//选择全部 } void Widget::on_readyRead_Handler(){ //sender()获得是那个socket通道发来的信号的。(可能连接了多个客户端) //找到那个通道连接的那个对象QTcpSocket QTcpSocket* tmpSocket = qobject_cast<QTcpSocket *>(sender()); //获取数据 // QString revDate = tmpSocket->readAll(); // revDate = revDate.toUtf8(); QString revDate = tmpSocket->readAll(); QString data = revDate.toUtf8(); //把数据放到textEditRev上面 ui->textEditRev->insertPlainText("客户端" + QString::number(tmpSocket->peerPort()) + ":" + data + "\n"); //让滚动条一直在下面,并且显示光标 ui->textEditRev->moveCursor(QTextCursor::End); ui->textEditRev->ensureCursorVisible(); } void Widget::msocketState(QAbstractSocket::SocketState socketState){ int tmpIndex; qDebug() << "client out In state: " << socketState; //找出是那个客户端点击了断开连接 QTcpSocket* tmpSocket = qobject_cast<QTcpSocket *>(sender()); switch(socketState){ case QAbstractSocket::UnconnectedState://这个表示客户端断开连接或者没有连接 //QComboBox::findText() 是在 QComboBox 中查找指定文本并返回其索引的函数。 tmpIndex = ui->comboBoxChildren->findText(QString :: number(tmpSocket->peerPort())); //根据索引去掉comboBoxChildren里面的端口 ui->comboBoxChildren->removeItem(tmpIndex); //然后清理该端口的资源 //deleteLater() 是 "请晚一点再删除我",告诉Qt:"等当前事件处理完了,在下一轮事件循环中安全地删除这个对象" tmpSocket->deleteLater(); if(ui->comboBoxChildren->count() == 0){ //判断还有没有客户端在线 //如果没有的话,此时是不能发消息的 ui->btnSend->setEnabled(false); } ui->textEditRev->insertPlainText("客户端" + QString :: number(tmpSocket->peerPort()) + ":" + "断开连接" + "\n"); break; } } void Widget::on_newClient_connect(){ qDebug() << "newClient In"; //检查服务器的 待处理连接队列中是否有客户端在等待 //多个客户端连接的时候 if(server->hasPendingConnections()){ //获得QTcpSocket以与客户端通信 QTcpSocket* connection = server->nextPendingConnection(); //打印客户端的地址和端口号 qDebug() << "Client Addr: " << connection->peerAddress().toString() << "Port: " << connection->peerPort(); //将连接显示到textEditRev上面 ui->textEditRev->insertPlainText("客户端地址:" + connection->peerAddress().toString() + "\n客户端端口号:" + QString :: number(connection->peerPort()) + "\n"); //创建接收到客户端数据的信号与槽 //当连接成功的时候,就进行数据交互,本质还是IO操作 //readyRead() 信号是 "有新的数据可以读了" 的通知 connect(connection,SIGNAL(readyRead()),this,SLOT(on_readyRead_Handler())); //创建客户端断开的信号与槽 //方式一,void QAbstractSocket::stateChanged(QAbstractSocket::SocketState socketState) //当 QAbstractSocket 的状态发生变化时,会发出这个信号 connect(connection,SIGNAL(stateChanged(QAbstractSocket::SocketState)), this,SLOT(msocketState(QAbstractSocket::SocketState))); //处理连接的时候,把加进来的连接的端口放到comboBoxChildren //但是仅仅只在ui上面 //setCurrentText() 是 "请在下拉框/组合框中显示这个文本" //把连接进来的客户端端口号放进comboBoxChildren ui->comboBoxChildren->addItem(QString::number(connection->peerPort())); ui->comboBoxChildren->setCurrentText(QString::number(connection->peerPort())); //有连接就恢复发送按键 //isChecked() 是用于查询按钮类控件是否被选中的函数,返回 true 表示选中/勾选,false 表示未选中。 if(!ui->btnSend->isChecked()){ ui->btnSend->setEnabled(true); } } } Widget::~Widget() { delete ui; } void Widget::on_btnListen_clicked() { //开始监听 //bool QTcpServer::listen //(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) //QHostAddress要检查的IP地址,port就是端口 //重新构造,将字符串IP地址转化为QHostAddress这个类型 //QHostAddress addr(); //QHostAddress::Any 是自动检查并同时监听 IPv4 和 IPv6 接口。 //listen() 返回 true 表示监听成功,false 表示监听失败。 int port = ui->lineEditPort->text().toInt();//端口 if(!server->listen(QHostAddress(ui->comboBoxAddr->currentText()),port)){//监听不成功的时候 qDebug() << "listenError"; QMessageBox msgBox; msgBox.setWindowTitle("监听失败"); msgBox.setText("端口被占用!"); msgBox.exec(); return; } //监听成功 ui->btnListen->setEnabled(false); ui->btnStopListen->setEnabled(true); ui->btnLineout->setEnabled(true); } void Widget::on_btnStopListen_clicked() { //关闭所有的QTcpSocket // QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname"); //在当前对象的所有子对象中查找指定名称和类型的对象 //获取server对象下所有的 QTcpSocket 客户端连接。 QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>();//获得了所有的客户端 //关闭所有客户端与服务端的连接 for(QTcpSocket* tmp: tcpSocketClients){ tmp->close(); } //关闭服务端 server->close(); //重置按钮 ui->btnListen->setEnabled(true); ui->btnStopListen->setEnabled(false); ui->btnLineout->setEnabled(false); } void Widget::on_btnLineout_clicked() { //程序直接退出 on_btnStopListen_clicked(); //删除服务端 delete server; //退出程序 this->close(); } void Widget::on_btnSend_clicked() { // QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname"); //在当前对象的所有子对象中查找指定名称和类型的对象 //获取server对象下所有的 QTcpSocket 客户端连接。 //找出所有与客户端的连接 QList<QTcpSocket *> tcpSocketClients = server->findChildren<QTcpSocket *>(); //判断是否有连接 if(tcpSocketClients.isEmpty()){ QMessageBox msgBox; msgBox.setWindowTitle("发送失败"); msgBox.setText("当前无连接!"); msgBox.exec(); return; } //判断是私发还是广播 if(ui->comboBoxChildren->currentText() != "all"){//选择是那个端口号发送 QString currentName = ui->comboBoxChildren->currentText(); //查找是否有这个端口号 for(QTcpSocket* tmp : tcpSocketClients){ if(QString::number(tmp->peerPort()) == currentName){ tmp->write(ui->textEditSend->toPlainText().toStdString().c_str()); } } }else if(ui->comboBoxChildren->currentText() == "all"){ //广播 for(QTcpSocket* tmp : tcpSocketClients){ tmp->write(ui->textEditSend->toPlainText().toStdString().c_str()); } } }• widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QAbstractSocket> #include <QTcpServer> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); public slots: void on_newClient_connect(); void on_readyRead_Handler(); void msocketState(QAbstractSocket::SocketState); void mComboBox_refresh(); private slots: void on_btnListen_clicked(); void on_btnStopListen_clicked(); void on_btnLineout_clicked(); void on_btnSend_clicked(); private: Ui::Widget *ui; QTcpServer *server; }; #endif // WIDGET_H• mycombobox.cpp
#include "mycombobox.h" #include <QMouseEvent> //QT中没有点击ComboBox就刷新的信号 //所有需要重写鼠标点击事件,然后发送一个信号 mycombobox::mycombobox(QWidget *parent) : QComboBox(parent) { } void mycombobox::mousePressEvent(QMouseEvent *e) { //button() - 返回触发当前事件的特定按钮 if(e->button() == Qt::LeftButton){ emit on_ComboBox_clicked(); } //继续传递下去,调用基类处理,确保正常弹出下拉列表 // 缺少 QComboBox::mousePressEvent(e); 可能导致 ComboBox无法正常弹出选项列表 QComboBox::mousePressEvent(e); }• mycombobox.h
#ifndef MYCOMBOBOX_H #define MYCOMBOBOX_H #include <QComboBox> #include <QWidget> class mycombobox : public QComboBox { Q_OBJECT public: mycombobox(QWidget* parent); protected: void mousePressEvent(QMouseEvent *e) override; signals: void on_ComboBox_clicked(); }; #endif // MYCOMBOBOX_H5. 创建QTtcp客户端
5.1 ui
5.2 代码
• widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QTcpSocket> #include <QTimer> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); //UI的初始化 this->setLayout(ui->verticalLayout); //断开连接和发送在还有连接到服务端是不能操作的 ui->btndisCon->setEnabled(false); ui->btnSend->setEnabled(false); //创建新的TcpSocket去连接服务端 client = new QTcpSocket(this); //创建读取的信号与槽 connect(client,SIGNAL(readyRead()),this,SLOT(mRead_Data_From_Server())); } void Widget::mRead_Data_From_Server(){ //读取服务端发送过来的数据 setInsertColor(Qt::black,client->readAll()); //解决滚动条问题 ui->textEditRev->moveCursor(QTextCursor::End); ui->textEditRev->ensureCursorVisible(); } Widget::~Widget() { delete ui; } void Widget::on_btnConnect_clicked() { //这个函数用来启动一个到远程服务器的连接,连接结果通过信号通知。 //virtual void //connectToHost(const QString &hostName, quint16 port, // QIODevice::OpenMode openMode = ReadWrite, QAbstractSocket::NetworkLayerProtocol protocol = AnyIPProtocol) //去连接服务端,根据IP和端口号 client->connectToHost(ui->lineEditIPAddr->text(),ui->lineEditPort->text().toInt()); //用于获取套接字的当前状态 //这里有一个bug,就是错误的服务端IP地址都能连接成功,这是由于client->state() == QAbstractSocket::ConnectingState //这个条件造成的,因为正在连接也不知道能不能连接成功,只要是正在连接就过了这个条件 // if(client->state() == QAbstractSocket::ConnectingState || // client->state() == QAbstractSocket::ConnectedState){ // ui->textEditRev->append("连接成功!"); // ui->lineEditPort->setEnabled(false); // ui->lineEditIPAddr->setEnabled(false); // ui->btnConnect->setEnabled(false); // ui->btndisCon->setEnabled(true); // ui->btnSend->setEnabled(true); // } //解决方法,超时判负 timer = new QTimer(this);//创建定时器 timer->setSingleShot(true);//设置为单次定时器 timer->setInterval(5000);//间隔为5s //如果超时的操作 connect(timer,SIGNAL(timeout()),this,SLOT(onTimeout())); //所以解决方法还是用信号与槽,使用这个信号(判断客户端是否连接成功) //QAbstractSocket::connected() 是 Qt 网络编程中表示连接成功建立的信号。 connect(client,SIGNAL(connected()),this,SLOT(onConnect())); //而且还需要做连接不成功的信号与槽, //QAbstractSocket::error() 是 Qt 网络编`程中表示发生错误的信号。 connect(client,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(onError(QAbstractSocket::SocketError))); //不希望被按下一次连接之后,被多次按下,从而造成影响 this->setEnabled(false); //开启定时器 timer->start(); } void Widget::onError(QAbstractSocket::SocketError socketError){ qDebug() << "连接错误:" << socketError;//错误码打印出来 //errorString() 是 "告诉我刚才发生了什么错误" 的错误信息查询函数,返回人类可读的错误描述。 ui->textEditRev->append("连接出问题了:" + client->errorString()); this->setEnabled(true); on_btndisCon_clicked(); } void Widget::onConnect(){//连接成功就做这个事 //关闭定时器 timer->stop(); ui->textEditRev->append("连接成功!"); this->setEnabled(true); ui->lineEditPort->setEnabled(false); ui->lineEditIPAddr->setEnabled(false); ui->btnConnect->setEnabled(false); ui->btndisCon->setEnabled(true); ui->btnSend->setEnabled(true); } void Widget::onTimeout(){ ui->textEditRev->append("连接超时!"); //还要清理之前不正确的IP,不然会对正确的IP造成影响的 client->abort(); this->setEnabled(true); } void Widget::on_btndisCon_clicked() { //disconnectFromHost() 是 "我要优雅地断开连接,把该发的数据都发完再走" 的礼貌断开函数。 client->disconnectFromHost(); client->close(); ui->textEditRev->append(QString::number(client->peerPort()) + "断开连接!"); ui->lineEditPort->setEnabled(true); ui->lineEditIPAddr->setEnabled(true); ui->btnConnect->setEnabled(true); ui->btndisCon->setEnabled(false); ui->btnSend->setEnabled(false); } void Widget::setInsertColor(Qt::GlobalColor color,QString str){ // //ui->textEditRev->setForegroundRole(),这个也可以设置颜色, // //但是会统一设置的,没法做到颜色上区分,所以要使用光标级别的 // QTextCursor cursor = ui->textEditRev->textCursor(); // //void QTextCursor::setCharFormat(const QTextCharFormat &format) // //用于设置文本字符格式。 // QTextCharFormat format;//QTextCharFormat 类是 Qt 中用于设置文本字符格式的类 // //用于设置前景画刷的函数。 // format.setForeground(QBrush(QColor(Qt::red))); // cursor.setCharFormat(format); // cursor.insertText(ui->textEditSend->toPlainText()); //获得当前光标 QTextCursor cursor = ui->textEditRev->textCursor(); //QTextCursor::setCharFormat(const QTextCharFormat &format)是QTextCursor类的一个方法,用于应用字符格式到文本。 QTextCharFormat format; //设置前景色 format.setForeground(QBrush(QColor(color))); //应用字符格式到文本。 cursor.setCharFormat(format); //insertText()是QTextCursor类的一个方法,用于在光标位置插入文本。 cursor.insertText(str); } void Widget::on_btnSend_clicked() { //获取要发送的数据 QByteArray sendData = ui->textEditSend->toPlainText().toUtf8(); //发送出去 client->write(sendData); //区别客户端发送的消息和服务端发送过来的消息(用颜色区分) setInsertColor(Qt::red,sendData);//客户端发送,显示红色 }• widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QTcpSocket> #include <QTimer> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); private slots: void on_btnConnect_clicked(); void onTimeout(); void onConnect(); void onError(QAbstractSocket::SocketError socketError); void on_btndisCon_clicked(); void on_btnSend_clicked(); void mRead_Data_From_Server(); private: Ui::Widget *ui; QTcpSocket* client; QTimer *timer; void setInsertColor(Qt::GlobalColor color,QString str);//用来设置颜色 }; #endif // WIDGET_H