SpringBoot整合WebSocket,实现即时通讯


前言

Hi,大家好,我是希留。
在项目的开发工程中,可能会遇到实时性比较高的场景需求,例如说,聊天 IM 即使通讯功能、消息订阅服务、在线客服等等。那遇到这种功能的时候应该怎么去做呢?通常是使用WebSocket去实现。
那么,本篇文章就带大家来了解一下是什么是WebSocket,以及使用SpringBoot搭建一个简易的聊天室功能。如果对你有帮助的话,还不忘点赞支持一下,感谢~
源码地址:
https://github.com/277769738/java-sjzl-demo/tree/master/springboot-websocket
https://gitee.com/huoqstudy/java-sjzl-demo/tree/master/springboot-websocket


提示:以下是本篇文章正文内容,下面案例可供参考

一、什么是WebSocket?

WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。它是一种在单个TCP连接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为标准。
全双工和单工的区别?

  • 全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时(瞬时)进行信号的双向传输(A→B且B→A)。指A→B的同时B→A,是瞬时同步的。
  • 单工、半双工(Half Duplex),所谓半双工就是指一个时间段内只有一个动作发生,举个简单例子,一条窄窄的马路,同时只能有一辆车通过,当目前有两辆车对开,这种情况下就只能一辆先过,等到头儿后另一辆再开,这个例子就形象的说明了半双工的原理。早期的对讲机、以及早期集线器等设备都是基于半双工的产品。随着技术的不断进步,半双工会逐渐退出历史舞台。

二、Http与WebSocket的区别

1.Http

http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接。在这里插入图片描述

2.WebSocket

WebSocket协议是一种长链接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通讯。
在这里插入图片描述

三、代码实现

1.添加依赖

<!--websocket依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 引入 Fastjson ,实现对 JSON 的序列化,因为后续我们会使用它解析消息 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>

2.消息

因为 WebSocket 协议,不像 HTTP 协议有 URI 可以区分不同的 API 请求操作,所以我们需要在 WebSocket 的 Message 里,增加能够标识消息类型,这里我们采用 type 字段。所以在这个示例中,我们采用的 Message 采用 JSON 格式编码,格式如下:

{
    type : "", //消息类型
    boby: {} //消息体}

type 字段,消息类型。通过该字段,我们知道使用哪个 MessageHandler 消息处理器。关于 MessageHandler ,我们在 「2.6 消息处理器」中,详细解析。
body 字段,消息体。不同的消息类型,会有不同的消息体。

2.1 Message

创建 Message 接口,基础消息体,所有消息体都要实现该接口。目前作为一个标记接口,未定义任何操作。

public interface Message {
}

2.2 认证相关 Message

创建 AuthRequest 类,用户认证请求。TYPE 静态属性,消息类型为 AUTH_REQUEST 。
accessToken 属性,认证 Token 。在 WebSocket 协议中,我们也需要认证当前连接,用户身份是什么。一般情况下,我们采用用户调用 HTTP 登录接口,登录成功后返回的访问令牌 accessToken 。
代码如下:

public class AuthRequest implements Message{
    public static final String TYPE = "AUTH_REQUEST";

    /**
     * 认证 Token
     */
    private String accessToken;

    public String getAccessToken() {
        return accessToken;
    }
    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }
}

2.3 创建AuthResponse 类

WebSocket 协议是基于 Message 模型,进行交互。但是,这并不意味着它的操作,不需要响应结果。例如说,用户认证请求,是需要用户认证响应的。所以,我们创建 AuthResponse 类,作为用户认证响应。代码如下:

public class AuthResponse implements Message {

    public static final String TYPE = "AUTH_RESPONSE";

    /**
     * 响应状态码
     */
    private Integer code;
    /**
     * 响应提示
     */
    private String message;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

2.4 发送消息相关 Message

创建 SendToOneRequest 类,发送给指定人的私聊消息的 Message。代码如下:

public class SendToOneRequest implements Message {

    public static final String TYPE = "SEND_TO_ONE_REQUEST";

    /**
     * 发送给的用户
     */
    private String toUser;

    /**
     * 消息编号
     */
    private String msgId;

    /**
     * 发送的内容
     */
    private String content;


    public String getToUser() {
        return toUser;
    }

    public void setToUser(String toUser) {
        this.toUser = toUser;
    }

    public String getMsgId() {
        return msgId;
    }

    public void setMsgId(String msgId) {
        this.msgId = msgId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

在服务端接收到发送消息的请求,需要异步响应发送是否成功。所以,创建 SendResponse 类,发送消息响应结果的 Message 。代码如下:

public class SendResponse implements Message{

    public static final String TYPE = "SEND_RESPONSE";

    /**
     * 消息编号
     */
    private String msgId;
    /**
     * 响应状态码
     */
    private Integer code;
    /**
     * 响应提示
     */
    private String message;

}

在服务端接收到发送消息的请求,需要转发消息给对应的人。所以,创建 SendToUserRequest 类,发送消息给一个用户的 Message 。代码如下:

public class SendToUserRequest implements Message{

    public static final String TYPE = "SEND_TO_USER_REQUEST";

    /**
     * 消息编号
     */
    private String msgId;
    /**
     * 内容
     */
    private String content;

    public String getMsgId() {
        return msgId;
    }

    public void setMsgId(String msgId) {
        this.msgId = msgId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

2.5 消息处理器

每个客户端发起的 Message 消息类型,我们会声明对应的 MessageHandler 消息处理器。这个就类似在 SpringMVC 中,每个 API 接口对应一个 Controller 的 Method 方法。

2.5.1 MessageHandler

创建 MessageHandler 接口,消息处理器接口。定义了泛型 ,需要是 Message 的实现类。定义的两个接口方法。代码如下:

public interface MessageHandler<T extends Message> {

    /**
     * 执行处理消息
     * @param session 会话
     * @param message 消息
     */
    void execute(WebSocketSession session, T message);

    /**
     * 消息类型,即每个 Message 实现类上的 TYPE 静态字段
     * @return
     */
    String getType();
}
2.5.2 AuthMessageHandler

创建 AuthMessageHandler 类,处理 AuthRequest 消息。代码如下:

@Component
public class AuthMessageHandler implements MessageHandler<AuthRequest>{

    @Override
    public void execute(WebSocketSession session, AuthRequest message) {
        // 如果未传递 accessToken
        if (StringUtils.isEmpty(message.getAccessToken())) {
            AuthResponse authResponse = new AuthResponse();
            authResponse.setCode(1);
            authResponse.setMessage("认证 accessToken 未传入");
            WebSocketUtil.send(session, AuthResponse.TYPE,authResponse);
            return;
        }

        // 添加到 WebSocketUtil 中,考虑到代码简化,我们先直接使用 accessToken 作为 User
        WebSocketUtil.addSession(session, message.getAccessToken());

        // 判断是否认证成功。这里,假装直接成功
        AuthResponse authResponse = new AuthResponse();
        authResponse.setCode(0);
        WebSocketUtil.send(session, AuthResponse.TYPE, authResponse);
    }
    @Override
    public String getType() {
        return AuthRequest.TYPE;
    }
}
2.5.3 SendToOneRequest

创建 SendToOneHandler 类,处理 SendToOneRequest 消息。代码如下:

@Component
public class SendToOneHandler implements MessageHandler<SendToOneRequest>{

    @Override
    public void execute(WebSocketSession session, SendToOneRequest message) {
        // 这里,假装直接成功
        SendResponse sendResponse = new SendResponse();
        sendResponse.setMsgId(message.getMsgId());
        sendResponse.setCode(0);
        WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);

        // 创建转发的消息
        SendToUserRequest sendToUserRequest = new SendToUserRequest();
        sendToUserRequest.setMsgId(message.getMsgId());
        sendToUserRequest.setContent(message.getContent());

        // 广播发送
        WebSocketUtil.send(message.getToUser(), SendToUserRequest.TYPE, sendToUserRequest);
    }

    @Override
    public String getType() {
        return SendToOneRequest.TYPE;
    }
}
2.5.4 SendToAllHandler

创建 SendToAllHandler 类,处理 SendToAllRequest 消息。代码如下:

@Component
public class SendToAllHandler implements MessageHandler<SendToAllRequest> {

    @Override
    public void execute(WebSocketSession session, SendToAllRequest message) {
        // 这里,假装直接成功
        SendResponse sendResponse = new SendResponse();
        sendResponse.setMsgId(message.getMsgId());
        sendResponse.setCode(0);
        WebSocketUtil.send(session, SendResponse.TYPE, sendResponse);

        // 创建转发的消息
        SendToUserRequest sendToUserRequest = new SendToUserRequest();
        sendToUserRequest.setMsgId(message.getMsgId());
        sendToUserRequest.setContent(message.getContent());

        // 广播发送
        WebSocketUtil.broadcast(SendToUserRequest.TYPE, sendToUserRequest);
    }

    @Override
    public String getType() {
        return SendToAllRequest.TYPE;
    }
}

2.6 WebSocketUtil

创建 WebSocketUtil 工具类,代码如下,主要提供两方面的功能:

  • Session 会话的管理
  • 多种发送消息的方式
public class WebSocketUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);

    /**
     * Session 与用户的映射
     */
    private static final Map<WebSocketSession, String> SESSION_USER_MAP = new ConcurrentHashMap<>();
    /**
     * 用户与 Session 的映射
     */
    private static final Map<String, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 添加 Session 。在这个方法中,会添加用户和 Session 之间的映射
     * @param session Session
     * @param user 用户
     */
    public static void addSession(WebSocketSession session, String user) {
        // 更新 USER_SESSION_MAP
        USER_SESSION_MAP.put(user, session);
        // 更新 SESSION_USER_MAP
        SESSION_USER_MAP.put(session, user);
    }


    /**
     * 发送消息给单个用户的 Session
     * @param session Session
     * @param type 消息类型
     * @param message 消息体
     * @param <T> 消息类型
     */
    public static <T extends Message> void send(WebSocketSession  session, String type, T message) {
        // 创建消息
        TextMessage  messageText = buildTextMessage(type, message);
        // 遍历给单个 Session ,进行逐个发送
        sendTextMessage(session, messageText);
    }
    /**
     * 广播发送消息给所有在线用户
     * @param type 消息类型
     * @param message 消息体
     * @param <T> 消息类型
     */
    public static <T extends Message> void broadcast(String type, T message) {
        // 创建消息
        TextMessage messageText = buildTextMessage(type, message);
        // 遍历 SESSION_USER_MAP ,进行逐个发送
        for (WebSocketSession session : SESSION_USER_MAP.keySet()) {
            sendTextMessage(session, messageText);
        }
    }

    /**
     * 发送消息给指定用户
     * @param user 指定用户
     * @param type 消息类型
     * @param message 消息体
     * @param <T> 消息类型
     * @return 发送是否成功
     */
    public static <T extends Message> boolean send(String user, String type, T message) {
        // 获得用户对应的 Session
        WebSocketSession session = USER_SESSION_MAP.get(user);
        if (session == null) {
            LOGGER.error("[send][user({}) 不存在对应的 session]", user);
            return false;
        }
        // 发送消息
        send(session, type, message);
        return true;
    }

    /**
     * 构建完整的消息
     * @param type 消息类型
     * @param message 消息体
     * @param <T> 消息类型
     * @return 消息
     */
    private static <T extends Message> TextMessage  buildTextMessage(String type, T message) {
        JSONObject messageObject = new JSONObject();
        messageObject.put("type", type);
        messageObject.put("body", message);
        return new TextMessage(messageObject.toString());
    }

    /**
     * 真正发送消息
     *
     * @param session Session
     * @param textMessage 消息
     */
    private static void sendTextMessage(WebSocketSession  session, TextMessage textMessage) {
        if (session == null) {
            LOGGER.error("[sendTextMessage][session 为 null]");
            return;
        }
        try {
            session.sendMessage(textMessage);
        } catch (IOException e) {
            LOGGER.error("[sendTextMessage][session({}) 发送消息{}) 发生异常",
                    session, textMessage, e);
        }
    }

}

3.编写处理类MyHandler

处理类,在Spring中,处理消息的具体业务逻辑,进行开启、关闭连接等操作。

public class MyHandler extends TextWebSocketHandler implements InitializingBean {
    private Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 消息类型与 MessageHandler 的映射
     * 无需设置成静态变量
     */
    private final Map<String, MessageHandler> HANDLERS = new HashMap<>();
    @Autowired
    private ApplicationContext applicationContext;


    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        System.out.println("获取到消息 >> " + message.getPayload());
        logger.info("[handleMessage][session({}) 接收到一条消息({})]", session, message);
        // 获得消息类型
        JSONObject jsonMessage = JSON.parseObject(message.getPayload());
        String messageType = jsonMessage.getString("type");
        // 获得消息处理器
        MessageHandler messageHandler = HANDLERS.get(messageType);
        if (messageHandler == null) {
            logger.error("[onMessage][消息类型({}) 不存在消息处理器]", messageType);
            return;
        }
        // 解析消息
        Class<? extends Message> messageClass = this.getMessageClass(messageHandler);
        // 处理消息
        Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
        messageHandler.execute(session, messageObj);
    }

    /**
     * 连接建立时触发
     **/
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.info("[afterConnectionEstablished][session({}) 接入]", session);
        // 解析 accessToken
        String accessToken = (String) session.getAttributes().get("accessToken");
        // 创建 AuthRequest 消息类型
        AuthRequest authRequest = new AuthRequest();
        authRequest.setAccessToken(accessToken);
        // 获得消息处理器
        MessageHandler<AuthRequest> messageHandler = HANDLERS.get(AuthRequest.TYPE);
        if (messageHandler == null) {
            logger.error("[onOpen][认证消息类型,不存在消息处理器]");
            return;
        }
        messageHandler.execute(session, authRequest);
    }

    /**
     * 关闭连接时触发
     **/
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("断开连接!");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 通过 ApplicationContext 获得所有 MessageHandler Bean
        applicationContext.getBeansOfType(MessageHandler.class).values()
                // 添加到 handlers 中
                .forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler));
        logger.info("[afterPropertiesSet][消息处理器数量:{}]", HANDLERS.size());
    }

    private Class<? extends Message> getMessageClass(MessageHandler handler) {
        // 获得 Bean 对应的 Class 类名。因为有可能被 AOP 代理过。
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(handler);
        // 获得接口的 Type 数组
        Type[] interfaces = targetClass.getGenericInterfaces();
        Class<?> superclass = targetClass.getSuperclass();
        // 此处,是以父类的接口为准
        while ((Objects.isNull(interfaces) || 0 == interfaces.length) && Objects.nonNull(superclass)) {
            interfaces = superclass.getGenericInterfaces();
            superclass = targetClass.getSuperclass();
        }
        if (Objects.nonNull(interfaces)) {
            // 遍历 interfaces 数组
            for (Type type : interfaces) {
                // 要求 type 是泛型参数
                if (type instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) type;
                    // 要求是 MessageHandler 接口
                    if (Objects.equals(parameterizedType.getRawType(), MessageHandler.class)) {
                        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
                        // 取首个元素
                        if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) {
                            return (Class<Message>) actualTypeArguments[0];
                        } else {
                            throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
                        }
                    }
                }
            }
        }
        throw new IllegalStateException(String.format("类型(%s) 获得不到消息类型", handler));
    }
}

4.创建拦截器MyHandshakeInterceptor

在Spring中提供了websocket拦截器,可以在建立连接之前写些业务逻辑,比如校验登录等。

public class MyHandshakeInterceptor extends HttpSessionHandshakeInterceptor {

    /**
    * @Description 握手之前,若返回false,则不建立链接
    * @Date 21:59 2021/5/16
    * @return boolean
    **/
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, 
								    ServerHttpResponse serverHttpResponse, 
								    WebSocketHandler webSocketHandler, 
								    Map<String, Object> attributes) throws Exception {
        //获得 accessToken ,将用户id放入socket处理器的会话(WebSocketSession)中
        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) serverHttpRequest;
            attributes.put("accessToken", serverRequest.getServletRequest().getParameter("accessToken"));
        }
        // 调用父方法,继续执行逻辑
        return super.beforeHandshake(serverHttpRequest, serverHttpResponse, webSocketHandler, attributes);
    }

5.创建配置类

开启spring websocket功能。

@Configuration
@EnableWebSocket //开启spring websocket功能
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //配置处理器
        registry.addHandler(this.myHandler(), "/")
                //配置拦截器
                .addInterceptors(new MyHandshakeInterceptor())
                .setAllowedOrigins("*");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

    @Bean
    public MyHandshakeInterceptor webSocketShakeInterceptor() {
        return new MyHandshakeInterceptor();
    }
}

6.创建启动类

创建SpringBoot启动类

@SpringBootApplication
public class MyWebsocketApplication {

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

7.实现效果

打开三个浏览器,输入在线测试websocket地址
创建三个连接。分别设置服务地址如下:
ws://localhost:8080/?accessToken=1001
ws://localhost:8080/?accessToken=1002
ws://localhost:8080/?accessToken=1003
发送单人消息

{

   tpye: "SEND_TO_ONE_REQUEST",
   boby: {
                toUser: "1002",
                msgId: "qwwerqrsfd123",
                centent: "这是1001发送给1002的单聊消息"
    }
}

可以看到1002收到了1001发的单聊信息,1003未收到。效果图如下:
在这里插入图片描述
发送多人消息

{

   tpye: "SEND_TO_ALL_REQUEST",
   boby: {
                msgId: "qwerqcfwwerqrsfd123",
                centent: "我是一条群聊消息"
    }
}

可以看到1001,1002,1003都收到了消息,效果图如下:
在这里插入图片描述


总结

好了,以上就是今天要讲的内容,本文介绍了WebSocket协议以及使用它简单的实现及时聊天的场景。
喜欢的朋友,欢迎点赞支持一下。
感谢大家的阅读,如果有什么疑问或者建议,给我留言或者加我个人微信:xiliudd,做个朋友圈点赞之交
喜欢的朋友也可以扫码关注我,更多精彩内容等你~

在这里插入图片描述

  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java升级之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值