• 基于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

  • 相关阅读:
    牛顿迭代法
    C语言补遗
    Manjaro添加开机启动脚本
    tensorboard查看pytorch模型
    Kindle最佳排版记录
    01背包问题详解
    差分法介绍
    BFS详解
    UVA11732 "strcmp()" Anyone?
    LA3942 Remember the Word
  • 原文地址:https://www.cnblogs.com/HoChine/p/7553236.html
Copyright © 2020-2023  润新知