(1)在 .pro
文件中加入
QT += network
(2)
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
#include
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void getHostInfo();
private slots:
void slot_Detail();
private:
QLabel *HostLabel;
QLineEdit *HostNameLineEdit;
QLabel *IpLabel;
QLineEdit *AddressLineEdit;
QPushButton *DetailBtn;
QGridLayout *mainLay;
};
#endif // WIDGET_H
(3)
#include "widget.h"
#include
#include
#include
#include
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
this->resize(400, 300);
HostLabel = new QLabel("主机名: ");
HostNameLineEdit = new QLineEdit;
IpLabel = new QLabel("IP 地址: ");
AddressLineEdit = new QLineEdit;
DetailBtn = new QPushButton("详细");
mainLay = new QGridLayout(this);
mainLay->addWidget(HostLabel, 0, 0, 1, 1);
mainLay->addWidget(IpLabel, 1, 0, 1, 1);
mainLay->addWidget(HostNameLineEdit, 0, 1, 1, 2);
mainLay->addWidget(AddressLineEdit, 1, 1, 1, 2);
mainLay->addWidget(DetailBtn, 2, 0, 1, 3);
getHostInfo();
connect(DetailBtn, &QPushButton::clicked, this, &Widget::slot_Detail);
}
Widget::~Widget()
{
}
void Widget::getHostInfo()
{
// 获取本机主机名,QHostInfo 提供了一系列有关网络信息的静态函数
// 即可以根据主机名获取分配的 IP 地址,也可以根据 IP 地址获取相应的主机名。
QString localHostName = QHostInfo::localHostName();
HostNameLineEdit->setText(localHostName);
// 根据主机名获取相关主机信息,包括 IP 地址等
// fromName() 函数通过主机名查找 IP 地址信息
QHostInfo hostInfo = QHostInfo::fromName(localHostName);
// 获取主机 Ip 地址列表
QList<QHostAddress> listAddress = hostInfo.addresses();
if(!listAddress.isEmpty()) {
AddressLineEdit->setText(listAddress.at(2).toString());
}
}
void Widget::slot_Detail()
{
QString detail = "";
// 获取主机IP地址和网络接口列表
QList<QNetworkInterface> list = QNetworkInterface::allInterfaces();
for(int i = 0; i < list.count(); ++i) {
QNetworkInterface interface = list.at(i);
// 获取网络接口的名称
detail = detail + "设备: " + interface.name() + "\n";
// 获取网络接口的硬件地址
detail = detail + "硬件地址: " + interface.hardwareAddress() + "\n";
// 每个网络借口包括0个或多个IP地址,每个IP地址有选择性地与一个子网掩码或一个广播地址相关联
// QNetworkAddressEntry类,存储了被网络接口支持的一个IP地址,同时还包括与之相关的子网掩码和广播地址
QList<QNetworkAddressEntry> entryList = interface.addressEntries();
for(int j = 1; j < entryList.count(); ++j) {
QNetworkAddressEntry entry = entryList.at(j);
detail = detail + "\t" + "IP 地址: " + entry.ip().toString() + "\n";
detail = detail + "\t" + "子网掩码: " + entry.netmask().toString() + "\n";
detail = detail + "\t" + "广播地址: " + entry.broadcast().toString() + "\n";
}
}
QMessageBox::information(this, "Detail", detail);
}
(1)
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void CloseBtnClicked();
void dataReceived();
private:
QTextEdit *ReceiveTextEdit;
QPushButton *CloseBtn;
QVBoxLayout *mainLay;
int port;
QUdpSocket *UdpSocket;
};
#endif // WIDGET_H
(2)
#include "widget.h"
#include
#include
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
this->setWindowTitle("UDP Client");
this->resize(300, 300);
ReceiveTextEdit = new QTextEdit(this);
CloseBtn = new QPushButton("Close", this);
mainLay = new QVBoxLayout(this);
mainLay->addWidget(ReceiveTextEdit);
mainLay->addWidget(CloseBtn);
connect(CloseBtn, &QPushButton::clicked, this, &Widget::CloseBtnClicked);
port = 5555;
UdpSocket = new QUdpSocket(this);
// 当有数据到达I/O设备时(在这里是UdpSocket),发出 readyRead() 信号
connect(UdpSocket, &QUdpSocket::readyRead, this, &Widget::dataReceived);
// 被动接收数据的一方必须绑定固定端口,UDP通信中并无严格的服务端与客户端的区别
bool res = UdpSocket->bind(port); // 绑定到指定端口上
if(!res) {
QMessageBox::information(this, "error", "udp socket create error");
return;
}
}
Widget::~Widget()
{
}
void Widget::CloseBtnClicked()
{
close();
}
void Widget::dataReceived()
{// 将数据读出并显示
// 判断 UdpSocket 中是否有数据报可读
// hasPendingDatagrams() 在至少有一个数据报可读时返回 true
while(UdpSocket->hasPendingDatagrams()) {
QByteArray datagram;
// pendingDatagramSize() Returns the size of the first pending UDP datagram.
// If there is no datagram available, this function returns -1.
datagram.resize(UdpSocket->pendingDatagramSize());
// qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
// Receives a datagram and stores it in data
// datagram.data() : char * QByteArray::data()
UdpSocket->readDatagram(datagram.data(), datagram.size());
QString msg = datagram.data();
ReceiveTextEdit->insertPlainText(msg);
}
}
(1)
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
#include
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void StartBtnClicked();
void Timeout();
private:
QLabel *TimerLabel;
QLineEdit *TextLineEdit;
QPushButton *StartBtn;
QVBoxLayout *mainLay;
int port;
bool isStarted;
QUdpSocket *UdpSocket;
QTimer *Timer;
};
#endif // WIDGET_H
(2)
#include "widget.h"
#include
#include
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
setWindowTitle("UDP Server");
this->resize(300, 100);
TimerLabel = new QLabel("计时器", this);
TextLineEdit = new QLineEdit(this); // 要传输的文本
StartBtn = new QPushButton("开始", this);
mainLay = new QVBoxLayout(this);
mainLay->addWidget(TimerLabel);
mainLay->addWidget(TextLineEdit);
mainLay->addWidget(StartBtn);
isStarted = false;
Timer = new QTimer(this);
// 按钮控制定时器开关
connect(StartBtn, &QPushButton::clicked, this, &Widget::StartBtnClicked);
port = 5555;
UdpSocket = new QUdpSocket(this);
// 定时发送广播信息
connect(Timer, &QTimer::timeout, this, &Widget::Timeout);
}
Widget::~Widget()
{
}
void Widget::StartBtnClicked()
{
if(!isStarted) {
// 上一个状态没有被启动,那么点击之后应该启动
StartBtn->setText("停止");
Timer->start(1000);
isStarted = true;
}
else {
// 上一个状态没有被启动,那么点击之后应该停止
StartBtn->setText("开始");
isStarted = false;
Timer->stop();
}
}
void Widget::Timeout()// 定时向端口发送广播信息
{
QString msg = TextLineEdit->text(); // 读取要传输的文本
if(msg.isEmpty()) return;
// qint64 writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port)
// Sends the datagram
// QByteArray QString::toLatin1() const
// QHostAddress::Broadcast ,指定向广播地址发送,The IPv4 broadcast address. Equivalent to QHostAddress("255.255.255.255").
int len = UdpSocket->writeDatagram(msg.toLatin1().data(), msg.length(), QHostAddress::Broadcast, port);
if(len != msg.length()){
// 返回 -1,没有传输成功
QMessageBox::information(this, "error", "传输失败");
}
}
QTcpServer 类用于监听
客户端连接以及和客户端建立连接
构造函数:
QTcpServer::QTcpServer(QObject *parent = Q_NULLPTR);
设置监听:
// port:如果指定为0表示随机绑定一个可用端口。
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
// 判断当前对象是否在监听, 是返回true,没有监听返回false
bool QTcpServer::isListening() const;
// 如果当前对象正在监听,则返回监听的服务器地址信息, 否则返回 QHostAddress::Null
QHostAddress QTcpServer::serverAddress() const;
// 如果服务器正在侦听连接,则返回服务器的端口; 否则返回0
quint16 QTcpServer::serverPort() const
处理来自客户端的请求
// 当一个来自客户端建立连接的新请求可以处理时,自动调用该函数
[virtual protected] void QTcpServer::incomingConnection(qintptr socketDescriptor)
// 需要把新建的通信套接字的描述符设为参数 socketDescriptor
// 当接受新连接导致错误时,将发射如下信号。socketError参数描述了发生的错误相关的信息。
[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError);
// 每次有新连接可用时都会发出 newConnection() 信号。
[signal] void QTcpServer::newConnection();
QTcpSocket是一个套接字通信类,不管是客户端还是服务器端都需要使用。在Qt中发送和接收数据也属于IO操作(网络IO)
构造函数:
QTcpSocket::QTcpSocket(QObject *parent = Q_NULLPTR);
连接服务器:
[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);
[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);
I / O 操作:
// 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中
qint64 QIODevice::read(char *data, qint64 maxSize);
// 指定可接收的最大字节数 maxSize,返回接收的字符串
QByteArray QIODevice::read(qint64 maxSize);
// 将当前可用操作数据全部读出,通过返回值返回读出的字符串
QByteArray QIODevice::readAll();
// 发送指针 data 指向的内存中的 maxSize 个字节的数据
qint64 QIODevice::write(const char *data, qint64 maxSize);
// 发送指针 data 指向的内存中的数据,字符串以 \0 作为结束标记
qint64 QIODevice::write(const char *data);
// 发送参数指定的字符串
qint64 QIODevice::write(const QByteArray &byteArray);
// 在使用QTcpSocket进行套接字通信的过程中,如果该类对象发射出readyRead()信号,说明对端发送的数据达到了
// 之后就可以调用 read 函数接收数据了。
[signal] void QIODevice::readyRead();
// 调用connectToHost()函数并成功建立连接之后发出connected()信号。
[signal] void QAbstractSocket::connected();
// 在套接字断开连接时发出disconnected()信号。
[signal] void QAbstractSocket::disconnected();
要保证客户端能连接服务器端,在同一 wifi 下聊天可以自行百度修改防火墙配置,或者租个服务器运行客户端(也要设置防火墙允许Tcp连接)
新建一个名为 TcpClient
的类,这里我继承自 QWidget
(1)tcpclient.h
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
class TcpClient : public QWidget
{
Q_OBJECT
public:
TcpClient(QWidget *parent = nullptr);
~TcpClient();
private:
QListWidget *contentListWidget;
QLineEdit *sendLineEdit;
QPushButton *sendBtn;
QLabel *userNameLabel;
QLineEdit *userNameLineEdit;
QLabel *serverIPLabel;
QLineEdit *serverIPLineEdit;
QLabel *portLabel;
QLineEdit *portLineEdit;
QPushButton *enterBtn;
QGridLayout *mainLay;
bool status; // true 表示已经进入聊天室,false表示已经离开聊天室
int port;
QHostAddress *serverIP;
QString userName;
QTcpSocket *tcpSocket;
public slots:
void slot_Enter();
void slot_Connected();
void slot_Disconnected();
void slot_DataReceived();
void slot_Send();
};
#endif // WIDGET_H
(2)tcpclient.cpp 里实现
#include "tcpclient.h"
#include
#include
TcpClient::TcpClient(QWidget *parent)
: QWidget(parent)
{
setWindowTitle("TCP Client");
contentListWidget = new QListWidget;
sendLineEdit = new QLineEdit;
sendBtn = new QPushButton("发送");
userNameLabel = new QLabel("用户名: ");
userNameLineEdit = new QLineEdit;
serverIPLabel = new QLabel("服务器地址: ");
serverIPLineEdit = new QLineEdit;
portLabel = new QLabel("端口: ");
portLineEdit = new QLineEdit;
enterBtn = new QPushButton("进入聊天室");
mainLay = new QGridLayout(this);
mainLay->addWidget(contentListWidget, 0, 0, 1, 2);
mainLay->addWidget(sendLineEdit, 1, 0, 1, 1);
mainLay->addWidget(sendBtn, 1, 1, 1, 1);
mainLay->addWidget(userNameLabel, 2, 0, 1, 1);
mainLay->addWidget(userNameLineEdit, 2, 1, 1, 1);
mainLay->addWidget(serverIPLabel, 3, 0, 1, 1);
mainLay->addWidget(serverIPLineEdit, 3, 1, 1, 1);
mainLay->addWidget(portLabel, 4, 0, 1, 1);
mainLay->addWidget(portLineEdit, 4, 1, 1, 1);
mainLay->addWidget(enterBtn, 5, 0, 1, 2);
status = false;
port = 8010;
portLineEdit->setText(QString::number(port));
serverIP = new QHostAddress();
serverIPLineEdit->setText("127.0.0.1");// 服务器的ip地址
connect(enterBtn, &QPushButton::clicked, this, &TcpClient::slot_Enter);
connect(sendBtn, &QPushButton::clicked, this, &TcpClient::slot_Send);
// 设置按钮不能处理键盘和鼠标事件
sendBtn->setEnabled(false);
}
TcpClient::~TcpClient()
{
}
void TcpClient::slot_Enter()
{ // 实现进入和离开聊天室的功能
if(!status) {
QString ip = serverIPLineEdit->text();
// 判断给定的IP地址能否被正确解析
// setAddress参数为QString时,调用的是有bool返回值的重载函数
if(!serverIP->setAddress(ip)) {
QMessageBox::information(this, "error", "server ip address error");
return;
}
if(userNameLineEdit->text().isEmpty()) {
QMessageBox::information(this, "error", "User name can't be empty");
return;
}
userName = userNameLineEdit->text();
tcpSocket = new QTcpSocket(this);
connect(tcpSocket, &QTcpSocket::connected, this, &TcpClient::slot_Connected);
connect(tcpSocket, &QTcpSocket::disconnected, this, &TcpClient::slot_Disconnected);
connect(tcpSocket, &QTcpSocket::readyRead, this, &TcpClient::slot_DataReceived);
// 与TCP服务器连接,连接成功后发出 connected 信号
tcpSocket->connectToHost(*serverIP, port);
status = true;
}
else {
QString msg = userName + " : Leave Chat Room";
int len = tcpSocket->write(msg.toUtf8(), msg.length());
if(len != msg.length()) return;
// 断开连接,之后发出 disconnected 信号
tcpSocket->disconnectFromHost();
status = false;
}
}
void TcpClient::slot_Connected()
{
sendBtn->setEnabled(true);
enterBtn->setText("离开");
QString msg = userName + " Enter Chat Room";
int len = tcpSocket->write(msg.toUtf8(), msg.length());
if(len != msg.length()) {
QMessageBox::information(this, "error", "enter chat room failed");
return;
}
}
void TcpClient::slot_Disconnected()
{
sendBtn->setEnabled(false);
enterBtn->setText("进入聊天室");
}
void TcpClient::slot_DataReceived()
{
while(tcpSocket->bytesAvailable() > 0) {
// QByteArray datagram;
// datagram.resize(tcpSocket->bytesAvailable());
// tcpSocket->read(datagram.data(), datagram.size());
// QString msg = datagram.data();
// msg = msg.left(datagram.size());
QString msg = QString::fromUtf8(tcpSocket->readAll());
contentListWidget->addItem(msg);
}
}
void TcpClient::slot_Send()
{
if(sendLineEdit->text().isEmpty()) return;
QString msg = userName + ":" + sendLineEdit->text();
// qDebug() << "Client send : " << msg << " size = " << msg.size() << " length : " << msg.length();
tcpSocket->write(msg.toUtf8(), msg.toUtf8().size());
sendLineEdit->clear();
}
TcpServer
类,这里我也继承自了 QWidget
做界面设计
直接用封装的 QTcpServer
即可
(1)tcpserver.h
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
#include
#include "server.h"
class TcpServer : public QWidget
{
Q_OBJECT
public:
TcpServer(QWidget *parent = nullptr);
~TcpServer();
private:
QListWidget *ContentListWidget;
QLabel *PortLabel;
QLineEdit *PortLineEdit;
QPushButton *CreateBtn;
QGridLayout *mainLay;
int port;
Server *server;
public slots:
void slot_CreateServer();
void slot_updateServer(QString, int);
};
#endif // WIDGET_H
(2)tcpserver.cpp
#include "tcpserver.h"
TcpServer::TcpServer(QWidget *parent)
: QWidget(parent)
{
setWindowTitle("TCP Server");
this->resize(300, 300);
ContentListWidget = new QListWidget;
PortLabel = new QLabel("端口: ");
PortLineEdit = new QLineEdit;
CreateBtn = new QPushButton("创建聊天室");
mainLay = new QGridLayout(this);
mainLay->addWidget(ContentListWidget, 0, 0, 2, 2);
mainLay->addWidget(PortLabel, 2, 0, 1, 1);
mainLay->addWidget(PortLineEdit, 2, 1, 1, 1);
mainLay->addWidget(CreateBtn, 3, 0, 1, 2);
port = 8010;
PortLineEdit->setText(QString::number(port));
connect(CreateBtn, &QPushButton::clicked, this, &TcpServer::slot_CreateServer);
}
TcpServer::~TcpServer()
{
}
void TcpServer::slot_CreateServer()
{
server = new Server(this, port);
connect(server, &Server::signal_updateServer, this, &TcpServer::slot_updateServer);
// QPushButtn 继承自 QWidget
// In general an enabled widget handles keyboard and mouse events; a disabled widget does not.
CreateBtn->setEnabled(false);
}
void TcpServer::slot_updateServer(QString msg, int len)
{
ContentListWidget->addItem(msg.left(len));
}
TcpClientSocket
类,继承自 QTcpSocket
服务器端用来和客户端通信的套接字
,这里要进行自定义的一个原因是:服务器端通信套接字,接收完消息要进行一个转发,类似于回声服务器,但是这里不仅要发给发送消息的客户端,还要发送给其他客户端。我们需要在读完数据后立马触发信号,这样就能即时更新消息。
(1)tcpclientsocket.h
#ifndef TCPCLIENTSOCKET_H
#define TCPCLIENTSOCKET_H
#include
#include
class TcpClientSocket : public QTcpSocket
{
Q_OBJECT
public:
TcpClientSocket(QObject *parent = 0);
// 服务端用于通信的套接字
signals:
void signal_updateClients(QString, int); // 通知服务器向聊天室内的所有成员广播信息
void signal_Disconnected(int);
protected slots:
void slot_DataReceived();
void slot_Disconnected();
};
#endif // TCPCLIENTSOCKET_H
(2)tcpclientsocket.cpp
#include "tcpclientsocket.h"
// 服务器端通信的套接字
TcpClientSocket::TcpClientSocket(QObject *)
{
// if(parent) this->setParent(parent);
// once every time new data is available for reading from the device's current read channel.
connect(this, &QTcpSocket::readyRead, this, &TcpClientSocket::slot_DataReceived);
// when the socket has been disconnected.
connect(this, &QTcpSocket::disconnected, this, &TcpClientSocket::slot_Disconnected);
}
void TcpClientSocket::slot_DataReceived()
{// 读出数据
// qint64 QAbstractSocket::bytesAvailable() const
// Returns the number of incoming bytes that are waiting to be read.
while(this->bytesAvailable() > 0) {
QString msg = QString::fromUtf8(this->readAll());
int len = msg.size();
//qDebug() << "server receive :" << msg;
// 服务器端接收来自客户端的数据结束,向聊天室内的所有成员广播信息
emit signal_updateClients(msg, len);
}
}
void TcpClientSocket::slot_Disconnected()
{
// socketDescriptor() 用于获取底层的套接字描述符, 通常是一个整数
// 向监听QTcpServer发送当前断开连接的套接字的描述符(套接字编号)
emit signal_Disconnected(this->socketDescriptor());
}
Server
类,继承自 QTcpServer
QTcpServer 的作用就是监听
来自客户端的请求
(1)server.h
#ifndef SERVER_H
#define SERVER_H
#include
#include
#include
#include "tcpclientsocket.h"
class Server : public QTcpServer
{
Q_OBJECT
public:
Server(QObject *parent = 0, int port = 0);
// 保存与客户端连接的 TcpClientSocket
QList<TcpClientSocket *> tcpClientSocketList;
signals:
void signal_updateServer(QString, int);// 更新ui,聊天室内容
public slots:
void slot_updateClients(QString, int);
void slot_Disconnected(int);
protected: // 重载函数别写错了,32位写int,64位写long long
void incomingConnection(long long socketDescriptor);
};
#endif // SERVER_H
(2)server.cpp
#include "server.h"
#include
Server::Server(QObject *parent, int port) : QTcpServer(parent)
{
// 在指定的port端口,对任意地址进行监听
listen(QHostAddress::Any, port);
}
// 类似于accept,但此函数自动调用,处理来自客户端建立链接的请求
void Server::incomingConnection(long long socketDescriptor)
{
// 新建一个用于和客户端通信的 TcpSocket
TcpClientSocket *tcpClientSocket = new TcpClientSocket(this);
// 如果tcpClientSocket接受完来自客户端的信息,触发更新
connect(tcpClientSocket, &TcpClientSocket::signal_updateClients, this, &Server::slot_updateClients);
// 监听用于通信的套接字是否断开连接
connect(tcpClientSocket, &TcpClientSocket::signal_Disconnected, this, &Server::slot_Disconnected);
//将新创建的通信套接字的描述符指定为参数socketdescriptor
tcpClientSocket->setSocketDescriptor(socketDescriptor);
//将这个套接字加入客户端套接字列表中
tcpClientSocketList.append(tcpClientSocket);
}
void Server::slot_updateClients(QString msg, int len)
{// 服务器端将刚刚接收自某个客户端的信息,发送到所有客户端,实现聊天室消息同步
// 更新服务器对话框的内容
emit signal_updateServer(msg, len);
// 向聊天室内的所有成员广播信息
for(int i = 0; i < tcpClientSocketList.count(); ++i) {
// 取出与第i个客户端建立连接的用于通信的套接字
QTcpSocket *tmp = tcpClientSocketList.at(i);
if(tmp->write(msg.toUtf8(), msg.toUtf8().size()) != msg.toUtf8().size()) {
continue;
}
}
}
void Server::slot_Disconnected(int descriptor) {
// 遍历找到并删除断开的通信套接字
for(int i = 0; i < tcpClientSocketList.count(); ++i) {
QTcpSocket *tmp = tcpClientSocketList.at(i);
if(tmp->socketDescriptor() == descriptor) {
tcpClientSocketList.removeAt(i);
return;
}
}
}