Qt学习(十四)——网络通信之TCPLinux下的TCP

Linux下的TCP通信过程

在这里插入图片描述

在这里插入图片描述
【转自:Linux C/C++ TCP Socket通信实例

QT下的TCP通信过程

在这里插入图片描述

listen()【服务端】

Qt中,服务端的监听套接字不再是socket,而是QTcpServer和QTcpSocket。此外,它将绑定bind()监听listen()合为一个函数,即监听listen(),在调用的同时指定主机地址和端口号。

connectToHost()【客户端】

而客户端的通信套接字也不再是socket,而是QTcpSocket,连接也不是connect(),而是connectToHost(),意为主动向主机(服务端)发起连接。

newConnection()【服务端】

如果客户端发起连接并且成功,服务端将会收到一个信号。在Qt中,服务端不再是由accept()来确定连接的建立,而是会触发一个newConnection()信号。既然是信号,就要有与之对应的槽函数。槽函数中主要的工作是取出建立好的套接字QTcpSocket(它是真正的通信套接字)。

write()【服务端/客户端】

客户端用write()发送数据,如果数据传送成功,对方的通信套接字会触发readyRead()信号(继承自QIODevice类的信号),说明可以开始读数据了,我们需要在对应的槽函数中做接收处理。反过来,服务端给客户端发信息也是一样的。

connected() & disconnected()【服务端/客户端】

只要成功与对方建立连接,无论是服务端还是客户端,通信套接字都会自动触发connected()信号,而如果对方主动断开连接,通信套接字会自动触发disconnected()信号,我们可以利用这两个信号作出一些提示,比如在客户端与服务器端成功连接后,在客户端窗口输出“已成功连接服务器!”或者“已与服务器断开连接!”。

P.S:

  • 用到网络编程时,Qt的.pro文件中要加上 QT += network。如果不加的话,关于TCP的头文件都是不会有提示的。
  • 服务端有两个套接字,写这部分代码的时候要记得加上<QTcpServer><QTcpSocket>

listen()是服务端通过QTcpServer对象调用的,它的参数是主机地址和端口号。主机地址默认是QHostAddress::Any —— 绑定当前网卡所有的IP地址。下面我们在服务端创建一个监听套接字:

p_tcpServer=new QTcpServer(this);           
p_tcpSocket=new QTcpSocket(this);           
//服务端监听                                     
p_tcpServer->listen(QHostAddress::Any,8888);
复制代码

只要客户端和服务端建立连接,服务端就会产生一个newConnection()信号,在相应的槽函数中调用QTcpServer的nextPendingConnection()取出建立好连接的套接字(取出来就是一个指针,不需要动态分配空间)。

如果我们想在服务端窗口显示当前是谁给我们发消息过来(获取对方的IP地址和端口号),也可以实现,这些信息就包含在套接字中。只要调用通信套接字QTcpSocket对象的peerAddress()方法就可以获取对方的IP地址,但是到这一步获取到的IP地址是我们看不懂的,还需要再调用toString()来转化成常见的xxx.xxx.xxx.xxx格式的IP地址。端口是用Qt中封装好的内置类型qint16来表示的,也是需要用调用QTcpSocket对象的peerPort()方法来获取。

//服务端连接                                                                         
connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::fetchSocket);
//fetchSocket的实现
void ServerWidget::fetchSocket(){
    //从新建的连接中取出套接字
    p_tcpSocket=p_tcpServer->nextPendingConnection();
    //获取IP地址
    QString ip=p_tcpSocket->peerAddress().toString();
    //获取端口号
    qint16 port=p_tcpSocket->peerPort();
    //连接信息输出到文本框中
    p_recvBox->setText(QString("[%1:%2] 连接成功!").arg(ip).arg(port));
    //从套接字中读取数据
    connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ServerWidget::readData);
}

复制代码

实现效果:
在这里插入图片描述
TCP通信的机制与打电话很相似,只要获取到对方的通信套接字QTcpSocket,后面往通信套接字里写入的数据都会被发送到指定IP地址下指定的端口,就好像双方之间建立了一个通道。下面让我们来看看客户端是怎么与服务端通过通信套接字建立连接的:

//建立连接                                                                           
connect(p_connectButton,&QPushButton::clicked,this,&ClientWidget::getConnection);
//getConnection的实现
void ClientWidget::getConnection(){
    //获取服务器IP地址和端口号,这是我们通过文本框输入的
    QString m_ip=p_ipEdit->text();
    qint16 m_port=p_portEdit->text().toInt();
    //主动与服务器建立连接
    p_tcpSocket->connectToHost(QHostAddress(m_ip),m_port);
}
复制代码

只要客户端connectToHost()中的IP地址与端口号和服务器端listen()中监听的IP地址与端口号是一致的,那么它们之前的通道就建立起来了。建立完连接后,当然就是进行消息的收发了。

往通信套接字中写数据调用的是write()方法,它的参数可以是char *,也可以是QByteArray字节数组,但我们从文本编辑区获取的内容一般是QString类型的,所以这里还要进行一个转化。

//发送消息                                                                   
connect(p_sendButton,&QPushButton::clicked,this,&ServerWidget::sendData);
//sendData的实现
void ServerWidget::sendData(){
    //先进行判断,是否已经获取套接字
    if(p_tcpSocket==NULL){
        return;
    }
    //获取文本编辑框的内容
    QString str=p_sendBox->toPlainText();
    //往套接字中写入数据
    p_tcpSocket->write(str.toUtf8().data());
}

复制代码

当服务端或客户端往通信套接字中写入数据后,另一端会自动触发readyRead()信号,我们可以在与之对应的槽函数里面,把套接字中的数据取出并在窗口中展示:

//接收从服务端发来的数据                                                            
connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::readData);
//readData的实现
void ClientWidget::readData(){
    QByteArray array=p_tcpSocket->readAll();
    QString str="服务端:";
    str.append(QString(array));
    p_recvBox->append(str);
}

复制代码

实现效果:
在这里插入图片描述
(这个效果要配合客户端一起使用才能出来。)

当客户端或服务端想主动断开连接时,需要调用QTcpSocket的disconnectFromHost()方法,然后调用QTcpSocket的close()方法。断开连接后,再往QTcpSocket套接字中写入数据就不会再被另一方获取了,除非重新建立连接:

//关闭连接                                                                           
connect(p_closeButton,&QPushButton::clicked,this,&ServerWidget::closeConnection);
//closeConnection的实现
void ServerWidget::closeConnection(){
    //先进行判断,是否已经获取套接字
    if(p_tcpSocket==NULL){
        return;
    }
    //主动和客户端断开连接
    p_tcpSocket->disconnectFromHost();
    p_tcpSocket->close();
    //断开后,将套接字重新赋值为NULL
    p_tcpSocket=NULL;
}
复制代码

P.S:这里如果不对套接字是否为空进行判断,在套接字为空的情况下去断开连接,会使程序异常结束。而在断开连接之后,需要将套接字指针置为NULL,否则你不知道它会指向哪里。

服务端的实现

//ServerWidget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QTcpServer>
#include <QTcpSocket>
#include <QTextEdit>
#include <QWidget>

class ServerWidget : public QWidget
{
    Q_OBJECT

public:
    ServerWidget(QWidget *parent = 0);
    ~ServerWidget();
private:
    QTcpServer *p_tcpServer;    //监听套接字
    QTcpSocket *p_tcpSocket;    //通信套接字
    QTextEdit *p_recvBox;   //接收消息文本框,只读
    QTextEdit *p_sendBox;   //发送消息文本框
protected:
    void ServerWidget::fetchSocket();   //获取通信套接字
    void ServerWidget::readData();  //从套接字中读取数据
    void ServerWidget::sendData();  //往套接字中写入数据
    void ServerWidget::closeConnection();   //断开连接

};

#endif // WIDGET_H
复制代码
//ServerWidget.cpp
#include "ServerWidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QPushButton>
#include <QDebug>
ServerWidget::ServerWidget(QWidget *parent)
    : QWidget(parent)
{
    //绘制窗口
    QVBoxLayout *p_vLayout=new QVBoxLayout(this);
    p_recvBox=new QTextEdit(this);
    p_sendBox=new QTextEdit(this);
    p_vLayout->addWidget(p_recvBox);
    p_recvBox->setAttribute(Qt::WA_Disabled);
    p_vLayout->addWidget(p_sendBox);
    QHBoxLayout *p_hLayout=new QHBoxLayout(this);
    QPushButton *p_sendButton=new QPushButton("send",this);
    QPushButton *p_closeButton=new QPushButton("close",this);
    p_hLayout->addStretch(1);
    p_hLayout->addWidget(p_sendButton);
    p_hLayout->addStretch(2);
    p_hLayout->addWidget(p_closeButton);
    p_hLayout->addStretch(1);
    p_vLayout->addLayout(p_hLayout);
    this->resize(500,500);
    //实现服务端功能
    p_tcpServer=new QTcpServer(this);
    p_tcpSocket=new QTcpSocket(this);
    //服务端监听
    p_tcpServer->listen(QHostAddress::Any,8888);
    //服务端连接
    connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::fetchSocket);
    //发送消息
    connect(p_sendButton,&QPushButton::clicked,this,&ServerWidget::sendData);
    //关闭连接
    connect(p_closeButton,&QPushButton::clicked,this,&ServerWidget::closeConnection);
}

ServerWidget::~ServerWidget()
{

}
void ServerWidget::fetchSocket(){
    //从新建的连接中取出套接字
    p_tcpSocket=p_tcpServer->nextPendingConnection();
    //获取IP地址
    QString ip=p_tcpSocket->peerAddress().toString();
    //获取端口号
    qint16 port=p_tcpSocket->peerPort();
    //连接信息输出到文本框中
    p_recvBox->setText(QString("[%1:%2] 连接成功!").arg(ip).arg(port));
    //从套接字中读取数据
    connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ServerWidget::readData);
}
void ServerWidget::sendData(){
    //获取文本编辑框的内容
    QString str=p_sendBox->toPlainText();
    //往套接字中写入数据
    p_tcpSocket->write(str.toUtf8().data());
}
void ServerWidget::readData(){
    //从套接字中读取数据
    QByteArray array=p_tcpSocket->readAll();
    //追加到文本框中,注意append与setText不同
    p_recvBox->append(QString(array));

}
void ServerWidget::closeConnection(){
    //主动和客户端断开连接
    p_tcpSocket->disconnectFromHost();
    p_tcpSocket->close();
}
复制代码

注意:读取套接字数据的connect语句要写在建立连接的connect语句中,因为只有建立连接后,QTcpSocket对象才是我们的目标对象。否则,在此之前它只是一个野指针。

为了防止在成功建立连接以及正确获取到通信套接字之前,就对套接字进行操作(用户不会知道你代码的逻辑是什么,因此需要预防各种不当操作可能带来的后果),我们可以加一层判断,在构造函数中先将QTcpServer和QTcpSocket对象设为空指针,当QTcpServer和QTcpSocket对象为NULL时,说明还没有成功建立连接并正确获取通信套接字。此时,点击发送消息或点击关闭不会执行任何操作。修改后的ServerWidget.cpp文件:

#include "ServerWidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTextEdit>
#include <QPushButton>
#include <QDebug>
ServerWidget::ServerWidget(QWidget *parent)
    : QWidget(parent)
{
    //先置为空指针
    p_tcpSocket=NULL;
    p_tcpServer=NULL;
    //绘制窗口
    QVBoxLayout *p_vLayout=new QVBoxLayout(this);
    p_recvBox=new QTextEdit(this);
    p_sendBox=new QTextEdit(this);
    p_vLayout->addWidget(p_recvBox);
    p_recvBox->setAttribute(Qt::WA_Disabled);
    p_vLayout->addWidget(p_sendBox);
    QHBoxLayout *p_hLayout=new QHBoxLayout(this);
    QPushButton *p_sendButton=new QPushButton("send",this);
    QPushButton *p_closeButton=new QPushButton("close",this);
    p_hLayout->addStretch(1);
    p_hLayout->addWidget(p_sendButton);
    p_hLayout->addStretch(2);
    p_hLayout->addWidget(p_closeButton);
    p_hLayout->addStretch(1);
    p_vLayout->addLayout(p_hLayout);
    this->resize(500,500);
    //实现服务端功能
    p_tcpServer=new QTcpServer(this);
    p_tcpSocket=new QTcpSocket(this);
    //服务端监听
    p_tcpServer->listen(QHostAddress::Any,8888);
    //服务端连接
    connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::fetchSocket);
    //发送消息
    connect(p_sendButton,&QPushButton::clicked,this,&ServerWidget::sendData);
    //关闭连接
    connect(p_closeButton,&QPushButton::clicked,this,&ServerWidget::closeConnection);
}

ServerWidget::~ServerWidget()
{

}
void ServerWidget::fetchSocket(){
    //从新建的连接中取出套接字
    p_tcpSocket=p_tcpServer->nextPendingConnection();
    //获取IP地址
    QString ip=p_tcpSocket->peerAddress().toString();
    //获取端口号
    qint16 port=p_tcpSocket->peerPort();
    //连接信息输出到文本框中
    p_recvBox->setText(QString("[%1:%2] 连接成功!").arg(ip).arg(port));
    //从套接字中读取数据
    connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ServerWidget::readData);
}
void ServerWidget::sendData(){
    //先进行判断,是否已经获取套接字
    if(p_tcpSocket==NULL){
        return;
    }
    //获取文本编辑框的内容
    QString str=p_sendBox->toPlainText();
    //往套接字中写入数据
    p_tcpSocket->write(str.toUtf8().data());
}
void ServerWidget::readData(){
    //先进行判断,是否已经获取套接字
    if(p_tcpSocket==NULL){
        return;
    }
    //从套接字中读取数据
    QByteArray array=p_tcpSocket->readAll();
    //追加到文本框中,注意append与setText不同
    p_recvBox->append(QString(array));

}
void ServerWidget::closeConnection(){
    //先进行判断,是否已经获取套接字
    if(p_tcpSocket==NULL){
        return;
    }
    //主动和客户端断开连接
    p_tcpSocket->disconnectFromHost();
    p_tcpSocket->close();
    //断开后,将套接字重新赋值为NULL
    p_tcpSocket=NULL;
}
复制代码

客户端的实现

服务端基本完成了,接下来是客户端。客户端的实现要简单得多,首先,需要在项目下新建一个C++类文件:
在这里插入图片描述
实现客户端只需要使用到QTcpSocket通信套接字。当客户端点击“connect”按钮时,会调用QTcpSocket套接字对象的connectToHost()方法,主动向指定的IP地址下指定的端口发送建立连接的请求。

//ClientWidget.h
#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H

#include <QWidget>
#include <QTcpSocket>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
#pragma execution_character_set("utf-8")
class ClientWidget : public QWidget
{
    Q_OBJECT
public:
    explicit ClientWidget(QWidget *parent = nullptr);
private:
    QTcpSocket *p_tcpSocket;
    QLineEdit *p_portEdit;
    QLineEdit *p_ipEdit;
    QPushButton *p_connectButton;
    QPushButton *p_sendButton;
    QPushButton *p_closeButton;
    QTextEdit *p_recvBox;
    QTextEdit *p_sendBox;
protected:
    void ClientWidget::readData();  //从套接字读取数据
    void ClientWidget::sendData();  //向套接字发送数据
    void ClientWidget::getConnection(); //主动与服务端建立连接
    void ClientWidget::closeConnection();   //主动与服务端断开连接
signals:

public slots:
};

#endif // CLIENTWIDGET_H
复制代码
#include "ClientWidget.h"
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QHostAddress>
ClientWidget::ClientWidget(QWidget *parent) : QWidget(parent)
{

     p_tcpSocket=new QTcpSocket(this); //??
    //绘制界面
    this->resize(500,500);
    this->move(1300,225);
    this->setWindowTitle("客户端");
    QGridLayout *p_gridLayout=new QGridLayout(this);
    QLabel *p_port=new QLabel("端口号:",this);
    QLabel *p_ip=new QLabel("IP地址:",this);
    //为什么动态分配就异常结束??
    p_connectButton=new QPushButton();
    p_sendButton=new QPushButton();
    p_closeButton=new QPushButton();
    p_recvBox=new QTextEdit();
    p_sendBox=new QTextEdit();
    p_portEdit=new QLineEdit();
    p_ipEdit=new QLineEdit();
    p_connectButton->setText("connect");
    p_sendButton->setText("send");
    p_closeButton->setText("close");
    p_recvBox->setAttribute(Qt::WA_Disabled);
    p_gridLayout->addWidget(p_port,0,0,1,1);
    p_gridLayout->addWidget(p_ip,1,0,1,1);
    p_gridLayout->addWidget(p_portEdit,0,1,1,2);
    p_gridLayout->addWidget(p_ipEdit,1,1,1,2);
    p_gridLayout->addWidget(p_connectButton,0,3,2,1,Qt::AlignCenter);
    p_gridLayout->addWidget(p_recvBox,2,0,1,4);
    p_gridLayout->addWidget(p_sendBox,6,0,1,4);
    p_gridLayout->addWidget(p_sendButton,10,0,1,1);
    p_gridLayout->addWidget(p_closeButton,10,3,1,1);
    //建立连接
    connect(p_connectButton,&QPushButton::clicked,this,&ClientWidget::getConnection);
    //接收从服务端发来的数据
    connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::readData);
    //发送数据到服务端
    connect(p_sendButton,&QPushButton::clicked,this,&ClientWidget::sendData);
    //与服务端断开连接
    connect(p_closeButton,&QPushButton::clicked,this,&ClientWidget::closeConnection);


}
void ClientWidget::getConnection(){
    //获取服务器IP地址和端口号
    QString m_ip=p_ipEdit->text();
    qint16 m_port=p_portEdit->text().toInt();
    //主动与服务器建立连接
    p_tcpSocket->connectToHost(QHostAddress(m_ip),m_port);
}
void ClientWidget::readData(){

}
void ClientWidget::sendData(){

}
void ClientWidget::closeConnection(){

}
复制代码

实现效果:
在这里插入图片描述
客户端向服务端发送连接,服务端显示连接成功。

那如何让客户端也知道已经连接成功呢?连接成功时,客户端通信套接字会自动触发connected()信号,在与之对应的槽函数中可以作出相应的提示。

#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H

#include <QWidget>
#include <QTcpSocket>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
#pragma execution_character_set("utf-8")
class ClientWidget : public QWidget
{
    Q_OBJECT
public:
    explicit ClientWidget(QWidget *parent = nullptr);
private:
    QTcpSocket *p_tcpSocket;
    QLineEdit *p_portEdit;
    QLineEdit *p_ipEdit;
    QPushButton *p_connectButton;
    QPushButton *p_sendButton;
    QPushButton *p_closeButton;
    QTextEdit *p_recvBox;
    QTextEdit *p_sendBox;
protected:
    void ClientWidget::readData();  //从套接字读取数据
    void ClientWidget::sendData();  //向套接字发送数据
    void ClientWidget::getConnection(); //主动与服务端建立连接
    void ClientWidget::closeConnection();   //主动与服务端断开连接
    void ClientWidget::sureConnection();    //新增sureConnection()声明,确认与服务器连接成功	
signals:

public slots:
};

#endif // CLIENTWIDGET_H
复制代码
#include "ClientWidget.h"
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QHostAddress>
ClientWidget::ClientWidget(QWidget *parent) : QWidget(parent)
{

     p_tcpSocket=new QTcpSocket(this);
    //绘制界面
    this->resize(500,500);
    this->move(1300,225);
    this->setWindowTitle("客户端");
    QGridLayout *p_gridLayout=new QGridLayout(this);
    QLabel *p_port=new QLabel("端口号:",this);
    QLabel *p_ip=new QLabel("IP地址:",this);
    p_connectButton=new QPushButton();
    p_sendButton=new QPushButton();
    p_closeButton=new QPushButton();
    p_recvBox=new QTextEdit();
    p_sendBox=new QTextEdit();
    p_portEdit=new QLineEdit();
    p_ipEdit=new QLineEdit();
    p_connectButton->setText("connect");
    p_sendButton->setText("send");
    p_closeButton->setText("close");
    p_recvBox->setAttribute(Qt::WA_Disabled);
    p_gridLayout->addWidget(p_port,0,0,1,1);
    p_gridLayout->addWidget(p_ip,1,0,1,1);
    p_gridLayout->addWidget(p_portEdit,0,1,1,2);
    p_gridLayout->addWidget(p_ipEdit,1,1,1,2);
    p_gridLayout->addWidget(p_connectButton,0,3,2,1,Qt::AlignCenter);
    p_gridLayout->addWidget(p_recvBox,2,0,1,4);
    p_gridLayout->addWidget(p_sendBox,6,0,1,4);
    p_gridLayout->addWidget(p_sendButton,10,0,1,1);
    p_gridLayout->addWidget(p_closeButton,10,3,1,1);
    //建立连接
    connect(p_connectButton,&QPushButton::clicked,this,&ClientWidget::getConnection);
    //新增代码,客户端确认建立连接
    connect(p_tcpSocket,&QTcpSocket::connected,this,&ClientWidget::sureConnection);
    //接收从服务端发来的数据
    connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::readData);
    //发送数据到服务端
    connect(p_sendButton,&QPushButton::clicked,this,&ClientWidget::sendData);
    //与服务端断开连接
    connect(p_closeButton,&QPushButton::clicked,this,&ClientWidget::closeConnection);


}
void ClientWidget::getConnection(){
    //获取服务器IP地址和端口号
    QString m_ip=p_ipEdit->text();
    qint16 m_port=p_portEdit->text().toInt();
    //主动与服务器建立连接
    p_tcpSocket->connectToHost(QHostAddress(m_ip),m_port);
}
void ClientWidget::sureConnection(){
    p_recvBox->setText("成功与服务器建立连接!");
}
void ClientWidget::readData(){

}
void ClientWidget::sendData(){

}
void ClientWidget::closeConnection(){

}
复制代码

实现效果:
在这里插入图片描述
接下来,要实现的是客户端和服务端之间的数据传输。有了通信套接字之后,发送方调用write()方法往通信套接字中写入数据,接收方会自动触发readyRead()信号,在槽函数中将套接字的数据读取出来并展示在窗口中:

//接收
void ClientWidget::readData(){
    QByteArray array=p_tcpSocket->readAll();
    QString str="服务端:";
    str.append(QString(array));
    p_recvBox->append(str);
}
//发送
void ClientWidget::sendData(){
    QString str=p_sendBox->toPlainText();
    p_tcpSocket->write(str.toUtf8().data());
}
复制代码

实现效果:
在这里插入图片描述
到此为止,服务端和客户端之间消息的发送与接收功能就基本实现了。但是,我们还需要对关闭连接做一些处理:

void ClientWidget::closeConnection(){
    p_tcpSocket->disconnectFromHost();
    p_tcpSocket->close();
}
复制代码

实现效果:
在这里插入图片描述
这样一来,在断开连接后,如果点击发送,编译器会提示这个设备没有打开。

P.S:如有错误,欢迎指正~