公司系统每次更新代码并部署会影响到业务人员使用系统,甚至会造成数据丢失,因此想在发版本前在页面上通知到每个使用者,便着手做个推送消息的功能。
springboot+vue+nginx+springsecurity
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-websocketartifactId>
<version>5.3.18version>
dependency>
// 注意大部分网上的文章都是使用的以下依赖,我使用的话会启动报错,就改为上面的依赖了
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
.authorizeRequests()
.antMatchers("/websocket/*").permitAll()
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocket {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 用户ID
*/
private Integer userId;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
// 用来存在线连接用户信息
private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<Integer, Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value="userId") Integer userId) {
try {
this.session = session;
this.userId = userId;
if (userId != 0 && !sessionPool.containsKey(userId)){
webSockets.add(this);
sessionPool.put(userId, session);
}
log.info("【websocket消息】有新的连接,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(@PathParam(value="userId") Integer userId,String message) {
log.info("【websocket消息】收到客户端消息:"+message);
}
/** 发送错误时的处理
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误,原因:"+error.getMessage());
error.printStackTrace();
}
// 此为广播消息
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:"+message);
for(WebSocket webSocket : webSockets) {
try {
if(webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息
public void sendOneMessage(Integer userId, String message) {
Session session = sessionPool.get(userId);
if (session != null&&session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:"+message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息(多人)
public void sendMoreMessage(Integer[] userIds, String message) {
for(Integer userId:userIds) {
Session session = sessionPool.get(userId);
if (session != null&&session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:"+message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
接下来你可以单独根据业务定义一个接口接收前端的要传的消息,目标接收人等,然后调用websocket去发消息。为什么要单独定义一个接口不直接使用websocket通讯,因为可能有些逻辑要处理,这样更方便。
@RestController
@RequestMapping("/msg")
public class sendMsgController {
@Resource
private MsgService msgService;
@RequestMapping(value = "/sendWsMsg", method = RequestMethod.POST)
@ApiOperation("sendWsMsg")
@PreAuthenticated
public CommonResp<Void> sendWsMsg(@RequestBody SendWsMsgParam sendWsMsgParam){
msgService.sendWsMsg(sendWsMsgParam);
return new CommonResp<>();
}
}
@Data
@ApiModel("ws消息内容")
public class SendWsMsgParam {
/**
* 消息类型
*/
@ApiModelProperty(value = "消息类型", example = "1:全局消息 2:专人消息", required = true)
private Integer type;
/**
* 接收人id
*/
@ApiModelProperty(value = "接收人", example = "[12,13]")
private List<Integer> receivePerson = new ArrayList<>();
/**
* 内容
*/
@ApiModelProperty(value = "内容", example = "警告")
private String content;
}
@Service
@Slf4j
public class LoanFlowService {
@Resource
private WebSocket webSocket;
public void sendWsMsg(SendWsMsgParam sendWsMsgParam) {
if (sendWsMsgParam.getType() == 1){
webSocket.sendAllMessage("【" + SecurityFrameworkUtils.getLoginUser().getNickname() + "】:" + sendWsMsgParam.getContent());
}else {
for (int i = 0; i < sendWsMsgParam.getReceivePerson().size(); i++) {
webSocket.sendOneMessage(sendWsMsgParam.getReceivePerson().get(i),"【" + SecurityFrameworkUtils.getLoginUser().getNickname() + "】:" + sendWsMsgParam.getContent());
}
}
}
}
我这里定义了一个悬浮按钮组件,点击按钮弹出编辑框,可以编辑发送类型,接收人,内容。
因为一般情况下,前端访问的地址会根据nginx转发到真正的服务地址,因此在nginx中必须要配置转发websocket的逻辑。比如假设我这里本来访问的是ws://123.1.0.25:9071/,要转发到http://127.0.0.1:8080(当然如果是没有nginx的情况下,就直接使用原本服务的地址+原本服务的端口号即可,比如本机的时候,就应该是ws://localhost:8080/websocket/this.userId,注意这个8080是本身的服务端口)
location ~/websocket/ {
proxy_pass http://127.0.0.1:8080;//你自己的转发目标地址
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 36000s; #10小时未传输数据则关闭连接
}
然后点击按钮发送消息就可以了,屏幕边上就会弹出消息框。