基于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、注意事项

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

注意事项


作者 HoChine
2017 年 09月 03日
项目演示: http://hochine.cn/demo/realTimeChooseSeat
GitHub地址: https://github.com/HoChine/RealTime-chooseSeat

posted @ 2017-09-19 18:11  HoChine  阅读(1308)  评论(4编辑  收藏  举报