学习记录680@springboot+vue+nginx+springsecurity环境下的websocket实战

起因

公司系统每次更新代码并部署会影响到业务人员使用系统,甚至会造成数据丢失,因此想在发版本前在页面上通知到每个使用者,便着手做个推送消息的功能。

环境

springboot+vue+nginx+springsecurity

springboot相关代码

pom

<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-websocketartifactId>
    <version>5.3.18version>
dependency>

// 注意大部分网上的文章都是使用的以下依赖,我使用的话会启动报错,就改为上面的依赖了
<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-websocketartifactId>
dependency>

springsecurity放开websocket限制

.authorizeRequests()
    .antMatchers("/websocket/*").permitAll()

WebSocketConfig 配置类

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();
    }

}

WebSocket类,用于连接、接收、发送消息

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());
        }
    }
}
}

vue相关代码

我这里定义了一个悬浮按钮组件,点击按钮弹出编辑框,可以编辑发送类型,接收人,内容。

悬浮按钮组件






消息编辑框




nginx配置

因为一般情况下,前端访问的地址会根据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小时未传输数据则关闭连接
  }

然后点击按钮发送消息就可以了,屏幕边上就会弹出消息框。

你可能感兴趣的:(spring,boot,websocket,vue,nginx,springsecurity)