使用websocket实现协同编辑

1、协同编辑的意思是什么?

其实,协同编辑无非就是字面意思,多人同时编辑,并且能够同步看到对方问保存的数据,典型的例子可以参考石墨文档,腾讯文档。

2、技术解决

核心技术就是信息的实时通信

以及多人编辑时所产生的冲突

这里我采用websocket来进行实时通信,大家都知道他是一个全双工通信协议,经过时间的考证,还是非常好用的,多数流行语言都有与之响应封装好的软件包

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。详细的概念可以百度自行查找,比比皆是

编辑冲突的问题可以使用合并算法,上锁等技术(这里没有做过多的研究,所以我使用下面的方式,嘻嘻)

编辑冲突问题交给用户,当前用户实时地看到了别人正在编辑,那么当前用户就自觉性地停止编辑。

3、实现思路

1. 用户打开图像编辑页面,与后端建立长连接。
2. 后端将当前用户加入当前图像编辑列表。
3. 前端监听用户对于图像内容的修改,每一次修改将整个修改内容发送给后端。
4. 后端接收到信息,不做任何处理,直接将图像信息发送给图像编辑用户列表中其他的所有用户。
5. 前端收到后端的文本信息直接覆盖掉当前图像内容。

4、示例

废话就不多说了,直接上代码(复制粘贴即可使用呦)

这里是使用java语言编写的,采用的是原生注解方式,还有其他实现方式,在这里不一一介绍了(主要是上百度搜的没几个能用的,不是相关代码不全,就是长篇大论,最后还是不行,我实在是不会用啊!!!)

首先我们要使用websocket,肯定是要在pom里导入依赖包的(maven无法解析的,可以添加一下响应的版本号)

        <!-- webscoket        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

创建MyWebSocketConfig文件来注入bean

package com.example.javawebsocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


//@Configuration注解标识的类中声明了1个或者多个@Bean方法,Spring容器可以使用这些方法来注入Bean
@Configuration
public class MyWebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

接下来就是创建一个原生注解的文件

package cn.staitech.system.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;


import cn.staitech.common.security.utils.SecurityUtils;
import com.alibaba.fastjson.parser.JSONToken;
import io.swagger.models.auth.In;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
@Component
//主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
@ServerEndpoint(value = "/websocket/{imageid}")
//此注解相当于设置访问URL
public class WebSocketServer {

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */






    private Session session;

    private String userName;

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

    /**为了保存在线用户信息,在方法中新建一个list存储一下【实际项目依据复杂度,可以存储到数据库或者缓存】**/
    private static Map<Long,Session> sessionPool = new HashMap<Long,Session>();

    private static Map<Integer, CopyOnWriteArraySet<WebSocketServer>> newwebSockets = new HashMap<Integer,CopyOnWriteArraySet<WebSocketServer>>();





    /**
     * 建立连接
     * @param session
     * @param userName
     */
    @OnOpen
    public void onOpen(Session session,@PathParam(value = "imageid") Integer imageid) throws IOException {
        Long userid = SecurityUtils.getUserId();
        this.session = session;
        webSockets.add(this);
        // 如果id不存在,创建一个新的用户存储池,格式为 图像id:[用户1,用户2]
        if(! newwebSockets.containsKey(imageid)){
            CopyOnWriteArraySet<WebSocketServer> webSocketslist =new CopyOnWriteArraySet<>();
            newwebSockets.put(imageid,webSocketslist);
            newwebSockets.get(imageid).add(this);
        }else{
            newwebSockets.get(imageid).add(this);
        }
        sessionPool.put(userid, session);
        Session res = sessionPool.get(userid);
        System.out.println(imageid+"【websocket消息】有新的连接,总数为:"+newwebSockets.get(imageid).size());
    }


    /**
     * 断开连接
     */
    @OnClose
    public void onClose(@PathParam(value = "imageid") Integer imageid) {
        webSockets.remove(this);
        newwebSockets.get(imageid).remove(this);
        System.out.println(imageid + "【websocket消息】连接断开,总数为:"+newwebSockets.get(imageid).size());
    }


    /**
     * 发送错误
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("[连接ID:{}] 错误原因:{}" + this.session + error.getMessage());
    }


    /**
     * 收到信息
     */
    @OnMessage
    public String onMessage(String message) {
        System.out.println("【websocket消息】收到客户端消息:"+message);
        return message;
    }

    // 此为广播消息
    public void sendAllMessage(String message,Integer imageid) {
        for(WebSocketServer webSocket : newwebSockets.get(imageid)) {
            System.out.println("【websocket消息】广播消息:"+message);
            try {
                webSocket.session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    // 此为单点消息
    public void sendOneMessage(String userName, String message) {
        System.out.println("【websocket消息】单点消息:"+message);
        System.out.println(sessionPool);
        Session session = sessionPool.get(userName);
        if (session != null) {
            try {
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

我们我两个接口来调用一下上面两个接口

package com.example.javawebsocket;


import com.example.javawebsocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/annotation")
public class Websocket {


    @Autowired
    private WebSocketServer webSocketServer;

//    @GetMapping("/sendAllWebSocket")
//    public String test() {
//        String text="你们好!这是websocket群体发送!";
//        webSocketServer.sendAllMessage(text);
//        return text;
//    }

    @GetMapping("/sendOneWebSocket/{userName}")
    public String sendOneWebSocket(@PathVariable("userName") String userName) {
        String text=userName+" 你好! 这是websocket单人发送!";
        webSocketServer.sendOneMessage(userName,text);
        return text;
    }


    @CrossOrigin
    @RequestMapping(value = "/newimage",method = RequestMethod.POST)
    public String newimage(@RequestBody socketvo  req){
        webSocketServer.sendAllMessage(req.getImagename(), req.getImageid());
        System.out.println(req.getImagename());
        return "ok";
    }

}

基本上大致就是酱紫了,可以根据不同需求来自行更改

posted @ 2022-08-29 10:23  Μikey  阅读(419)  评论(0编辑  收藏  举报