需求:
当用户进行某个操作,需要通知其他用户时,其他用户可以实时接收消息。
工程为 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
=================================前端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; //主要
.........省略其他代码
}