前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始模拟抖音网络世界大战

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

作者头像
不期而遇丨
发布2022-09-09 17:58:24
3840
发布2022-09-09 17:58:24
举报
文章被收录于专栏:用户8637799的专栏

效果图

刚开始有这个想法完全归结于看到的抖音 抖音原视频 觉得有意思;点击博客上方绘画,可以预览; 项目架构由: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("\r\n") != -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或者评论

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 效果图
    • 依赖关系:
      • 配置文件
        • mapper-service-实体类基于mybatis-plus自动生成插件
          • controller代码
            • 表结构
            相关产品与服务
            对象存储
            对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档