基於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、注意事項
此處需要修改為你自己的端口,否則會出現監聽不到的情況。
作者 HoChine
2017 年 09月 03日
項目演示: http://hochine.cn/demo/realTimeChooseSeat
GitHub地址: https://github.com/HoChine/RealTime-chooseSeat