简介
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。WebSocket协议基于TCP协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。
默认情况下:WebSocket 协议使用 80 端口;
WebSocket目前支持两种统一资源标志符ws和wss,类似于HTTP和HTTPS。
如: ws://example.com:80/some/path
如: ws://127.0.0.1:45678 即ip+端口
产生背景:
因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开), 因此websocket应运而生。
优点:
1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
TCP和Websocket协议的区别
- 层次结构: WebSocket 是应用层协议,而 TCP 是传输层协议。
- 协议特点: TCP 是一种面向连接的协议,使用三次握手建立连接,提供可靠的数据传输。而 WebSocket 是一种无状态的协议,使用 HTTP 协议建立连接,可以进行双向通信,WebSocket 的数据传输比 TCP 更加轻量级。
- 数据格式: TCP 传输的数据需要自定义数据格式,而 WebSocket 可以支持多种数据格式,如 JSON、XML、二进制等。WebSocket 数据格式化可以更好的支持 Web 应用开发。
- 连接方式: TCP 连接的是物理地址和端口号,而 WebSocket 连接的是 URL 地址和端口号。
- 发送方:TCP套接字非阻塞操作出现部分发送的情况。Websocket从发送的数据来看,不再是一系列字节,而是按照一个完整的"消息体"发送出去的,这个"消息体"无法进一步再分割,要么全部发送成功,要么压根就不发送,不存在像TCP套接字非阻塞操作那样出现部分发送的情况。换言之,Web Socket里对套接字的操作是非阻塞操作。
- 接收方:在TCP套接字的场景下,接收方从TCP套接字读取的字节数,并不一定等于发送方调用send所发送的字节数。WebSocket的接收方从套接字读取数据,根本不是像TCP 套接字那样直接用recv/read来读取, 而是采取事件驱动机制。即应用程序注册一个事件处理函数,当web socket的发送方发送的数据在接收方应用从内核缓冲区拷贝到应用程序层已经处于可用状态时 ,应用程序注册的事件处理函数以回调(callback)的方式被调用。
QWebSocket
要使用 Qt 的 WebSocket 模块,先在 pro 文件中加上 websockets.
QT += websockets
QWebSocket类
常用函数
QHostAddress localAddress() const;
quint16 localPort() const;
QUrl requestUrl() const;
QNetworkRequest request() const;
当有需要接收的数据时,会发出该信号
void textMessageReceived(const QString &message); //字符串的方式接收数据信息
void binaryMessageReceived(const QByteArray &message);//二进制的方式接收数据信息
当需要发送数据时,调用下面的发送函数
qint64 sendTextMessage(const QString &message); //字符串的方式发送数据信息
qint64 sendBinaryMessage(const QByteArray &data);//二进制的方式发送数据信息
客户端断开连接,收到断开信号:
void disconnected();
服务端QWebSocketServer
QWebSocketServer以 QTcpServer 为模型,并且行为相同。所以,如果你知道如何使用 QTcpServer,你就知道如何使用 QWebSocketServer.
常用的函数、信号、槽函数:
- bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
监听连接。 如果端口为 0,则自动选择一个端口。 如果地址是 QHostAddress::Any,则服务器将侦听所有网络接口。
- void newConnection(); 信号,接收连接,发出该信号
- virtual QWebSocket *nextPendingConnection(); //获取连接的客户端
- void close(); //关闭监听套接字
流程
- 创建服务器:new QWebSocketServer
- 监听:listen
m_pWebSocketServer->listen(QHostAddress::LocalHost, mPort);//端口号
- 有新的连接,触发这个信号:QWebSocketServer::newConnection
- 在处理newConnection信号的槽函数中,获得新的QWebSocket客户端:QWebSocketServer::nextPendingConnection
- 接收到信息时候,触发信号:QWebSocket::binaryMessageReceived或者QWebSocket::textMessageReceived
- 发送数据,调用函数QWebSocket::sendTextMessage或QWebSocket::sendBinaryMessage
- 客户端断开连接,触发信号:QWebSocket::disconnected,在处理disconnected信号的槽函数中,QWebSocket客户端调用deleteLater,进行释放资源。
- 主动关闭服务端,调用QWebSocketServer::close,所有的客户端连接qDeleteAll(m_clients.begin(), m_clients.end());
客户端QWebSocket
客户端直接使用QWebSocket的函数进行操作即可
流程
- 创建客户端:new QWebSocket
- 通过QWebSocket的open 函数连接服务端的 Url
- 连接服务端成功后,会触发QWebSocket::connected信号。从而获知连接成功
- 发送数据,调用函数QWebSocket::sendTextMessage或QWebSocket::sendBinaryMessage
- 接收到信息时候,触发信号:QWebSocket::binaryMessageReceived或者QWebSocket::textMessageReceived
- 客户端关闭时,要主动调QWebSocket的close函数,让服务端知道该客户端已断开连接。
- 断开连接成功,会触发信号QWebSocket::disconnected,在该信号的槽函数处理断开连接的相应工作。
完整代码
开发环境:QT5.15.2 MSVC 2019 64Bit
服务端
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QWebSocketServer>
#include <QWebSocket>
#include <QMap>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void sendTextMessageSignal(QString msg);
private slots:
void on_pushButton_Listen_clicked();
void OnNewConnectionSlot();
void OnTextReceivedSlot(QString msg);
void OndisconnectedSlot();
void on_pushButton_send_clicked();
private:
Ui::Widget *ui;
QWebSocketServer *server;
QList<QWebSocket*> m_clientList;
QMap<QWebSocket*,QString> m_clientStatus;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
server = new QWebSocketServer("testserver",QWebSocketServer::NonSecureMode,this);
connect(server,&QWebSocketServer::newConnection,this,&Widget::OnNewConnectionSlot);
ui->lineEdit_address->setText("Any");
ui->lineEdit_port->setText("45678");
}
Widget::~Widget()
{
qDeleteAll(m_clientList);
server->close();
delete ui;
}
void Widget::on_pushButton_Listen_clicked()
{
QHostAddress address;
if(ui->lineEdit_address->text()=="Any")
{
address=QHostAddress::Any;
}
else
{
address=QHostAddress(ui->lineEdit_address->text());
}
int nPort = ui->lineEdit_port->text().toInt();
bool bListen = server->listen(address,nPort);
if(bListen)
ui->pushButton_Listen->setEnabled(false);
}
void Widget::OnNewConnectionSlot()
{
QWebSocket *client = server->nextPendingConnection();
m_clientList.append(client);
qDebug() << "Client:" << client->peerAddress().toString() << client->peerName() <<client->peerPort() ;
QString strstatus = "ip:" + client->peerAddress().toString() + " 端口:"+ QString::number(client->peerPort()) + "连接成功\n";
m_clientStatus.insert(client,strstatus);
ui->textEdit_clientlist->insertPlainText(strstatus);
ui->textEdit_clientlist->
connect(client,&QWebSocket::textMessageReceived,this,&Widget::OnTextReceivedSlot);
connect(this,&Widget::sendTextMessageSignal,client,&QWebSocket::sendTextMessage);//给所有客户端发送数据
connect(client,&QWebSocket::disconnected,this,&Widget::OndisconnectedSlot);
}
void Widget::OnTextReceivedSlot(QString msg)
{
ui->textEdit_recv->setText(msg);
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
pClient->sendTextMessage("回答" + msg);
}
void Widget::OndisconnectedSlot()
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
m_clientStatus.remove(pClient);
ui->textEdit_clientlist->clear();
QList<QString> textlist = m_clientStatus.values();
for(auto text : textlist)
{
ui->textEdit_clientlist->insertPlainText(text);
}
m_clientList.removeAll(pClient);
pClient->deleteLater();
}
void Widget::on_pushButton_send_clicked()
{
QString msg = ui->textEdit_send->toPlainText();
emit sendTextMessageSignal(msg);
}
客户端
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QWebSocket>
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_pushButton_connect_clicked();
void onConnectedSlot();
void ondisconnectedSlot();
void onTextReceivedSlot(QString message);
void on_pushButton_send_clicked();
void on_pushButton_disconnect_clicked();
private:
Ui::Widget *ui;
QWebSocket *m_ClientSocket;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
m_ClientSocket = new QWebSocket();
m_ClientSocket->setParent(this);
ui->label_connet->setText("尚未连接服务端");
ui->pushButton_connect->setEnabled(true);
ui->pushButton_disconnect->setEnabled(false);
ui->lineEdit_url->setText("ws://127.0.0.1:45678");
}
Widget::~Widget()
{
m_ClientSocket->close();
delete ui;
}
void Widget::on_pushButton_connect_clicked()
{
QUrl url(ui->lineEdit_url->text());
m_ClientSocket->open(url);
connect(m_ClientSocket, &QWebSocket::connected, this, &Widget::onConnectedSlot);
connect(m_ClientSocket, &QWebSocket::disconnected, this, &Widget::ondisconnectedSlot);
}
void Widget::onConnectedSlot()
{
ui->pushButton_connect->setEnabled(false);
ui->pushButton_disconnect->setEnabled(true);
QString strstatus = "连接服务端" + ui->lineEdit_url->text() + "成功";
ui->label_connet->setText(strstatus);
connect(m_ClientSocket, &QWebSocket::textMessageReceived, this, &Widget::onTextReceivedSlot);
}
void Widget::ondisconnectedSlot()
{
QString strstatus = "断开服务端" + ui->lineEdit_url->text() + "连接";
ui->label_connet->setText(strstatus);
ui->pushButton_connect->setEnabled(true);
ui->pushButton_disconnect->setEnabled(false);
}
void Widget::onTextReceivedSlot(QString message)
{
ui->textEdit_recv->setText(message);
}
void Widget::on_pushButton_send_clicked()
{
QString message = ui->textEdit_send->toPlainText();
m_ClientSocket->sendTextMessage(message);
}
void Widget::on_pushButton_disconnect_clicked()
{
m_ClientSocket->close();
}
延伸:
延伸
- 客户端主动断开连接和客户端被动关闭程序
客户端调用QWebSocket的close;服务端触发信号QWebSocket::disconnected,在处理disconnected信号的槽函数中,单个QWebSocket客户端调用deleteLater,进行释放资源;最后客户端触发信号QWebSocket::disconnected
- 服务端主动断开连接和服务端被动关闭程序
服务端所有的通信QWebSocket对象调用QWebSocket的close,服务端触发信号QWebSocket::disconnected,在处理disconnected信号的槽函数中,单个QWebSocket客户端调用deleteLater,进行释放资源服务端被动关闭程序;客户端触发信号QWebSocket::disconnected。最后服务端QWebSocketServer对象调用QWebSocketServer的close。
- 以二进制发送和接收数据
客户端:
//发送
void Widget::on_pushButton_send_bit_clicked()
{
QString message = ui->textEdit_send->toPlainText();
QByteArray arry = message.toUtf8();
m_ClientSocket->sendBinaryMessage(arry);
}
//接收
private slots:
void OnbinaryReceivedSlot(QByteArray msg);
connect(m_ClientSocket, &QWebSocket::binaryMessageReceived, this, &Widget::OnbinaryReceivedSlot);
void Widget::OnbinaryReceivedSlot(QByteArray msg)
{
QString strMsg = QString::fromUtf8(msg);
ui->textEdit_recv->setText(strMsg);
}
服务端:
接收
private slots:
void OnbinaryReceivedSlot(QByteArray msg);
connect(client,&QWebSocket::binaryMessageReceived,this,&Widget::OnbinaryReceivedSlot);
void Widget::OnbinaryReceivedSlot(QByteArray msg)
{
QString strMsg = QString::fromUtf8(msg);
ui->textEdit_recv->setText(strMsg);
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
strMsg = "回答" + strMsg;
QByteArray arry = strMsg.toUtf8();
pClient->sendBinaryMessage(arry);
}
发送
void Widget::on_pushButton_send_bit_clicked()
{
QString msg = ui->textEdit_send->toPlainText();
QByteArray arry = msg.toUtf8();
emit sendbinaryMessageSignal(arry);
}
- 服务端多线程模式
使用QWebsocketserver监听 nextPendingConnection得到的websocket无法跨线程使用。
在QTcpServer中我们可以重写inComming函数得到socket描述符传入多线程中再使用QTcpSocket的setSocketDescript函数解决问题 然而QWebSocketServer并没有此函数。
thread->start();
worker->moveToThread(thread);
worker为QWebSocket的类,移到其他线程后调用 m_socket->sendTextMessage(mess);
QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread
即可以在子线程接收数据,无法在子线程发送数据。导致只能在主线程发送。
代码
#ifndef QTHREADWORKER_H
#define QTHREADWORKER_H
#include <QObject>
#include <QWebSocket>
class QThreadWorker : public QObject
{
Q_OBJECT
public:
explicit QThreadWorker(QWebSocket *socket,QObject *parent = nullptr);
signals:
void sendTextMsgSignals(QString msg);
void finished();
public slots:
void OnTextReceivedSlot(QString msg);
void OnbinaryReceivedSlot(QByteArray msg);
void OndisconnectedSlot();
public:
QWebSocket* m_socket;
};
#endif // QTHREADWORKER_H
#include "threadworker.h"
#include <QThread>
QThreadWorker::QThreadWorker(QWebSocket *socket,QObject *parent)
: m_socket(socket),QObject{parent}
{
connect(m_socket,&QWebSocket::textMessageReceived,this,&QThreadWorker::OnTextReceivedSlot);
connect(m_socket,&QWebSocket::binaryMessageReceived,this,&QThreadWorker::OnbinaryReceivedSlot);
connect(m_socket,&QWebSocket::disconnected,this,&QThreadWorker::OndisconnectedSlot);
}
void QThreadWorker::OnTextReceivedSlot(QString msg)
{
qDebug() << QThread::currentThreadId() << "接收:" << m_socket->peerAddress().toString() << m_socket->peerName() <<m_socket->peerPort() << "::" << msg;
emit sendTextMsgSignals("回答:" + msg);
}
void QThreadWorker::OnbinaryReceivedSlot(QByteArray msg)
{
}
void QThreadWorker::OndisconnectedSlot()
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
pClient->deleteLater();
//同时需要销毁该对象,退出线程
emit finished();
}
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QWebSocketServer>
#include <QWebSocket>
#include <QMap>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void sendTextMessageSignal(QString msg);
void sendbinaryMessageSignal(QByteArray msg);
private slots:
void on_pushButton_Listen_clicked();
void OnNewConnectionSlot();
void OnTextReceivedSlot(QString msg);
void OnbinaryReceivedSlot(QByteArray msg);
void OndisconnectedSlot();
void on_pushButton_send_clicked();
void on_pushButton_disconnect_clicked();
//二进制发送
void on_pushButton_send_bit_clicked();
void on_sendTextMsgSlot(QString msg);
private:
Ui::Widget *ui;
QWebSocketServer *server;
QList<QWebSocket*> m_clientList;
QMap<QWebSocket*,QString> m_clientStatus;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include "threadworker.h"
#include <QThread>
#include "qthreadsend.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
server = new QWebSocketServer("testserver",QWebSocketServer::NonSecureMode,this);
connect(server,&QWebSocketServer::newConnection,this,&Widget::OnNewConnectionSlot);
ui->lineEdit_address->setText("Any");
ui->lineEdit_port->setText("45678");
ui->pushButton_disconnect->setEnabled(false);
}
Widget::~Widget()
{
for(auto client : m_clientList)
{
client->close();
}
server->close();
delete ui;
}
void Widget::on_pushButton_Listen_clicked()
{
QHostAddress address;
if(ui->lineEdit_address->text()=="Any")
{
address=QHostAddress::Any;
}
else
{
address=QHostAddress(ui->lineEdit_address->text());
}
int nPort = ui->lineEdit_port->text().toInt();
bool bListen = server->listen(address,nPort);
if(bListen)
{
ui->pushButton_disconnect->setEnabled(true);
ui->pushButton_Listen->setEnabled(false);
}
}
void Widget::OnNewConnectionSlot()
{
QWebSocket *client = server->nextPendingConnection();
m_clientList.append(client);
qDebug() << QThread::currentThreadId() << "Client:" << client->peerAddress().toString() << client->peerName() <<client->peerPort() ;
QString strstatus = "ip:" + client->peerAddress().toString() + " 端口:"+ QString::number(client->peerPort()) + "连接成功\n";
m_clientStatus.insert(client,strstatus);
ui->textEdit_clientlist->insertPlainText(strstatus);
QThreadWorker *worker = new QThreadWorker(client);
// 将worker移动到新线程中执行
QThread* thread = new QThread();
worker->moveToThread(thread);
connect(worker,&QThreadWorker::sendTextMsgSignals,this,&Widget::on_sendTextMsgSlot);
connect(worker,&QThreadWorker::finished, worker,&QThreadWorker::deleteLater);
connect(worker,&QThreadWorker::finished, thread,&QThread::quit);
connect(thread,&QThread::finished, thread,&QThread::deleteLater);
thread->start();
}
void Widget::OnTextReceivedSlot(QString msg)
{
ui->textEdit_recv->setText(msg);
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
pClient->sendTextMessage("回答" + msg);
}
void Widget::OnbinaryReceivedSlot(QByteArray msg)
{
QString strMsg = QString::fromUtf8(msg);
ui->textEdit_recv->setText(strMsg);
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
strMsg = "回答" + strMsg;
QByteArray arry = strMsg.toUtf8();
pClient->sendBinaryMessage(arry);
}
void Widget::OndisconnectedSlot()
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
m_clientStatus.remove(pClient);
ui->textEdit_clientlist->clear();
QList<QString> textlist = m_clientStatus.values();
for(auto text : textlist)
{
ui->textEdit_clientlist->insertPlainText(text);
}
m_clientList.removeAll(pClient);
pClient->deleteLater();
}
void Widget::on_pushButton_send_clicked()
{
QString msg = ui->textEdit_send->toPlainText();
emit sendTextMessageSignal(msg);
}
void Widget::on_pushButton_disconnect_clicked()
{
for(auto client : m_clientList)
{
client->close();
}
server->close();
ui->pushButton_Listen->setEnabled(true);
ui->pushButton_disconnect->setEnabled(false);
}
void Widget::on_pushButton_send_bit_clicked()
{
QString msg = ui->textEdit_send->toPlainText();
QByteArray arry = msg.toUtf8();
emit sendbinaryMessageSignal(arry);
}
void Widget::on_sendTextMsgSlot(QString msg)
{
QThreadWorker *pClient = qobject_cast<QThreadWorker *>(sender());
qint64 ncout = pClient->m_socket->sendTextMessage(msg);
pClient->m_socket->flush();
qDebug() << QThread::currentThreadId() << "发送:" << msg;
}