SpringBoot使用WebSocket实现即时消息

开发 前端
本篇给大家介绍SpringBoot使用WebSocket实现即时消息,希望能够帮助到你!

[[389469]]

 环境:SpringBoot2.3.9.RELEASE

依赖

<dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-web</artifactId> 
</dependency> 
<dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-websocket</artifactId> 
</dependency> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

定义消息类型

抽象消息对象

public class AbstractMessage { 
    /** 
     *  消息类型 
     */ 
    protected String type ; 
     
    /** 
     *  消息内容 
     */ 
    protected String content ; 
    /** 
     *  消息日期 
     */ 
    protected String date ; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

消息对象子类

1、Ping检查消息

public class PingMessage extends AbstractMessage { 
    public PingMessage() {} 
    public PingMessage(String type) { 
        this.type = type ; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

 2、系统消息

public class SystemMessage extends AbstractMessage { 
    public SystemMessage() {} 
    public SystemMessage(String type, String content) { 
        this.type = type ; 
        this.content = content ; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

 3、点对点消息

public class PersonMessage extends AbstractMessage { 
    private String fromName ; 
    private String toName ; 

  • 1.
  • 2.
  • 3.
  • 4.

 消息类型定义 

public enum MessageType { 
     
    /** 
     *  系统消息 0000;心跳检查消息 0001;点对点消息2001 
     */ 
    SYSTEM("0000"), PING("0001"), PERSON("2001") ; 
     
    private String type ; 
     
    private MessageType(String type) { 
        this.type = type ; 
    } 
 
    public String getType() { 
        return type; 
    } 
 
    public void setType(String type) { 
        this.type = type; 
    } 
     

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

 WebSocket服务端点

该类作用就是定义客户端连接的地址

@ServerEndpoint(value = "/message/{username}",  
    encoders = {WsMessageEncoder.class}, 
    decoders = {WsMessageDecoder.class}, 
    subprotocols = {"gmsg"}, 
    configurator = MessageConfigurator.class)   
@Component   
public class GMessageListener {   
   
    public static ConcurrentMap<String, UserSession> sessions = new ConcurrentHashMap<>(); 
    private static Logger logger = LoggerFactory.getLogger(GMessageListener.class) ; 
   
    private String username ; 
     
    @OnOpen   
    public void onOpen(Session session, EndpointConfig config, @PathParam("username") String username){ 
        UserSession userSession = new UserSession(session.getId(), username, session) ; 
        this.username = username ; 
        sessions.put(username, userSession) ; 
        logger.info("【{}】用户进入, 当前连接数:{}", username, sessions.size()) ;  
    }   
   
    @OnClose   
    public void onClose(Session session, CloseReason reason){   
        UserSession userSession = sessions.remove(this.username) ; 
        if (userSession != null) { 
            logger.info("用户【{}】, 断开连接, 当前连接数:{}", username, sessions.size()) ; 
        } 
    } 
     
    @OnMessage 
    public void pongMessage(Session session, PongMessage message) { 
        ByteBuffer buffer = message.getApplicationData() ; 
        logger.debug("接受到Pong帧【这是由浏览器发送】:" + buffer.toString()); 
    } 
     
    @OnMessage 
    public void onMessage(Session session, AbstractMessage message) { 
        if (message instanceof PingMessage) { 
            logger.debug("这里是ping消息"); 
            return ; 
        } 
        if (message instanceof PersonMessage) { 
            PersonMessage personMessage = (PersonMessage) message ; 
            if (this.username.equals(personMessage.getToName())) { 
                logger.info("【{}】收到消息:{}", this.username, personMessage.getContent()); 
            } else { 
                UserSession userSession = sessions.get(personMessage.getToName()) ; 
                if (userSession != null) { 
                    try { 
                        userSession.getSession().getAsyncRemote().sendText(new ObjectMapper().writeValueAsString(message)) ; 
                    } catch (JsonProcessingException e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
            return ; 
        } 
        if (message instanceof SystemMessage) { 
            logger.info("接受到消息类型为【系统消息】") ;  
            return ; 
        } 
    } 
     
    @OnError 
    public void onError(Session session, Throwable error) { 
        logger.error(error.getMessage()) ; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

 WsMessageEncoder.java类

该类的主要作用是,当发送的消息是对象时,该如何转换

public class WsMessageEncoder implements Encoder.Text<AbstractMessage> { 
    private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ; 
    @Override 
    public void init(EndpointConfig endpointConfig) { 
    } 
    @Override 
    public void destroy() { 
    } 
    @Override 
    public String encode(AbstractMessage tm) throws EncodeException { 
        String message = null ; 
        try { 
            message = new ObjectMapper().writeValueAsString(tm); 
        } catch (JsonProcessingException e) { 
            logger.error("JSON处理错误:{}", e) ; 
        } 
        return message; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

 WsMessageDecoder.java类

该类的作用是,当接收到消息时如何转换成对象。

public class WsMessageDecoder implements  Decoder.Text<AbstractMessage> { 
 
    private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ; 
    private static Set<String> msgTypes = new HashSet<>() ; 
     
    static { 
        msgTypes.add(MessageType.PING.getType()) ; 
        msgTypes.add(MessageType.SYSTEM.getType()) ; 
        msgTypes.add(MessageType.PERSON.getType()) ; 
    } 
    @Override 
    @SuppressWarnings("unchecked"
    public AbstractMessage decode(String s) throws DecodeException { 
        AbstractMessage message = null ; 
        try { 
            ObjectMapper mapper = new ObjectMapper() ; 
            Map<String,String> map = mapper.readValue(s, Map.class) ; 
            String type = map.get("type") ; 
            switch(type) { 
                case "0000"
                    message = mapper.readValue(s, SystemMessage.class) ; 
                    break; 
                case "0001"
                    message = mapper.readValue(s, PingMessage.class) ; 
                    break; 
                case "2001"
                    message = mapper.readValue(s, PersonMessage.class) ; 
                    break; 
            } 
        } catch (JsonProcessingException e) { 
            logger.error("JSON处理错误:{}", e) ; 
        } 
        return message ; 
    } 
 
    // 该方法判断消息是否可以被解码(转换) 
    @Override 
    @SuppressWarnings("unchecked"
    public boolean willDecode(String s) { 
        Map<String, String> map = new HashMap<>() ; 
        try { 
            map = new ObjectMapper().readValue(s, Map.class); 
        } catch (JsonProcessingException e) { 
            e.printStackTrace(); 
        } 
        logger.debug("检查消息:【" + s + "】是否可以解码") ; 
        String type = map.get("type") ; 
        if (StringUtils.isEmpty(type) || !msgTypes.contains(type)) { 
            return false ; 
        } 
        return true ; 
    } 
    @Override 
    public void init(EndpointConfig endpointConfig) { 
    } 
    @Override 
    public void destroy() { 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.

 MessageConfigurator.java类

该类的作用是配置服务端点,比如配置握手信息

public class MessageConfigurator extends ServerEndpointConfig.Configurator { 
    private static Logger logger = LoggerFactory.getLogger(MessageConfigurator.class) ; 
    @Override 
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { 
        logger.debug("握手请求头信息:" + request.getHeaders()); 
        logger.debug("握手响应头信息:" + response.getHeaders()); 
        super.modifyHandshake(sec, request, response); 
    }    

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

 WebSocke配置类

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

 当以jar包形式运行时需要配置该bean,暴露我们配置的@ServerEndpoint;当我们以war独立tomcat运行时不能配置该bean。

前端页面

<!doctype html> 
<html> 
 <head> 
  <meta charset="UTF-8"
  <meta name="Author" content=""
  <meta name="Keywords" content=""
  <meta name="Description" content=""
  <script src="g-messages.js?v=1"></script> 
  <title>WebSocket</title> 
  <style type="text/css"
  </style> 
  <script> 
    let gm = null ; 
    let username = null ; 
    function ListenerMsg({url, protocols = ['gmsg'], options = {}}) { 
        if (!url){  
            throw new Error("未知服务地址") ; 
        } 
        gm = new window.__GM({ 
            url: url, 
            protocols: protocols 
        }) ; 
        gm.open(options) ; 
    } 
    ListenerMsg.init = (user) => { 
        if (!user) { 
            alert("未知的当前登录人") ; 
            return ; 
        } 
        let url = `ws://localhost:8080/message/${user}` ; 
        let msg = document.querySelector("#msg"
        ListenerMsg({url, options: { 
            onmessage (e) { 
                let data = JSON.parse(e.data) ; 
                let li = document.createElement("li") ; 
                li.innerHTML = "【" + data.fromName + "】对你说:" + data.content ; 
                msg.appendChild(li) ; 
            } 
        }}) ; 
    } 
    function enter() { 
        username = document.querySelector("#nick").value ; 
        ListenerMsg.init(username) ; 
        document.querySelector("#chat").style.display = "block" ; 
        document.querySelector("#enter").style.display = "none" ; 
        document.querySelector("#cu").innerText = username ; 
    } 
    function send() { 
        let a = document.querySelector("#toname") ; 
        let b = document.querySelector("#content") ; 
        let toName = a.value ; 
        let content = b.value ; 
        gm.sendMessage({type: "2001", content, fromName: username, toName}) ; 
        a.value = '' ; 
        b.value = '' ; 
    } 
  </script> 
 </head> 
 <body> 
    <div id="enter"
        <input id="nick"/><button type="button" onclick="enter()">进入</button> 
    </div> 
    <hr/> 
    <div id="chat" style="display:none;"
        当前用户:<b id="cu"></b><br/> 
        用户:<input id="toname" name="toname"/><br/><br/> 
        内容:<textarea id="content" rows="3" cols="22"></textarea><br/> 
        <button type="button" onclick="send()">发送</button> 
    </div> 
    <div> 
        <ul id="msg"
        </ul> 
    </div> 
 </body> 
</html> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.

到此所有的代码完毕,接下来测试

测试

打开两个标签页,以不同的用户进入。

输入对方用户名发送消息

成功了,简单的websocket。我们生产环境还就这么完的,8g内存跑了6w的用户。

完毕!!!

 

 

责任编辑:姜华 来源: 今日头条
相关推荐

2023-08-14 08:01:12

websocket8g用户

2020-10-09 12:45:19

创建消息即时消息编程语言

2020-10-09 15:00:56

实时消息编程语言

2019-10-28 20:12:40

OAuthGuard中间件编程语言

2019-09-29 15:25:13

CockroachDBGoJavaScript

2020-03-31 12:21:20

JSON即时消息编程语言

2020-10-12 09:20:13

即时消息Access页面编程语言

2020-10-19 16:20:38

即时消息Conversatio编程语言

2020-10-16 14:40:20

即时消息Home页面编程语言

2021-02-05 07:28:11

SpringbootNettyWebsocke

2015-03-18 15:37:19

社交APP场景

2022-06-28 08:37:07

分布式服务器WebSocket

2020-10-10 20:51:10

即时消息编程语言

2023-07-26 07:28:55

WebSocket服务器方案

2021-03-26 08:16:32

SpringbootWebsocket前端

2024-09-02 09:31:19

2010-05-24 09:51:37

System Cent

2024-09-12 14:50:08

2024-11-14 11:56:45

2024-09-11 08:35:54

点赞
收藏

51CTO技术栈公众号