从零开始模拟抖音网络世界大战

2022-09-09 17:58:24 浏览数 (1)

效果图

刚开始有这个想法完全归结于看到的抖音 抖音原视频 觉得有意思;点击博客上方绘画,可以预览; 项目架构由:springboot mybatis-plus组成 服务器和客户端交互采用WebSocket实现; 结构图:

依赖关系:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>war</packaging>

    <groupId>org.example</groupId>
    <artifactId>websocket</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <!--引入spring boot parent ,统一boot依赖版本号-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>

    <dependencies>

        <!--启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JSONObject对象依赖的jar包 开始 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.6.graal</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

        <!-- mybatis-plus 所需依赖  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--自动生成工具-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- 开发热启动 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- websocket依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!--配置spring-jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--引入测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--引入数据库依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- thmeleaf模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

</project>

配置文件

代码语言:javascript复制
server.port=8081
logging.level.com.kfd=debug

# mysql
spring.datasource.url=jdbc:mysql://localhost:3306/webSocket
spring.datasource.username=root
spring.datasource.password=root

# 连接参数
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=10

spring.jmx.default-domain=websocket

mybatis-plus.mapper-locations=classpath:/mapper/*.xml
# thymeleaf
spring.thymeleaf.cache=false
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/templates/

mapper-service-实体类基于mybatis-plus自动生成插件

代码语言:javascript复制
public class MybatisPlusGenerator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/websocket?useUnicode=true&useSSL=false&characterEncoding=utf8", "root", "root")
                .globalConfig(builder -> {
                    builder.author("game") // 设置作者
                            //.enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir(System.getProperty("user.dir")   "/src/main/java"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.kfd") // 设置父包名
//                            .moduleName("springboot") // 设置父包模块名
                            // .service()  // 设置自定义service路径,不设置就是默认路径
                            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, System.getProperty("user.dir")   "/src/main/resources/mapper/")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("game") // 设置需要生成的表名
//                            .addTablePrefix("t_", "c_")
                            // 设置自动填充的时间字段
//                            .entityBuilder().addTableFills(
//                                    new Column("create_time", FieldFill.INSERT), new Column("update_time", FieldFill.INSERT_UPDATE))
                    ; // 设置过滤表前缀

                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

controller代码

代码语言:javascript复制
package com.kfd.controller;


import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.kfd.entity.Game;
import com.kfd.service.impl.GameServiceImpl;
import com.kfd.utils.SpringUtil;
import org.json.JSONException;
import org.springframework.stereotype.Controller;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Controller
@ServerEndpoint("/ws/{username}")
public class MyWebSocket {

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //发送消息
    public void sendMessage(Session session, String message) throws IOException {
        if (session != null) {
            synchronized (session) {
                session.getBasicRemote().sendText(message);
            }
        }
    }

    //给指定用户发送信息
    public void sendInfo(String userName, String message) {
        Session session = sessionPools.get(userName);
        try {
            sendMessage(session, message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 群发消息
    public void broadcast(String message) {
        for (Session session : sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

    //建立连接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName) {
        sessionPools.put(userName, session);
        addOnlineCount();
        // 广播上线消息
        GameServiceImpl iGameService = SpringUtil.getBean(GameServiceImpl.class);
        List<Game> list = iGameService.list();
        for (Game game : list) {
            sendInfo(userName, JSON.toJSONString(game));
        }
        broadcast(userName.substring(0, 1)   "***:进入房间!当前共:"   onlineNum   "人");
    }

    //关闭连接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName) {
        sessionPools.remove(userName);
        subOnlineCount();
        // 广播下线消息
        broadcast(userName.substring(0, 1)   "***退出房间!当前人数为"   onlineNum);
    }

    //收到客户端信息后,群发
    @OnMessage
    public void onMessage(String message) throws IOException, JSONException {
        Game game = JSON.parseObject(message, Game.class);
        //只有坐标在图中的时候才添加数据库
        if (Integer.parseInt(game.getCoorx()) <= 990 && Integer.parseInt(game.getCoory()) <= 990) {
            GameServiceImpl iGameService = SpringUtil.getBean(GameServiceImpl.class);
            //查看该点位是否存在消息,存在则删除
            Game one = iGameService.getOne(Wrappers.<Game>lambdaQuery()
                    .eq(Game::getCoorx, game.getCoorx()).eq(Game::getCoory, game.getCoory()));
            if (one != null) {
                iGameService.remove(Wrappers.<Game>lambdaQuery()
                        .eq(Game::getCoorx, game.getCoorx()).eq(Game::getCoory, game.getCoory()));
            }
            iGameService.save(game);
            broadcast(message);
        }
    }

    //错误时调用
    @OnError
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
    }

    public static void addOnlineCount() {
        onlineNum.incrementAndGet();
    }

    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }

    public static AtomicInteger getOnlineNumber() {
        return onlineNum;
    }

    public static ConcurrentHashMap<String, Session> getSessionPools() {
        return sessionPools;
    }
}

这里有一个坑,虽然是springboot项目,使用@ServerEndpoint后即无法通过 @Autowired 注入对象, 这个类比较特殊 可以在 构造方法中注入,这里通过反射获取

代码语言:javascript复制
package com.kfd.utils;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

前端使用了layui框架的颜色提取器,画板基于canvas

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:layout="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <title>Insert title here</title>
    <link rel="stylesheet" href="https://www.layuicdn.com/layui/css/layui.css">
</head>
<body>
<div style="margin-left: 30px;">
    <form class="layui-form" action="">
        <div class="layui-form-item">
            <div class="layui-input-inline" style="width: 120px;">
                <input type="text" value="" disabled="disabled" placeholder="请选择颜色"
                       class="layui-input" id="test-form-input">
            </div>
            <div class="layui-inline" style="left: -11px;">
                <div id="test-form"></div>
            </div>
            <a id="bb" download="不期而遇">
                <img id="image" style="display: none"/>
            </a>
            <button id="save" type="button">保存为图片</button>
        </div>
    </form>
</div>
<div class="layui-form-item">
    <div style="margin-left: 50px;" class="layui-input-inline">
			<textarea id="message_content" class="form-control aa"
                      readonly="readonly"
                      style="display:block;overflow:auto;width:200px;height:996px;font-family: “Arial”,“Microsoft YaHei”,“黑体”,“宋体”,sans-serif;text-shadow: 0 0 0.5vw rgba(96,229,138,0.66), 0 0 0.1vw #47ece5, 0 0 0.1vw #d8e067, 0 0 0.1vw #f1be51;"
                      cols="50" rows="55"></textarea>
    </div>
    <div class="container layui-input-inline">
        <canvas id="myCanvas" width="1000" height="1000"
                style="border: 1px solid #d3d3d3;">
        </canvas>
    </div>
</div>
<div id="aaa" style="display: none" layout:fragment="form-layer">
    <div class="admin-login-background">
        <div class="layui-form login-form">
            <form class="layui-form" action="">
                <div class="layui-form-item">
                    <input type="text" name="title" id="title" required
                           lay-verify="title" autocomplete="off" placeholder="你的名字"
                           class="layui-input"/>
                </div>
            </form>
        </div>
    </div>
</div>

</body>
<script src=" https://www.layuicdn.com/layui/layui.js"></script>
<script src="/js/jquery-3.4.1.js"></script>
<script src="http://pv.sohu.com/cityjson?ie=utf-8"></script>
<script>
    layui.use(['colorpicker', 'layer', 'form'], function () {
        var $ = layui.$, colorpicker = layui.colorpicker;
        var layer = layui.layer;
        var ws = null;
        colorpicker.render({
            elem: '#test-form',
            done: function (color) {
                $('#test-form-input').val(color);
            }
        });

        var canvas = document.getElementById("myCanvas");

        var a = new run(canvas)

        $("#save").click(function () {
            layer.confirm('确定要保存为图片吗?', {
                btn: ['确定', '取消']
                // 按钮
            }, function (i) {
                // var image = new Image();
                var image = document.getElementById("image");
                var bb = document.getElementById("bb");
                image.src = a.canvas.toDataURL("image/png");
                bb.href = a.canvas.toDataURL("image/png");
                bb.click();
                layer.close(i);
            })

        });

        layer.ready(function () {
            layer.open({
                title: "登录",
                type: 1,
                closeBtn: 0,
                content: $('#aaa'), // 弹出层容器
                btn: ['确认'],
                yes: function (index, layero) {
                    if ($('#title').val() == "" || $('#title').val() == null || $('#title').val().length > 5) {
                        layer.msg('名称不能为空,且长度不能大于5', {
                            icon: 2
                        });
                        return false;
                    } else {
                        if ('WebSocket' in window) {
                            ws = new WebSocket("ws://127.0.0.1:8081/ws/"   $('#title').val());
                        } else {
                            alert("当前浏览器不支持WebSocket,请换个浏览器重试");
                            return false;
                        }
                        ws.onopen = function () {
                            console.log("建立 websocket 连接...");
                            //ip地址
                            //ws.send(JSON.stringify(returnCitySN));
                        };
                        $('#aaa').attr("style", "display:none;");
                        layer.closeAll('page');
                        ws.onmessage = function (event) {
                            //服务端发送的消息
                            try {
                                var obj = JSON.parse(event.data);

                                a.drawBgBox(parseInt(obj.coorx), parseInt(obj.coory), true, obj.colour)
                            } catch (error) {
                                if (event.data.indexOf("rn") != -1) {
                                    eval(event.data);
                                } else {
                                    $('#message_content').append(
                                        event.data   'n');
                                }
                            }
                        };
                        ws.error = function () {
                            alert("连接出错");
                        }
                        ws.close = function () {
                            alert("连接关闭");
                        }
                    }
                }
            });
        })
    });

</script>
<style>
    canvas {
        padding-left: 0;
        padding-right: 0;
        margin-left: auto;
        margin-right: auto;
        display: block;
        width: 1000px;
        background-color: #d3d3d3;
    }

    body {
        cursor: crosshair;


    }

    .container {
        margin: 0 auto;
        width: 1000px;
    }
</style>
</html>

表结构

服务器内存有限,这里只存储改变后的颜色,即之前存在值,则删除后再新增,非改变颜色操作则不存储; 保存图片按钮,不支持手机端,手机访问不太友好

有问题可以联系qq或者评论

0 人点赞