Spring Boot 2.2 原生注解简易集成 websocket

1 摘要

Websocket 是一种基于 TCP 连接的可进行双工通讯的协议,能够更好地方便客户端与服务端数据传输,允许服务端向客户端主动发送消息。相较于 HTTP 连接, websocket 不需要3次握手,1次握手之后即可实现服务端与客户端的数据通讯。Websocket 通常用于实时聊天通讯场景,本文将介绍在 SpringBoot 中使用原生注解简易集成 websocket。

2 核心 Maven 依赖

./demo-websocket/pom.xml
        <!-- Websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

其中 springboot 的版本为 2.2.5.RELEASE

3 配置信息

./demo-websocket/src/main/resources/application.yml
## config

## server
server:
  port: 8200

websocket 的端口即为服务端口,若不自定义,则默认为 8080 端口

4 核心代码类

4.1 websocket 配置类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/config/WebsocketConfig.java
package com.ljq.demo.springboot.websocket.common.config;

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

/**
 * @Description: websocket 配置信息
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
@Configuration
@EnableWebSocket
public class WebsocketConfig {

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

4.2 websocket 消息体封装类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/model/entity/SocketMessage.java
package com.ljq.demo.springboot.websocket.model.entity;

import lombok.Data;

/**
 * @Description: 聊天信息封装类
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
@Data
public class SocketMessage {

    /**
     * 消息发送者
     */
    private String from;
    /**
     * 消息接收者
     */
    private String to;
    /**
     * 消息内容
     */
    private String content;
}

4.3 socket 消息编码类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/util/SocketEncoder.java
package com.ljq.demo.springboot.websocket.common.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

/**
 * @Description: socket 消息编码器
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
public class SocketEncoder implements Encoder.Text<SocketMessage> {

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String encode(SocketMessage socketMessage) throws EncodeException {
        try {
            return objectMapper.writeValueAsString(socketMessage);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

4.4 socket 消息解码类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/common/util/SocketDecoder.java
package com.ljq.demo.springboot.websocket.common.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

/**
 * @Description: websocket 消息解码器
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
public class SocketDecoder implements Decoder.Text<SocketMessage> {

    private static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public SocketMessage decode(String s) throws DecodeException {

        try {
            return objectMapper.readValue(s, SocketMessage.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean willDecode(String s) {
        return (s != null);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

4.5 websocket 服务端站点类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/web/ChatEndPoint.java
package com.ljq.demo.springboot.websocket.web;

import com.ljq.demo.springboot.websocket.common.util.SocketDecoder;
import com.ljq.demo.springboot.websocket.common.util.SocketEncoder;
import com.ljq.demo.springboot.websocket.model.entity.SocketMessage;
import lombok.extern.slf4j.Slf4j;
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.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Description: websocket 服务端站点
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
@Slf4j
@Component
@ServerEndpoint(value = "/chat/{username}", encoders = {SocketEncoder.class}, decoders = {SocketDecoder.class})
public class ChatEndPoint {

    private Session session;

    private static Set<ChatEndPoint> chatEndPoints = new CopyOnWriteArraySet<>();

    private static Map<String, String> users = new HashMap<>();

    /**
     * 打开 socket 连接
     *
     * @param session
     * @param username
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        log.debug("Websocket is opening ");

        this.session = session;
        chatEndPoints.add(this);
        users.put(session.getId(), username);
        log.debug("username: {}", username);

        SocketMessage message = new SocketMessage();
        message.setFrom(username);
        message.setContent("Connected !!!, online user: " + users.size());
        broadcast(message);
        log.debug("online user: {}", users.size());
    }

    /**
     * 发送信息
     *
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(Session session, SocketMessage message) {
        message.setFrom(users.getOrDefault(session.getId(), ""));
        log.debug("message: {}", message.toString());

        broadcast(message);
        log.debug("online user: {}", users.size());
    }

    /**
     * 关闭 socket 连接
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        log.debug("Socket is closing .");
        log.debug("username: {}", users.getOrDefault(session.getId(), ""));

        chatEndPoints.remove(this);
        users.remove(session.getId());

        SocketMessage message = new SocketMessage();
        message.setFrom(users.getOrDefault(session.getId(), ""));
        message.setContent("Socket disconnected .");
        broadcast(message);
        log.debug("online user: {}", users.size());
    }

    /**
     * 异常
     *
     * @param session
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        log.error("Socket exception", throwable);
        log.debug("online user: {}", users.size());
    }

    /**
     * 信息发送
     *
     * @param message
     */
    private static void broadcast(SocketMessage message) {
        chatEndPoints.forEach(chatEndPoint -> {
            synchronized (chatEndPoint) {
                try {
                    chatEndPoint.session.getBasicRemote().sendObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (EncodeException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

websocket 注解说明:

@ServerEndpoint : websocket 服务端站点注解,用于Java类,类似于 @Controller, 使用该注解的类,即可接收客户端发来的 socket 信息

@OnOpen: 该注解用于方法上,打开 socket 连接时触发

@OnMessage: 该注解用于方法上,在客户端发送消息时触发

@OnClose: 该注解用于方法上,当关闭 socket 连接时触发

@OnError: 该注解用于方法上,当 socket 连接出现异常时触发,触发异常,会导致 socket 连接断开。通常发送的消息不合法、格式错误会导致异常抛出。

4.6 SpringBoot 启动类
./demo-websocket/src/main/java/com/ljq/demo/springboot/websocket/DemoWebsocketApplication.java
package com.ljq.demo.springboot.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Description: websocket SpringBoot 启动类
 * @Author: junqiang.lu
 * @Date: 2020/3/14
 */
@SpringBootApplication
public class DemoWebsocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoWebsocketApplication.class, args);
    }

}

5 测试

在线 websocket 测试地址:

WebSocket在线测试工具

在线WebSocket测试工具

启动项目,在测试网站输入 websocket 地址:

ws://127.0.0.1:8200/chat/demo1

127.0.0.1 为本机 ip 地址,可根据需要自行更改 ip 地址

demo1 为用户名,测试时可自行更换

连接成功,返回的结果为:

{
    "from": "demo1",
    "to": null,
    "content": "Connected !!!, online user: 1"
}

控制台日志:

2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 42| Websocket is opening 
2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 47| username: demo1
2020-03-18 16:49:29 | DEBUG | http-nio-8200-exec-1 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 53| online user: 1

发送消息,消息内容:

{
    "to": "demo2",
    "content": "Hello"
}

返回结果:

{
    "from": "demo1",
    "to": "demo2",
    "content": "Hello"
}

控制台日志:

2020-03-18 16:52:18 | DEBUG | http-nio-8200-exec-2 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 65| message: SocketMessage(from=demo1, to=demo2, content=Hello)
2020-03-18 16:52:18 | DEBUG | http-nio-8200-exec-2 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 68| online user: 1

断开连接,控制台日志:

2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 78| Socket is closing .
2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 79| username: demo1
2020-03-18 16:55:59 | DEBUG | http-nio-8200-exec-3 | com.ljq.demo.springboot.websocket.web.ChatEndPoint 88| online user: 0

以上为单个用户的测试,可以使用多个设备登录测试网站进行 websocket 连接测试

6 参考资料推荐

A Guide to the Java API for WebSocket

Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理

【websocket】spring boot 集成 websocket 的四种方式

WebSocket在线测试工具

在线WebSocket测试工具

7 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值