基于socket.io的实时在线选座系统

时间:2023-03-09 02:30:14
基于socket.io的实时在线选座系统

基于socket.io的实时在线选座系统(demo)


前言

前段时间公司做一个关于剧院的项目,遇到了这样一种情况。

在高并发多用户同时选座的情况下,假设A用户进入选座页面,正在选择座位,此时还没有提交所选择的座位。

这时B用户进入选座页面,迅速的选择了座位,提交。

而这个时候,A终于选择完毕,提交。 发现座位已经被买了。

当用户越多这样的情况越严重。

具体场景就是如此。

1、简介

本项目是基于jquery.seat-charts在线选座插件。集合socket.io,实现的实时选座系统,可应用于剧院,影院,车票等!

Socket.IO是一个WebSocket库,包括了客户端的js和服务器端的nodejs,它主要是为了实现客户端和服务端的全双工通信。我们传统的http请求(抛开长链接不谈),只实现了一请求一回复的,没有办法做到服务器端向客户端推送数据的情况。而Socket.IO则实现了这一点。

依赖的模块

  • node.js
  • express
  • socket.io
  • jquery.seat-charts

2、安装部署

2.1 部署 express服务器

express是一个小巧的Node.js的Web应用框架,在构建HTTP服务器时经常使用到,所以直接以Socket.IO和express为例子来讲解。

在node.js环境下

npm install express 

express XXX                 *(XXX)是你的项目名字*

cd XXX                      *进入你的项目*

npm install                 *下载依赖*
2.2 添加依赖模块、修改默认的express框架

本项目没有使用express默认的模板引擎jade,采用了ejs模板,对新手来说更友好,学习成本更低。

npm install -D socket.io ejs

虽然简单,但是如果使用在express框架上则需要修改以下几个位置。

> views目录下所有文件

改为ejs后缀。 并把内容改为标准html 5 模板。

app.js 文件

app.set('view engine', 'jade');
修改为 ==> app.set('view engine', 'ejs');

bin > www 文件

因为socket.io需要监听服务,所以我们需要把www文件中的server 抛出
module.exports = server; 添加在www文件最后一行即可

新建 bin > socket.js 文件 (后续添加该处代码)

放置socket.io核心代码。实现模块分离。
bin > www 放置服务配置
bin > socket.js 放置socket.io配置信息

package.json 修改入口配置

"scripts": {
"start": "node ./bin/www"
}
修改为
"scripts": {
"start": "node ./bin/socket.js"
}

3、服务端代码

bin > socket.js

var server = require("./www");
var io = require("socket.io")(server); io.chooseSeat = {}; io.on('connection', function(socket) {
//用户选择的座位
socket.chooseSeat = {};
socket.isSold = false; socket.on('login', function(data) {
io.emit("loginlock",io.chooseSeat);
}); //监听用户选择座位
socket.on('selected', function(data) {
socket.chooseSeat[data.id] = data;
io.chooseSeat[data.id] = data;
io.emit("locking",data);
});
socket.on('cancleselected', function(data) {
delete socket.chooseSeat[data.id];
delete io.chooseSeat[data.id];
io.emit("canclelocking",data)
}); socket.on('sold', function(data) {
// 把售卖的座位信息返给其他用户
socket.isSold = true
io.emit('seatsold', Object.keys(data));
}); //监听用户退出 释放用户选择的未提交座位
socket.on('disconnect', function() {
// 如果没有购买,直接就退出了,才去释放座位 for(var t in socket.chooseSeat){
delete io.chooseSeat[t];
} if (!socket.isSold) {
io.emit('userout', socket.chooseSeat);
}
})
});

4、客户端代码

bin > socket.js

<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>基于socket.io的实时在线选座系统(影院版)</title>
<meta name="keywords" content="jQuery在线选座,jQuery选座系统,WebSocket,socket.io,实时选座系统" />
<meta name="description" content="本项目是基于jquery.seat-charts在线选座插件。集合socket.io,实现的实时选座系统,可应用于剧院,影院,车票等" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" type="text/css" href="css/reset.css" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
</head>
<body>
<div class="container">
<h2 class="title"><a href="#">jQuery在线选座(影院版)</a></h2>
<div class="demo clearfix">
<!---左边座位列表-->
<div id="seat_area">
<div class="front">屏幕</div>
</div>
<!---右边选座信息-->
<div class="booking_area">
<p>电影:<span>天将雄师</span></p>
<p>时间:<span>03月20日 22:15</span></p>
<p>座位:</p>
<ul id="seats_chose"></ul>
<p>票数:<span id="tickects_num">0</span></p>
<p>总价:<b>¥<span id="total_price">0</span></b></p>
<input type="button" class="btn" id="commitSeat" value="确定购买" />
<div id="legend"></div>
</div>
</div>
</div>
<script src="js/jquery-3.2.1.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="js/jquery.seat-charts.js"></script>
<script src="js/index.js"></script>
</body>
</html>

pulic > js > index.js

$(function(){
var price = 100; //电影票价
var initData = {
socket: io.connect('http://192.168.1.96:3000'),
mapData:[ //座位结构图 a 代表座位; 下划线 "_" 代表过道
'cccccccccc',
'cccccccccc',
'__________',
'cccccccc__',
'cccccccccc',
'cccccccccc',
'cccccccccc',
'cccccccccc',
'cccccccccc',
'cc__cc__cc'
],
iconStatus:[ // 座位状态
['c', 'available', '可选座'],
['c', 'selected', '已选中'],
['c', 'locking', '已锁定'],
['c', 'unavailable', '已售出'],
],
selectedSeat:{}
}
var interaction = {
initMap:function(){
var _this = this;
var $cart = $('#seats_chose'), //座位区
$tickects_num = $('#tickects_num'), //票数
$total_price = $('#total_price'); //票价总额
var sc = $('#seat_area').seatCharts({
map:initData.mapData,
naming: { //设置行列等信息
top: false, //不显示顶部横坐标(行)
getLabel: function(character, row, column) { //返回座位信息
return column;
}
},
legend: { //定义图例
node: $('#legend'),
items: initData.iconStatus
},
click: function() {
if (this.status() == 'available') { //若为可选座状态,添加座位
$('<li>' + (this.settings.row + 1) + '排' + this.settings.label + '座</li>')
.attr('id', 'cart-item-' + this.settings.id)
.data('seatId', this.settings.id)
.appendTo($cart);
$tickects_num.text(sc.find('selected').length + 1); //统计选票数量
$total_price.text(_this.getTotalPrice(sc) + price); //计算票价总金额 // 向服务器发送消息,座位被我选中
_this.emit("selected",{
firetype:'selected',
firetime:new Date().toLocaleString(),
character:this.settings.character,
column:this.settings.column,
data:this.settings.data,
id:this.settings.id,
label:this.settings.label,
row:this.settings.row
})
initData.selectedSeat[this.settings.id] = this.settings;
return 'selected';
} else if (this.status() == 'selected') { //若为选中状态
$tickects_num.text(sc.find('selected').length - 1); //更新票数量
$total_price.text(_this.getTotalPrice(sc) - price); //更新票价总金额
$('#cart-item-' + this.settings.id).remove(); //删除已预订座位 // 向服务器发送消息,座位被我取消
_this.emit("cancleselected",{
firetype:'cancleselected',
firetime:new Date().toLocaleString(),
character:this.settings.character,
column:this.settings.column,
data:this.settings.data,
id:this.settings.id,
label:this.settings.label,
row:this.settings.row
})
delete initData.selectedSeat[this.settings.id];
return 'available';
} else if (this.status() == 'unavailable') { //若为已售出状态
return 'unavailable';
} else {
return this.style();
}
}
});
//设置已售出的座位
sc.get(['1_3', '1_4', '4_4', '4_5', '4_6', '4_7', '4_8']).status('unavailable');
interaction.commitSeat();
},
getTotalPrice:function(sc){//计算票价总额
var total = 0;
sc.find('selected').each(function() {
total += price;
});
return total;
},
emit:function(type,msg){
initData.socket.emit(type,msg);
},
socketEvent:function(){
this.emit("login","用户进入选座页面");
initData.socket.on("loginlock",function(loginlock){
for(var t in loginlock){
var isMine = interaction.isMineFire(t,"selected");
if (!isMine) {
$('#'+t).addClass("locking");
}
}
})
initData.socket.on("locking",function(data){
var isMine = interaction.isMineFire(data.id,"selected");
if (!isMine) {
$('#'+data.id).addClass("locking")
}
})
initData.socket.on("canclelocking",function(data){
$('#'+data.id).removeClass("locking");
})
initData.socket.on("userout",function(outuser){
// outuser 为退出用户所选择的座位。
for(var t in outuser){
$('#'+t).removeClass("locking");
}
})
initData.socket.on("seatsold",function(soldseat){
// soldseat 为用户已经购买的座位。 客户端更新座位状态
$.each(soldseat,function(index,item){
$('#'+item).addClass('unavailable');
})
})
},
isMineFire:function(id,type){
return $('#'+id).attr('class').indexOf(type) > 0;
},
commitSeat:function(){
$("#commitSeat").click(function(){
if (JSON.stringify(initData.selectedSeat) === "{}") {
alert("请至少选择一个座位再提交!")
return false;
}
//$.post("http://XXXXXXXX",座位数据,function(){
// 延迟2秒模拟生成订单的ajax请求,请求成功跳转订单页。
setTimeout(function() {
interaction.emit("sold",initData.selectedSeat);
location.href = "/order";
}, 2000);
//})
})
}
}
interaction.initMap();
interaction.socketEvent();
})

5、查看效果

打开浏览器,输入localhost:3000
多打开几个浏览器,可查看实时响应效果

6、注意事项

此处需要修改为你自己的端口,否则会出现监听不到的情况。

基于socket.io的实时在线选座系统


作者 HoChine

2017 年 09月 03日

项目演示: http://hochine.cn/demo/realTimeChooseSeat

GitHub地址: https://github.com/HoChine/RealTime-chooseSeat