WebSocket
第一、二章是一些理论性的知识和设计思路,如果要直接看具体是如何实现的,请点击我。
一、WebSocket简介
websocket是一种基于HTTP协议的网络通信协议,通过使用双向通信的长连接,可以实现服务端和客户端之间双向通信,具有很好的实时性和可靠性。相比于传统的HTTP等协议,更适用于一些需要实时通信的场景。
要搭建一个websocket服务器,一般需要以下步骤:
1.选择语言和框架如python的Flask、Node.js的Socket.IO、C++和QT的Websockets等,并安装相应的依赖和库文件。
2.编写服务端代码,实现 WebSocket 协议和业务逻辑,处理连接、消息发送和接收、错误处理等。
3.部署服务器,将代码部署在云服务器或者本地。
4.编写客户端代码,设置向服务器发送数据,实现想要的功能。
5.测试服务器,使用浏览器或者 WebSocket 客户端向服务器发送连接请求和消息,检查服务器是否能够正确处理和响应请求。
6.调试优化。
总的来说,要搭建一个满足自身需求的服务器,需要具备一定的编程技能和相关经验,需要认真思考设计和实现细节,才能保证服务器的稳定性和性能。
二、设计理念
由于WebSocket是一种中心化的通信方式,通过设置服务器实现用户之间的通信,而且同时兼容各种不同的编程语言,因此可以编写服务器的规则,实现数据一对一发送、一对多发送和广播;而且还可以通过在服务器设置ID,实现同个应用的多开和并行,应用场景非常广泛。下面我将介绍本次搭建的服务器的设计理念。(以下所有的设计都基于QT的WebSockets,javascript也有测试,只要是面向对象的编程语言应该都通用)
一个websocket服务器需要具备以下基本功能:
1.基本功能:开启和关闭;
2.新用户连接处理和用户退出处理。
3.统计用户ID功能;
4.接收、转发、以及自身发送的功能;
5.服务器关闭或断开时处理。
通过搭建websocket服务器,实现星型拓扑网络结构的通信,且每个节点可以并行多个客户端。结构如下图所示。
上图中,不同的组可以通过设置类型分辨;同组内的不同用户可以通过设置id分辨。
要完成一次发送,要告诉服务器的信息有以下:
1.发送者是谁?
2.要通过什么方式发送?一对一?一对多?还是广播?
3.如果不是广播的方式,那么要给谁发送?
4.发送的信息是什么?
5.接收方有没有接收到信息?
初步确定了以上需求,那么就可以愉快的搭建我们的服务器了!
三、具体实现
本次我搭建了一个本地两组客户端的服务器,其中一组客户端是用QT编写的,另一组客户端时Web端,主要实现的功能是,QT端通过发送命令,Web端接收命令执行并返回给QT端简单的日志。其实也是项目需求,大多数处理都在QT向Web发送的数据,Web端可以做简单回复,如果你需要更多的Web端向QT端的数据发送功能,可以通过略微增改服务器代码实现。
另外,为什么我只实现两组用户的通信呢?因为我懒。哦不,其实多组用户是差不多的,只需要略微修改服务器的转发规矩即可,可以根据自己的需求为服务器增添任意的规则,我这边两组用户的规则已经调试好了,可以满足我的需求,你有什么需求也得自己修改和调试。接下来介绍的我的服务器搭建框架如下:
1. 服务端实现
这部分较为复杂,也是这篇博文的精华所在,建议配合完整代码食用。
1.1 服务端UI如下:
非常简单,本地服务器的搭建只需要设置端口就行了,还有开启、关闭、清空当前日志显示等功能。服务器开启对应的槽函数中,需要具备以下关键语句:
server = new QWebSocketServer("My WebSocket Server", QWebSocketServer::NonSecureMode, this);
if (!server->listen(QHostAddress::Any, Port)){
ui->plainTextEdit->appendPlainText(QString(current_time.toString()) + " 连接端口失败");
}
else{
ui->plainTextEdit->appendPlainText(QString(current_time.toString()) + " 连接端口成功!");
}
QObject::connect(server, &QWebSocketServer::newConnection, this, &MainWindow::reply_message);
上述三个步骤分别是在:创建QWebSocketServer服务器对象、连接端口并开始监听、 为新用户连接信号绑定对应的槽函数reply_message()。
新用户连接时,要为用户分配ID,每个ID都是特有的,所以在类的private属性中定义一个int型变量user_id,以及用户数目user_num,另外每次新建用户连接都会创建新的socket对象,需要一个保存这些socket对象的集,可以使用QList创建一个列表m_clients:
private:
int user_num = 0, user_id = 0;
QList<QWebSocket *> m_clients;
1.2 槽函数reply_message()中有以下关键步骤:
QWebSocket* my_socket = new QWebSocket();
my_socket = server->nextPendingConnection();
my_socket->setObjectName("用户_" + QString::number(user_id).rightJustified(3, '0'));
connect(my_socket, SIGNAL(textMessageReceived(QString)), this, SLOT(processTextMessage(QString)));
connect(my_socket, SIGNAL(disconnected()), this, SLOT(slot_disconnected()));
上述代码中两个步骤分别是:连接时创建新的对象my_socket,并且通过修改QT的ObjectName属性为每个对象分配特有的ID、绑定接收到消息和连接断开这两个信号的槽函数。
1.2.1 接收到消息时的处理结构如下:
QTime current_time = QTime::currentTime();
QObject *senderObject = sender();
if (message == "send_position"){...}
else if (message == "cesium_position"){...}
else if (message == "get_target_id"){...}
else{...}
相信这一步你应该会很不解,这个if语句为什么要这样分流。前面说的,因为我这个工程一共有两组用户,一个Web端,一个QT端,因为我要在服务器中为这两组不同的设置不同的类型,但是客户端刚建立连接时还没法发送消息,因此,需要在连接之后发送一个确认字,让服务器知道他是哪种类型的用户,所以在客户端刚连接上服务端后要先发送一次确认字再传递信息。对应的就是"send_position"和"cesium_position"两种情形了。判断好之后在他们socket的ObjectName属性最后添加对应的类型字符,如下
senderObject->setObjectName(senderObject->objectName()+="S"); //QT端
senderObject->setObjectName(senderObject->objectName()+="C"); //Web端
设置"get_target_id"情形是因为发送端在进行非广播的发送时,需要知道可以发送的目标都有谁,因此,设置了这一标志,所以我们在正常发送信息时就要注意,不能发送这几个特殊的标记。else里边就是对发送信息的处理了,在其中,进行信息的解析判断和分流,我发送端发送的数据时json形式的,实现框架如下:
QString id = senderObject->objectName().right(4);
if(senderObject->objectName().right(1) == "S"){
QString msg = message;
QJsonObject doc = QJsonDocument::fromJson(msg.toUtf8()).object();
QString tar_id = doc.value("target_id").toString();
if (tar_id == "0"){} //广播
else{} //发送给对应的目标
}
else if(senderObject->objectName().right(1) == "C"){} //处理类似于"S"情形
另外,可以通过这句代码得到信号触发的对象,也就是对应发信息的socket对象,QT的这个功能非常好,大大方便了我们的编程。
QObject *senderObject = sender();
有用户断开连接时,需要告知服务器,并从创建的连接列表m_clients中清除这个断开的对象:
m_clients.removeOne(clientSocket);
具体的实现细节可以看本文文末的实现代码或者自行下载代码。跳转到代码
1.3 服务器关闭时,也需要执行一些命令,关键步骤有以下:
for (QWebSocket *socket : m_clients){
socket->sendTextMessage("sever_close");
socket->close();
}
server->close();
delete server;
server = nullptr;
user_id = 0;
user_num = 0;
m_clients.erase(m_clients.begin(), m_clients.end());
以上三个步骤分别是:向所有连接的用户发送"sever_close"信息并且断开连接,关闭服务器、对象地址重置、重置统计信息和列表。
2. QT发送端实现
2.1 UI界面如下:
看上去很多按钮,其实也没有很复杂,毕竟是客户端,不比服务端那么自动化。使用时,首先绑定,发送确认字,就可以在发送区发送消息了,发送的数据我设置成了json形式。函数名和参数对应Web端要执行的函数和它的参数,可以按按钮“得到所有目标ID”来获取目前连接的Web端ID,发送目标为0时是广播的方式发送,发送ID的规则我设置成了强制三位,就是说,如果你要给ID为4的用户发送,那么你就得写成004,多用户发送时发送的每个ID中间以一个空格间隔。如果不好理解可以看代码,配合理解起来很方便。这里就不细讲了。直接上代码。
2.2 “绑定”按键的槽函数
void MainWindow::on_pushButton_1_released()
{
QTime current_time = QTime::currentTime();
client = new QWebSocket();
QString url = ui->lineEdit_3->text();
client->open(QUrl(url));
connect(client, &QWebSocket::textMessageReceived, this, &MainWindow::Receive_Server_Message);
ui->plainTextEdit_recv->appendPlainText(QString(current_time.toString()) + " " + "点击 发送确认字 确认身份。");
connect(client, SIGNAL(disconnected()), this, SLOT(slot_disconnected()));
ui->pushButton_2->setDisabled(true);
ui->pushButton_3->setEnabled(true);
ui->pushButton_1->setDisabled(true);
}
2.3 “发送确认字”按键的槽函数
void MainWindow::on_pushButton_3_released()
{
client->sendTextMessage("send_position");
ui->pushButton_2->setEnabled(true);
ui->pushButton_7->setEnabled(true);
ui->pushButton_8->setEnabled(true);
ui->pushButton_3->setDisabled(true);
}
2.4 “发送”按键的槽函数
void MainWindow::on_pushButton_2_released()
{
QJsonObject my_json, params_json;
QString func_name = ui->lineEdit_func->text();
QString args = ui->plainTextEdit_send->toPlainText();
QString target_id = ui->lineEdit_id->text();
my_json.insert("target_id", target_id);
my_json.insert("func_name", func_name);
my_json.insert("func_params", args);
QJsonDocument jsonDocument(my_json);
QString jsonString = jsonDocument.toJson();
client->sendTextMessage(jsonString);
}
2.5 “解除绑定”按键的槽函数
void MainWindow::on_pushButton_7_released()
{
QTime current_time = QTime::currentTime();
ui->pushButton_1->setEnabled(true);
ui->pushButton_2->setDisabled(true);
ui->pushButton_3->setDisabled(true);
ui->pushButton_7->setDisabled(true);
ui->pushButton_8->setDisabled(true);
ui->plainTextEdit_recv->appendPlainText(QString(current_time.toString()) + " " + "解除绑定");
// 清除之前的连接,避免多次收到数据
client->close();
client->deleteLater();
client = nullptr;
}
2.6 “得到所有目标ID”按键的槽函数
void MainWindow::on_pushButton_8_released()
{
client->sendTextMessage("get_target_id");
}
2.7 “服务器断开时的处理”
void MainWindow::slot_disconnected()
{
QTime current_time = QTime::currentTime();
ui->pushButton_1->setEnabled(true);
ui->pushButton_2->setDisabled(true);
ui->pushButton_3->setDisabled(true);
ui->pushButton_7->setDisabled(true);
ui->pushButton_8->setDisabled(true);
ui->plainTextEdit_recv->appendPlainText(QString(current_time.toString()) + " " + "服务器失去连接,请重新建立连接!");
}
具体工程代码在文末,需要可以自取。跳转到代码
3. Web端实现
web端实现很简单,用javascript语言写,处理连接、断开和有消息传来这三种信号即可,之后将收到的消息解析,执行不同的函数并将参数打印在console控制台。实现代码如下:
let socket = new WebSocket('ws://localhost:7770/');
socket.addEventListener('open', function (){
console.log("Cesium连接成功")
socket.send("cesium_position")
})
socket.addEventListener("close", function(){
console.log("服务器失去连接")
})
//操作执行函数
socket.addEventListener('message', function(a){
try {
var receive = JSON.parse(a.data);
console.log("收到发送端的命令: ")
socket.send("Cesium收到了修改的命令!")
FUNCREGISTRY[receive["func_name"]](receive["func_params"]);
} catch(error){
console.log("收到其他数据: ")
console.log(a);
}
})
function func_1(json){
console.log("func_1", json);
}
function func_2(json){
console.log("func_2", json);
}
function func_3(json){
console.log("func_3", json);
}
function func_4(json){
console.log("func_4", json);
}
let FUNCREGISTRY = {
"func_1": func_1,
"func_2": func_2,
"func_3": func_3,
"func_4": func_4,
}
至此,恭喜你,你的websocket通信服务以及搭建完成,可以愉快的玩耍了。
代码地址
github:
https://github.com/FLBa9762/Websocket-server.git
gitee:
https://gitee.com/flba666/Websocket-server.git