什么是WebSocket
WebSocket 是一种网络通信协议,很多高级功能都需要它。
我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
如果我们想要服务器给客户端发信息,只能由客户端建立长连接这种消耗性能的操作。
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
详细的WebSocket可以参考阮一峰的博客 :http://www.ruanyifeng.com/blog/2017/05/websocket.html
Maven 依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
SpringBoot配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** Created with IntelliJ IDEA.** @Auther: zlf* @Date: 2021/04/30/16:58* @Description:*/
@Configuration
//@EnableWebSocketMessageBroker
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}}
WebSokect 通信
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;/*** Created with IntelliJ IDEA.** @Auther: zlf* @Date: 2021/04/30/17:48* @Description:*/
@Slf4j
@ServerEndpoint("/webSocket/{code}")
@Component
public class WebSocketServer {/*** concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。*/private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();/*** 与客户端的连接会话,需要通过它来给客户端发送数据*/private Session session;/*** 接收识别码*/private String code = "";/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("code") String code) {this.session = session;//如果存在就先删除一个,防止重复推送消息,实际这里实现了set,不删除问题也不大webSocketSet.removeIf(webSocket -> webSocket.code.equals(code));webSocketSet.add(this);this.code = code;log.info("建立WebSocket连接,code:" + code+",当前连接数:"+webSocketSet.size());}/*** 连接关闭调用的方法*/@OnClosepublic void onClose() {webSocketSet.remove(this);log.info("关闭WebSocket连接,code:" + this.code+",当前连接数:"+webSocketSet.size());}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session) {log.info("收到来[" + code + "]的信息:" + message);}@OnErrorpublic void onError(Session session, Throwable error) {log.error("websocket发生错误");error.printStackTrace();}/*** 实现服务器主动推送*/private void sendMessage(String message) throws IOException {this.session.getBasicRemote().sendText(message);}/*** 群发自定义消息*/public void sendAll(String message) {log.info("推送消息到" + code + ",推送内容:" + message);for (WebSocketServer item : webSocketSet) {try {item.sendMessage(message);} catch (IOException e) {e.printStackTrace();}}}/*** 定点推送*/public void sendTo(String message, @PathParam("code") String code) {for (WebSocketServer item : webSocketSet) {try {if (item.code.equals(code)) {log.info("推送消息到[" + code + "],推送内容:" + message);item.sendMessage(message);}} catch (IOException e) {e.printStackTrace();}}}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}WebSocketServer that = (WebSocketServer) o;return Objects.equals(session, that.session) &&Objects.equals(code, that.code);}@Overridepublic int hashCode() {return Objects.hash(session, code);}
}
这样前端发起相应的请求就可以建立双向的通信。
#前端Vue代码
<template><div>websocket</div>
</template><script>
export default {data(){return{user : JSON.parse(localStorage.getItem("user"))?JSON.parse(localStorage.getItem("user")):null}},created(){this.initWebSocket();},methods: {initWebSocket(){ //初始化weosocket// 建立连接 在请求中带上token 由SpringSecurity进行权限认证const wsuri = "ws://localhost:8001/webSocket/"+this.user.id+'?Authorization='+localStorage.getItem("token");this.websock = new WebSocket(wsuri);this.websock.onmessage = this.websocketonmessage;this.websock.onopen = this.websocketonopen;this.websock.onerror = this.websocketonerror;this.websock.onclose = this.websocketclose;},websocketonopen(){ //连接建立之后执行send方法发送数据let actions = {"test":"12345"};this.websocketsend(JSON.stringify(actions));},websocketonerror(){//连接建立失败重连console.log("----连接失败,,重连")this.initWebSocket();},websocketonmessage(e){ //数据接收//const redata = JSON.parse(e.data);console.log(e.data);},websocketsend(Data){//数据发送this.websock.send(Data);},websocketclose(e){ //关闭console.log('断开连接',e);},},
}
</script><style></style>
关于权限认证
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.youshe.mcp.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;/*** @Description: JWT登录授权过滤器* @Author: zlf* @Date: 2021/3/30*/
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Value("${jwt.tokenHeader}")private String tokenHeader;@Value("${jwt.tokenHead}")private String tokenHead;@Autowiredprivate MyUserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws ServletException, IOException {String authHeader = request.getHeader(this.tokenHeader);if(StringUtils.isBlank(authHeader)){// 由于webSocket 没有设置token在请求头里,而在url中// websocket连接时,令牌放在url参数上,authHeader = request.getParameter(this.tokenHeader);}if (authHeader != null && authHeader.startsWith(this.tokenHead)) {String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "String username = jwtTokenUtil.getUserNameFromToken(authToken);//log.info("checking username:{}", username);if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(authToken, userDetails)) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, new BCryptPasswordEncoder().encode(userDetails.getPassword()), userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//log.info("authenticated user:{}", username);SecurityContextHolder.getContext().setAuthentication(authentication);}}}chain.doFilter(request, response);}}
测试
import com.youshe.commonutils.vo.ResultVo;
import com.youshe.mcp.entity.User;
import com.youshe.mcp.service.UserService;
import com.youshe.mcp.service.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.annotations.ApiIgnore;
import sun.plugin.liveconnect.SecurityContextHelper;import javax.xml.transform.Result;
import java.security.Security;/*** Created with IntelliJ IDEA.** @Auther: zlf* @Date: 2021/04/30/18:03* @Description:*/
@RestController
@RequestMapping("/test")
public class testController {@AutowiredUserService userService;@AutowiredWebSocketServer webSocketServer;// 向请求的用户 推送消息@GetMapping("test")public ResultVo test(){UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();String username = userDetails.getUsername();User user = userService.getUserByName(username);webSocketServer.sendTo("向客户端推送实时消息",user.getId());return ResultVo.ok();}
}