Spring clound+VUE+WebSocket实现消息推送 一(即时通讯)

需求:

  当用户进行某个操作,需要通知其他用户时,其他用户可以实时接收消息。

  工程为 Spring cloud + VUE 。

技术实现:

  Spring Cloud、Spring boot、vue、webSocket等。

流程

用户登录系统→连接到socket服务进行监听→页面用户操作某业务→调用socket服务传递json消息→socket服务推送消息到其他用户页面→收到消息解析json进行其他业务处理

通讯json

目前推送的有:业务的ID、业务的类型、推送的简约内容

============================新建服务====================================================

一:新建socket服务,聚合到父工程里(不会的去复习下spring boot)

1:配置socket属性文件application.yml,兼容后面文章的离线通讯

#eureka注册中心地址
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://XXX.XXX.XXX.XXX:8060/eureka/
    register-with-eureka: true
    fetch-registry: true
 
server:
  port: 9002
  servlet:
    context-path: /
spring:
  application:
    name: api-webSocket
  datasource:
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@XXX.XXX.XXX:8098:XE
    username: XXX
    password: XXX
    type: com.alibaba.druid.pool.DruidDataSource
    #最大活跃数
    maxActive: 20
    #初始化数量
    initialSize: 5
    #最大连接等待超时时间
    maxWait: 60000
  redis:
    host: XXX.XXX.XXX.XXX
    # 端口
    port: 6379
    # 超时时间
    timeout: 5000
    jedis:
      pool:
        #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 3000
        #连接池最大连接数(使用负值表示没有限制)
        max-active: 200
        #连接池中的最大空闲连接
        max-idle: 20
        #连接池中最小空闲连接
        min-idle: 2
  # MyBatis
mybatis:
  # 搜索指定包别名
  typeAliasesPackage:  com.fencer.rcdd
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml

# 日志配置
logging:
        level:
          com.fencer: debug
          org.springframework: WARN
          org.spring.springboot.dao: debug

2:父工程添加相关依赖pom.xml

 

	
			org.springframework.boot
			spring-boot-starter-websocket
			2.0.4.RELEASE
		

3:新建socket配置类

package com.XXX.io.ws;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
 * 类描述:
 *
 * @author :carry
 * @version: 1.0  CreatedDate in  2019年09月24日
 * 

* 修订历史: 日期 修订者 修订描述 */ @Configuration public class WebSocketConfig { /** * @方法描述: 注入ServerEndpointExporter, * * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint * @return: org.springframework.web.socket.server.standard.ServerEndpointExporter * @Author: carry */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }

 4:新建服务类

package com.fencer.io.ws;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 类描述:
 *
 * @author :carry
 * @version: 1.0  CreatedDate in  2019年09月24日
 * 

* 修订历史: 日期 修订者 修订描述 */ @Component @ServerEndpoint(value = "/websocket/{userId}")//设置访问URL public class WebSocket { //静态变量,用来记录当前在线连接数。 private static int onlineCount = 0; private Session session; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet webSockets = new CopyOnWriteArraySet<>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private static Map sessionPool = new HashMap(); /** * @方法描述: 开启socket * @return: void * @Author: carry */ @OnOpen public void onOpen(Session session, @PathParam(value = "userId") String userId) { this.session = session; webSockets.add(this);//加入set中 addOnlineCount(); //在线数加1 sessionPool.put(userId, session);//把对应用户id的session放到sessionPool中,用于单点信息发送 System.out.println("【websocket消息】 有新连接加入!用户id" + userId + ",当前在线人数为" + getOnlineCount()); } /** * @方法描述: 关闭socket * @return: void * @Author: carry */ @OnClose public void onClose() { webSockets.remove(this); subOnlineCount(); //在线数减1 System.out.println("【websocket消息】 连接断开!当前在线人数为" + getOnlineCount()); } /** * @方法描述: 收到客户端消息 * @return: void * @Author: carry */ @OnMessage public void onMessage(String message) { System.out.println("【websocket消息】收到客户端消息:" + message); } /** * @方法描述: 广播消息全体发送 * @return: void * @Author: carry */ public void sendAllMessage(String message) { for (WebSocket webSocket : webSockets) { System.out.println("【websocket消息】广播消息:" + message); try { webSocket.session.getAsyncRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } /** * @方法描述: 一对一单点消息 * @return: void * @Author: carry */ public void sendOneMessage(String userId, String message) { try { // 防止推送到客户端的信息太多导致弹窗太快 Thread.sleep(500); System.out.println("用户"+userId+"【websocket消息】单点消息:" + message); Session session = sessionPool.get(userId); if (session != null) { // getAsyncRemote是异步发送,加锁防止上一个消息还未发完下一个消息又进入了此方法 // 也就是防止多线程中同一个session多次被调用报错,虽然上面睡了0.5秒,为了保险最好加锁 synchronized (session) { session.getAsyncRemote().sendText(message); } } } catch (Exception e) { e.printStackTrace(); } } /** * @方法描述: 发生错误时调用 * @return: void * @Author: carry */ @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误"); error.printStackTrace(); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocket.onlineCount++; } public static synchronized void subOnlineCount() { WebSocket.onlineCount--; } }

5:对外暴露接口

package com.XX.io.ws;

import com.XX.common.base.AjaxResult;
import com.XX.common.base.BaseController;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JsonConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 类描述:
 *
 * @author :carry
 * @version: 1.0  CreatedDate in  2019年09月24日
 * 

* 修订历史: 日期 修订者 修订描述 */ @RestController @RequestMapping("/websocket") public class SocketController extends BaseController { @Autowired private WebSocket webSocket; /** * @方法描述: 向所有用户发送消息(一人对所有人发布同一个消息) * @return: com.XX.common.base.AjaxResult * @Author: carry */ @PostMapping("/sendAllWebSocket") public AjaxResult sendAllWebSocket(@RequestParam String jsonMsg) { try { webSocket.sendAllMessage(jsonMsg); } catch (Exception e) { return error(e.getMessage()); } return success(); } /** * @方法描述: 一对一发送消息(一人对一人发布同一个消息) * @return: com.XX.common.base.AjaxResult * @Author: carry */ @PostMapping("/sendOneWebSocketOneToOne") public AjaxResult sendOneWebSocketOneToOne(@RequestParam("userId") String userId, @RequestParam String jsonMsg) { try { webSocket.sendOneMessage(userId, jsonMsg); } catch (Exception e) { return error(e.getMessage()); } return success(); } /** * @方法描述: 一对一发送多消息(一人对一人发布多个消息) * 此方法会出现多线程问题,需要在sendOneMessage进行处理 * @return: com.XX.common.base.AjaxResult * @Author: carry */ @PostMapping("/sendManayWebSocketOneToOne") public AjaxResult sendManayWebSocketOneToOne(@RequestParam("userId") String userId, @RequestParam String jsonString) { try { JSONArray jsonArray = JSONArray.fromObject(jsonString); for(int i=0;i userList, @RequestParam String jsonMsg) { try { for (String userId : userList) { webSocket.sendOneMessage(userId, jsonMsg); } } catch (Exception e) { return error(e.getMessage()); } return success(); } }

6:入口类也贴上吧

package XXX.XXX;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.client.RestTemplate;

@EnableEurekaClient
@EnableHystrix
@MapperScan(basePackages = { "com.XX.XX.mapper" })
@ComponentScan(basePackages = "com.XXX.*")
@SpringBootApplication
public class WebSocketApplication implements CommandLineRunner {

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

	@Override
	public void run(String... args) throws Exception {
		System.out.println("######################WebSocket服务启动完成!######################");
	}

	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

============================其他服务的调用====================================================

二:其他服务的调用,使用rest+template调用(不会的可以复习一下spring colud 服务之间的调用)

 1:为了方便服务里面其他功能的方便,封装单独的调用公共类

SocketService.java
package com.xxx.rcdd.io.ws;

import com.xxx.common.base.AjaxResult;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import net.sf.json.JSONArray;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.List;

import static com.fencer.common.base.AjaxResult.error;
import static com.fencer.common.base.AjaxResult.success;

/**
 * 类描述:
 *
 * @author :carry
 * @version: 1.0  CreatedDate in  2019年09月26日
 * 

* 修订历史: 日期 修订者 修订描述 */ @Service public class SocketService { @Autowired RestTemplate restTemplate; /** * @方法描述: 向全部用户广播消息 * @return: void * @Author: carry */ public void sedMsgAll(String jsonMsg) { try { //发送消息 Thread thread = new Thread(new Runnable() { @Override public void run() { SocketService.this.sendAllWebSocket(jsonMsg); } }); thread.start(); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } /** * @方法描述:一对一发送消息(一人对一人发布同一个消息) * @return: void * @Author: carry */ public void sendOneMsgToOneToOne(String userId, String jsonMsg) { try { //发送消息 Thread thread = new Thread(new Runnable() { @Override public void run() { SocketService.this.sendOneWebSocketOneToOne(userId, jsonMsg); } }); thread.start(); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } /** * @方法描述:一对一发送多消息(一人对一人发布多个消息) * @return: void * @Author: carry */ public void sendManayToOneToOne(String userId,JSONArray jsonArray) { try { //发送消息 Thread thread = new Thread(new Runnable() { @Override public void run() { SocketService.this.sendManayWebSocketOneToOne(userId, jsonArray); } }); thread.start(); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } /** * @方法描述:一对多发送消息(一人对多人发布同一个消息) * @return: void * @Author: carry */ public void sendUserList(List userList, String jsonMsg) { try { //发送消息 Thread thread = new Thread(new Runnable() { @Override public void run() { SocketService.this.sendUserListWebSocket(userList, jsonMsg); } }); thread.start(); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } /** * @方法描述: 消息发送失败回调 * @return: com.fencer.rcdd.domain.util.SysUser * @Author: carry */ private AjaxResult fallbackOfMessage() { System.out.println("error fallbackOfMessage.... "); return error(); } /** * @方法描述: 向全部用户广播消息 * @return: com.fencer.rcdd.domain.util.SysUser * @Author: carry */ @HystrixCommand(fallbackMethod = "fallbackOfMessage") private AjaxResult sendAllWebSocket(String jsonMsg) { try { String url = "http://API-WEBSOCKET/websocket/sendAllWebSocket"; MultiValueMap map = new LinkedMultiValueMap(); map.add("jsonMsg", jsonMsg); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> request = new HttpEntity>(map, headers); restTemplate.postForEntity(url, request, AjaxResult.class); } catch (Exception e) { e.printStackTrace(); return error(e.getMessage()); } return success(); } /** * @方法描述:一对一发送消息(一人对一人发布同一个消息) * @return: com.fencer.rcdd.domain.util.SysUser * @Author: carry */ @HystrixCommand(fallbackMethod = "fallbackOfMessage") private AjaxResult sendOneWebSocketOneToOne(String userId, String jsonMsg) { try { String url = "http://API-WEBSOCKET/websocket/sendOneWebSocketOneToOne"; MultiValueMap map = new LinkedMultiValueMap(); map.add("userId", userId); map.add("jsonMsg", jsonMsg); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> request = new HttpEntity>(map, headers); restTemplate.postForEntity(url, request, AjaxResult.class); } catch (Exception e) { e.printStackTrace(); return error(e.getMessage()); } return success(); } /** * @方法描述:一对一发送多消息(一人对一人发布多个消息) * @return: com.fencer.rcdd.domain.util.SysUser * @Author: carry */ @HystrixCommand(fallbackMethod = "fallbackOfMessage") private AjaxResult sendManayWebSocketOneToOne(String userId,JSONArray jsonArray) { try { String jsonString = jsonArray.toString(); String url = "http://API-WEBSOCKET/websocket/sendManayWebSocketOneToOne"; MultiValueMap map = new LinkedMultiValueMap(); map.add("userId", userId); map.add("jsonString", jsonString); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> request = new HttpEntity>(map, headers); restTemplate.postForEntity(url, request, AjaxResult.class); } catch (Exception e) { e.printStackTrace(); return error(e.getMessage()); } return success(); } /** * @方法描述:一对多发送消息(一人对多人发布同一个消息) * @return: com.fencer.rcdd.domain.util.SysUser * @Author: carry */ @HystrixCommand(fallbackMethod = "fallbackOfMessage") private AjaxResult sendUserListWebSocket(List userList, String jsonMsg) { try { String url = "http://API-WEBSOCKET/websocket/sendUserListWebSocket"; MultiValueMap map = new LinkedMultiValueMap(); map.add("userList", userList); map.add("jsonMsg", jsonMsg); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> request = new HttpEntity>(map, headers); restTemplate.postForEntity(url, request, AjaxResult.class); } catch (Exception e) { e.printStackTrace(); return error(e.getMessage()); } return success(); } }

2:具体业务的调用

省略业务代码
            List> infoList = new ArrayList>();
            Map msgMap = new HashMap();
            msgMap.put("id", uuid);
            msgMap.put("type", "shift");
            msgMap.put("title", "通知内容");
            infoList.add(msgMap);
            infoList.add(msgMap);
            JSONArray jsonArray =JSONArray.fromObject(infoList);
            JSONObject  jsonObject=JSONObject.fromObject(msgMap);

            //发送消息
             socketService.sedMsgAll(jsonObject.toString());
            //socketService.sendOneMsgToOneToOne("3001",jsonObject.toString());
            // socketService.sendManayToOneToOne("3001", jsonArray);

=================================前端VUE部分====================================================

因为推送可能是在不同的页面,本人是在不变的页面比如导航组件里链接的socket服务进行监听,然后调用element的notic组件进行前台展示,前台可以根据消息的类型或者id,进行自己的处理,比如进入到业务的具体页面之类。

 mounted() {
    //链接socket
    this.connWebSocket();
  },
  beforeDestroy() {
    // 监听窗口关闭事件,vue生命周期销毁之前关闭socket当窗口关闭时,防止连接还没断开就关闭窗口。
    this.onbeforeunload();
  },
  methods: {
    connWebSocket() {
      let userInfo = JSON.parse(localStorage.getItem("userInfos"));
      let userId = userInfo.userId;
      // WebSocket
      if ("WebSocket" in window) {
        this.websocket = new WebSocket(
          "ws://localhost:9002/websocket/" + userId //userId 传此id主要后端java用来保存session信息,用于给特定的人发送消息,广播类消息可以不用此参数
        );

        //初始化socket
        this.initWebSocket();
      } else {
        ctx.$message.error("次浏览器不支持websocket");
      }
    },
    initWebSocket() {
      // 连接错误
      this.websocket.onerror = this.setErrorMessage;

      // 连接成功
      this.websocket.onopen = this.setOnopenMessage;

      // 收到消息的回调
      this.websocket.onmessage = this.setOnmessageMessage;

      // 连接关闭的回调
      this.websocket.onclose = this.setOncloseMessage;

      // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
      window.onbeforeunload = this.onbeforeunload;
    },
    setErrorMessage() {
      console.log(
        "WebSocket连接发生错误   状态码:" + this.websocket.readyState
      );
    },
    setOnopenMessage() {
      console.log("WebSocket连接成功    状态码:" + this.websocket.readyState);
    },
   setOnmessageMessage(result) {
      console.log("服务端返回:" + result.data);
      let msgMap = JSON.parse(result.data);
      let id = msgMap.id;
      let title = msgMap.title;
      let type = msgMap.type;
      // 根据服务器推送的消息做自己的业务处理

      this.$notify({
        title: "你有一条新信息",
        type: "info",
        duration: 0,
        dangerouslyUseHTMLString: true,
        message:
          '
' + title, position: "bottom-right" }); }, setOncloseMessage() { console.log("WebSocket连接关闭 状态码:" + this.websocket.readyState); }, onbeforeunload() { this.closeWebSocket(); }, closeWebSocket() { this.websocket.close(); } },

基本上面的代码复制到你们自己的工程里就能运行,有些涉及数据库、地址、或者公司隐私之类的,大部分用XXX去掉了,换成你们自己的即可。

 

======================================正式环境配合nginx使用=======================================

因为服务器对外只开通了8077端口,所以只能通过Nginx来反向代理

1:修改前台文件

connWebSocket() {
      let userInfo = JSON.parse(localStorage.getItem("userInfos"));
      let userId = userInfo.userId;
      if ("WebSocket" in window) {
        this.websocket = new WebSocket(
          "ws://xxx.xxx.xxx.xxx:8077/websocket/" + userId
        );

        this.initWebSocket();
      } else {
        ctx.$message.error("次浏览器不支持websocket");
      }
    },

 

2:修改Nginx配置文件

主要的2点

 1: map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
   }

2: websocket 服务的端口是9002,需要代理到此端口

location /websocket {
        proxy_pass http://127.0.0.1:9002;  
        proxy_redirect off; 
        proxy_set_header Host $host; 
        proxy_set_header X-Real-IP $remote_addr; 
        proxy_read_timeout 3600s; #超时时间
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header Upgrade $http_upgrade; #开启websocket支持
        proxy_set_header Connection $connection_upgrade; #开启websocket支持
    }

 

完整代码:


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  65555;
}


http {
   map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
   }

    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    autoindex       on;
    tcp_nopush      on;
    tcp_nodelay     on; 
    keepalive_timeout 120; 
    
    fastcgi_connect_timeout 300;
    fastcgi_send_timeout 300;
    fastcgi_read_timeout 300;
    fastcgi_buffer_size 64k;
    fastcgi_buffers 4 64k;    
    fastcgi_busy_buffers_size 128k;
    fastcgi_temp_file_write_size 128k;

    client_header_buffer_size 100m; #上传文件大小限制    

    gzip on;
    gzip_min_length   1k;
    gzip_buffers     4 16k;
    gzip_http_version 1.0;
    gzip_comp_level 9;
    gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/javascript image/jpeg image/gif image/png;
    #gzip_types       text/plain application/x-javascript text/css application/xml;
    gzip_vary on;
    server {
        listen       8077;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;


        location / {
            root   D:/dist;  
            index  index.html index.htm;
        }

	location /iserver {
            proxy_pass http://127.0.0.1:8090/iserver;
        }


	location ^~/api {
            proxy_pass http://127.0.0.1:8088;
        }

        location /websocket {
		proxy_pass http://127.0.0.1:9002;  
		proxy_redirect off; 
		proxy_set_header Host $host; 
		proxy_set_header X-Real-IP $remote_addr; 
		proxy_read_timeout 3600s; 
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
		proxy_set_header Upgrade $http_upgrade; 
		proxy_set_header Connection $connection_upgrade; 
        }
	
        error_page  404              /index.html;


	location /index.html {
            root   D:/dist; 
            index  index.html index.htm;
        }
 

}

==============================添加 消息提示音====================================================

1:vue页面修改,添加代码


			

2:添加一个调用的方法

methods: {
    myplay(){
         this.$refs.audio.play();
  	},
}

3:推送完消息后调用

setOnmessageMessage(result) {
      this.myplay();//调用播放方法

      console.log("服务端返回:" + result.data);
      let msgMap = JSON.parse(result.data);
      let id = msgMap.id;
      let business_Id = msgMap.business_Id;
      let title = msgMap.title;
      let type = msgMap.type; //主要
.........省略其他代码
}

 

你可能感兴趣的:(Spring,cloud,Vue,java)