websocket使用案例
[TOC]
实现效果
http://blog.qianchen.link/clipboard
设备A生成二维码,设备B扫描设备A的二维码后,可将设备A和设备B的输入框内容实时同步
原理
前端向后端获取剪切板标识和二维码(用于其他设备连接到同一剪切板),当输入框内容改变时发送消息到服务器,当收到服务器发送的消息时将内容赋值到输入框
后端将连接以<剪切板标识,<设备标识,连接Session>>的格式保存为静态变量,当收到客户端发送的消息时将消息发送到同一剪切板标识的设备
前端部分(nuxt)
前端部分使用nuxt(vue),其他框架代码也是类似,可根据情况自行修改
<template>
<div>
<!-- {{code}},{{baseUrl}} -->
<div style="width: 90%;margin: 0 auto;">
<h1>websocket剪切板</h1>
<div >
<Input v-model="text" type="textarea" show-word-limit autofocus :autosize="{minRows: 2}" placeholder="请输入内容..." @on-change="textChange"></Input>
</div>
<div v-if="img">
<h3>扫码二维码即可建立同步剪切板</h3>
<img :src="img" alt="">
</div>
</div>
<!-- <Button></Button> -->
</div>
</template>
<script>
export default {
data(){
return{
text:'',
socket:null,
isOpen:false,
code:null,
img:null,
path:'ws://blog.qianchen.link/api/clipboard/',
baseUrl:null,
}
},
methods:{
init() {
if(typeof(WebSocket) === "undefined"){
alert("您的浏览器不支持socket")
}else{
// 实例化socket
let date=new Date()
//加入时间用于区分不同设备
this.socket = new WebSocket(this.path+this.code+","+date.getMinutes()+date.getSeconds())
// this.socket.data
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.onmessage
}
},
open: function () {
console.log("socket连接成功")
this.isOpen=true;
this.$Message.success("socket连接成功");
},
error: function () {
console.log("连接错误")
this.isOpen=false
this.$Message.error("socket连接失败");
},
onmessage: function (msg) {
console.log(msg)
this.text=msg.data
},
send: function () {
this.socket.send(JSON.stringify({
code:this.code,
text:this.text
}))
},
close: function () {
console.log("socket已经关闭")
this.$Message.info("socket连接已关闭");
this.isOpen=false
},
textChange(event){
console.log(event)
this.send()
}
},
mounted(){
if (process.client) {
console.log(document.location.origin+document.location.pathname)
if (this.$route.query.code) {
//如果从URL参数中获取到code则代表是扫码连接,直接使用扫描到的参数连接剪切板
this.code=this.$route.query.code
this.init()
}else{
// console.log(document.location.host)
//URL中没有参数则向服务器获取code(剪切板标识)和二维码(供其他设备连接到同一剪切板用)
this.$axios.get('/clipboard/code?url='+document.location.origin+document.location.pathname).then((res=>{
this.code=res.data.data.code
this.img=res.data.data.img
console.log(res)
console.log(this.code)
this.init()
}))
}
}
},
destroyed () {
// 销毁监听
if ( this.socket) {
this.socket.onclose = this.close
this.$Message.info("socket连接已关闭");
}
}
}
</script>
<style>
</style>
后端部分(springboot)
添加依赖
<!-- spring的websocket依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!-- 生成二维码-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.0</version>
</dependency>
配置类(SpringWebSocketConfig.java)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class SpringWebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
消息对象类(ClipboardTO.java)
package com.qc.blog.to;
import lombok.Data;
/**
* @author zzq
*/
@Data
public class ClipboardTO {
String text;
String code;
}
websocket处理类(ClipboardSocketServer.java)——核心部分
package com.qc.blog.socket;
import com.alibaba.fastjson.JSON;
import com.qc.blog.to.ClipboardTO;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author zzq
*/
@ServerEndpoint("/clipboard/{code}")
@Component
public class ClipboardSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static AtomicInteger onlineNum = new AtomicInteger();
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
* <剪切板标识,<设备标识,连接Session>>
*/
private static ConcurrentHashMap<String, Map<String,Session>> sessionPools = new ConcurrentHashMap<>();
/**
* 建立连接成功调用
* @param session
* @param code
*/
@OnOpen
public void onOpen(Session session,@PathParam(value = "code") String code){
String[] fields = code.split(",");
Map<String, Session> stringSessionMap = sessionPools.get(fields[0]);
if (stringSessionMap == null) {
// 如果此标识的剪切板不存在,则初始化剪切板并将表示加入map中
stringSessionMap=new HashMap<>();
}
// 将自己加入对应的剪切板
stringSessionMap.put(fields[1],session);
sessionPools.put(fields[0],stringSessionMap);
System.out.println(code + "加入webSocket!当前人数为" + onlineNum);
}
/**
* 关闭连接时调用
* @param code
*/
@OnClose
public void onClose(@PathParam(value = "code") String code){
String[] fields = code.split(",");
Map<String, Session> stringSessionMap = sessionPools.get(fields[0]);
stringSessionMap.remove(fields[1]);
if (stringSessionMap.size()==0){
// 如果自己是最后一个连接,则将剪切板删除
sessionPools.remove(fields[0]);
}
System.out.println(code + "断开webSocket连接!当前人数为" + onlineNum);
}
/**
* 收到客户端消息时调用
* @param message
* @throws IOException
*/
@OnMessage
public void onMessage(String message) throws IOException{
System.out.println("server get" + message);
ClipboardTO clipboardTO=JSON.parseObject(message, ClipboardTO.class);
sendText(clipboardTO.getCode(), clipboardTO.getText());
}
/**
* 获取剪切板标识,然后将消息发送到该剪切板的全部设备
* @param code
* @param text
* @throws IOException
*/
private void sendText(String code, String text) throws IOException {
String[] fields = code.split(",");
Map<String, Session> sessionMap = sessionPools.get(fields[0]);
if (sessionMap != null) {
sessionMap.values().forEach(session -> {
try {
// 发送消息到客户端
session.getBasicRemote().sendText(text);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
/**
* 错误时调用
* @param session
* @param throwable
*/
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("发生错误");
throwable.printStackTrace();
}
}
获取连接初始化信息对象(ClipboardVo.java)
package com.qc.blog.vo;
import lombok.Data;
/**
* @author zzq
*/
@Data
public class ClipboardVo {
/**
* 剪切板标识(区分不同用户使用的剪切板)
*/
private String code;
/**
* 手机端连接时的二维码
*/
private String img;
}
ClipboardController(ClipboardController.java)
IdGeneratorSnowflake为UUID生成类,可自行更换(如java自带UUID)
package com.qc.blog.controller;
import com.qc.blog.utils.IdGeneratorSnowflake;
import com.qc.blog.utils.QrCodeUtils;
import com.qc.blog.utils.Res;
import com.qc.blog.vo.ClipboardVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zzq
*/
@RestController
@RequestMapping("clipboard")
@CrossOrigin
public class ClipboardController {
@Autowired
private IdGeneratorSnowflake idGeneratorSnowflake;
@GetMapping("code")
public Res getCode(String url){
ClipboardVo clipboardVo = new ClipboardVo();
long code = idGeneratorSnowflake.snowflakeId();
clipboardVo.setCode(String.valueOf(code));
String img = QrCodeUtils.createCode(url + "?code=" + code);
clipboardVo.setImg(img);
return Res.data(clipboardVo);
}
}
二维码生成工具类(QrCodeUtils.java)
package com.qc.blog.utils;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.util.Base64Utils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
/**
* @author zzq
*/
public class QrCodeUtils {
public QrCodeUtils() {
}
public static String createCode(String content){
int width=300;
int height=300;
String format="png";
HashMap hashMap=new HashMap();
hashMap.put(EncodeHintType.CHARACTER_SET, "utf-8");
hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hashMap.put(EncodeHintType.MARGIN, 2);
BitMatrix bitMatrix=null;
try {
bitMatrix= new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE , width, height,hashMap);
} catch (WriterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
try {
ImageIO.write(image, "png", byteArrayOutputStream);
} catch (IOException e) {
e.printStackTrace();
}
String img="data:image/png;base64,"+ Base64Utils.encodeToString(byteArrayOutputStream.toByteArray());
return img;
}
}