刚开始有这个想法完全归结于看到的抖音 抖音原视频 觉得有意思;点击博客上方绘画,可以预览; 项目架构由:springboot+mybatis-plus组成 服务器和客户端交互采用WebSocket实现; 结构图:
<?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>
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/
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();
}
}
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 注入对象, 这个类比较特殊 可以在 构造方法中注入,这里通过反射获取
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
<!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或者评论