使用Spring构建实时聊天通知的页面应用

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

网页端实时聊天,单纯使用HTTP也不是不能做,但是确实相对来说不是一个理想的方案,这里我们使用spring-boot-starter-websocket进行相关实现,更多功能可以查阅spring-websocket,以及Using WebSocket to build an interactive web application

客户端我们还是以Vue作为示例,其他前端框架类似

服务端同本站其他文章一样,基于Java17+Spring3+Cloud4,其他版本在部分代码中可能会稍有区别

实现

服务端

引入包

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

WebSocket相关配置

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		//客户端发送消息地址前缀
		config.setApplicationDestinationPrefixes("/socket");
		//服务端发送消息过滤前缀
		config.enableSimpleBroker("/message", "/notify", "/user");
	}

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		//建立连接地址
		registry.addEndpoint("/chat-websocket")
				.setAllowedOrigins("http://localhost:8080")
				.withSockJS();
	}

}

本地测试,这里我们需要增加.setAllowedOrigins("http://localhost:8080")来解决跨域的问题

本质上就是由客户端首先通过三次握手向服务端建立连接,由addEndpoint定义连接地址。然后可客户端发送消息,使用setApplicationDestinationPrefixes设置地址消息地址前缀,功能类似于@RequestMapping::value,使用enableSimpleBroker过滤服务端发送消息的前缀,当然还有setUserDestinationPrefix等其他配置,小伙伴们自行参考源码说明选用

以常用的两种实现举例,首先是广播通知指定团队的用户。当用户刚登录时,正常HTTP请求服务端,获取未读的通知,这个走正常流程即可。当用户登录后,客户端通过订阅之前HTTP请求得到的用户归属团队,订阅一个/多个地址,若后台需要发送广播时,调用服务端发送接口,定义发送的数个团队,则订阅该团队的用户即可收到消息,服务端简化代码如下:

@RestController
@RequestMapping("/user/message")
public class UserMessageController {

    @Resource
    public SimpMessageSendingOperations messagingTemplate;
    
    @PostMapping("/notify")
    public ResultObj<Object> notify(@RequestBody UserMessageNotifyParam param) {
        for (String teamId : param.getTeamId()) {
            messagingTemplate.convertAndSend(String.format("/notify/%s/receive", teamId), param.getMessage());
        }
        //todo save db
        return ResultObj.success();
    }
    
}

此时注意,message的对象内容需要携带messageId作为唯一键,用于解决用户订阅多个归属团队时,向其中多个发送同一条消息导致消息重复获取的问题

另一个常用的实现就是用户之间的消息发送,首先用户刚登陆时,和上例一样单纯走HTTP获取未读消息,当用户登录后,通过自己的Id订阅发送给自己的消息。作为另一个用户,给其发送消息时,有两种解决方案,一种是通过HTTP发送,这种发送和上例差不多,只不过是需要修改发送地址以及改为和用户相关而不是team相关,再加上相关的验证和鉴权操作,这里不再赘述,另一种解决方案是通过已经和服务端建立的TCP通道发送消息,简化代码如下:

@RestController
@RequestMapping("/user/message")
public class UserMessageController {

    @Resource
    public SimpMessageSendingOperations messagingTemplate;
    
    @MessageMapping("/message/send")
    public void userMsg(
            @Payload String msg,
            @Header("simpSessionId") String sessionId,
            @Header("token") String token
    ) {
        System.out.printf("[op:userMsg] Message is %s%n", msg);
        System.out.printf("[op:userMsg] Session id is %s%n", sessionId);
        System.out.printf("[op:userMsg] Token is %s%n", token);
        //check auth
        String sendUserId = tokenToUserId(token);
        System.out.printf("[op:userMsg] User id is %s%n", sendUserId);
        //get data
        JSONObject msgObj = JSON.parseObject(msg);
        String receiveUserId = msgObj.getString("userId");
        String sendMsg = msgObj.getString("sendMsg");
        //send
        messagingTemplate.convertAndSendToUser(receiveUserId, "/message/receive", sendMsg);
        //todo save db
    }
    
}

至此,实时聊天的基本服务端就构建完成了,我们这里只提供了简化的代码,小伙伴在使用在生产时,肯定需要对其安全性和鲁棒性进行增强,比如网关过滤,可以参考websocket-spring-gateway-demo,以及使用消息队列减轻服务器压力等等

客户端

我们这里使用sockjs-clientwebstomp-client的包进行socket连接配置

{
    //..
    "dependencies": {
        "sockjs-client": "^1.6.1",
        "webstomp-client": "^1.2.6"
        //..
    }
    //..
}

基本示例代码如下

<template>
  <div>
    <div id="main-content" class="container">
      <div class="row">
        <div class="col-md-6">
          <form class="form-inline">
            <div class="form-group">
              <label for="connect">WebSocket connection:</label>
              <button id="connect" class="btn btn-default" type="submit" :disabled="connected === true"
                      @click.prevent="connect">Connect
              </button>
              <button id="disconnect" class="btn btn-default" type="submit" :disabled="connected === false"
                      @click.prevent="disconnect">Disconnect
              </button>
            </div>
          </form>
        </div>
        <div class="col-md-6">
          <form class="form-inline">
            <div class="form-group">
              <label for="name">What is your name?</label>
              <input type="text" id="name" class="form-control" v-model="send_message" placeholder="Your name here...">
            </div>
            <button id="send" class="btn btn-default" type="submit" @click.prevent="send">Send</button>
          </form>
        </div>
      </div>
      <div class="row">
        <div class="col-md-12">
          <table id="conversation" class="table table-striped">
            <thead>
            <tr>
              <th>Greetings</th>
            </tr>
            </thead>
            <tbody>
            <tr v-for="item in received_messages" :key="item">
              <td>{{ item }}</td>
            </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import SockJS from "sockjs-client";
import Stomp from "webstomp-client";

export default {
  name: "adminTest",
  data() {
    return {
      received_messages: [],
      send_message: null,
      connected: false
    };
  },
  methods: {
    send() {
      console.log("Send message:" + this.send_message);
      if (this.stompClient && this.stompClient.connected) {
        const msg = {userId: "666", sendMsg: this.send_message};
        this.stompClient.send("/socket/message/send", JSON.stringify(msg), {token: "user_token"});
      }
    },
    connect() {
      this.socket = new SockJS("http://localhost:9527/chat-websocket");
      this.stompClient = Stomp.over(this.socket);
      this.stompClient.connect(
          {},
          frame => {
            this.connected = true;
            console.log(frame);
            this.stompClient.subscribe("/user/777/message/receive", tick => {
              console.log(tick);
              this.received_messages.push(tick.body);
            });
            this.stompClient.subscribe("/notify/123/receive", tick => {
              console.log(tick);
              this.received_messages.push(tick.body);
            });
            this.stompClient.subscribe("/notify/456/receive", tick => {
              console.log(tick);
              this.received_messages.push(tick.body);
            });
            this.stompClient.subscribe("/notify/111/receive", tick => {
              console.log(tick);
              this.received_messages.push(tick.body);
            });
          },
          error => {
            console.log(error);
            this.connected = false;
          }
      );
    },
    disconnect() {
      if (this.stompClient) {
        this.stompClient.disconnect();
      }
      this.connected = false;
    },
    tickleConnection() {
      this.connected ? this.disconnect() : this.connect();
    }
  },
};
</script>

<style scoped>

</style>

以上代码,基于该用户为666,其订阅了123456111团队,以及在尝试向用户777发送消息的简化示例

原文链接

欢迎大家对于本站的访问 - AsterCasc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值