第一步引入WebSocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
第二步配置WebSocket
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
第三步实现自己的服务器逻辑
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
// 日志打印 没有 就用sout
@ServerEndpoint(value = "/webSocket/{username}") //相对于接口名 @RequestMapping("/webSocket/{username}")
@Component
public class WebSocketServiceImpl {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServiceImpl.class);
// 线程安全的map
private static Map<String, WebSocketServiceImpl> webSocketMap = new ConcurrentHashMap<>();
// 原子 int
private static AtomicInteger connetCout = new AtomicInteger(0);
// getter setter
private Session session;
private String username;
private static synchronized Map<String, WebSocketServiceImpl> getWebSocket() {
return webSocketMap;
}
/**
* 初始化时执行
*/
@PostConstruct
public void init() {
LOG.debug("===websocket init===");
}
/**
* 传入 username 以区分不同会话
* 建立会话时执行
*/
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
this.username = username;
this.session = session;
webSocketMap.put(username, this);
connetCout.incrementAndGet();
sendMessage("hhh", username);
LOG.debug("==========={} Session 连接成功============", username);
LOG.debug("Session ID :{}", session.getId());
LOG.debug("=====size {}==========", webSocketMap.size());
}
/**
* 收到客户端消息时执行
*
* @param msg
* @param session
*/
@OnMessage
public void onMessage(String msg, Session session) {
//自己实现 收到客户端消息之后的处理逻辑
//给客户端返回消息 或者 操作数据库等等一系列操作
LOG.debug("收到消息:{}", msg);
sendMessage("你发的消息是:" + msg, username);
}
@OnClose
public void onClose() {
LOG.debug("===============close{}", username);
webSocketMap.remove(username);
if (session != null) {
try {
session.close();
connetCout.decrementAndGet();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
LOG.debug("用户名{}", this.username);
LOG.debug("会话是否连接{}", session.isOpen());
LOG.debug("会话id{}", session.getId());
}
/**
* 发送消息给指定人
*
* @param msg 消息
* @param receiver 消息接收方
*/
public static void sendMessage(String msg, String receiver) {
try {
ObjectMapper mapper = new ObjectMapper();
String json_msg = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(msg);
WebSocketServiceImpl webSocketService = webSocketMap.get(receiver);
if (webSocketService != null) {
Session session = webSocketService.session;
if (session.isOpen()) {
session.getAsyncRemote().sendText(json_msg);
}
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
public static void sendMessage(String msg, String[] receivers) {
for (String receiver : receivers) {
sendMessage(msg, receiver);
}
}
//广播消息 略
public int getSize() {
return webSocketMap.size();
}
}
第四步Vue实现客户端
用的是element-UI
<template>
<el-form :model="data">
<el-form-item label="消息">
<el-input v-model="data.msg"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="init">
初始化
</el-button>
<el-button @click="doSubmit">
发送
</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
data: {
msg: ""
},
//@ServerEndpoint(value = "/webSocket/{username}")
path: "ws://localhost:8866/webSocket/", // ws:// +ip:port/webSocket/
}
},
mounted() {
// this.init();
},
methods: {
doSubmit() {
console.log(this.data.msg)
this.send(this.data.msg)
},
init: function () {
if (typeof (WebSocket) === "undefined") {
this.$message.error("您的浏览器不支持socket")
} else {
// 实例化socket 此处 是登录时存放的 token 和 username
let token = sessionStorage.getItem("token")
let username = sessionStorage.getItem("username")
// 拼接 path : ws://localhost:8866/webSocket/username
// 如果不需要token验证 则写成
this.socket = new WebSocket(this.path + username)
// 带token 因为ws协议无法编辑请求头 这里通过子协议携带token
// this.socket = new WebSocket(this.path + username, [token])
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.getMessage
}
},
open: function () {
console.log("socket连接成功")
},
error: function () {
console.log("连接错误")
console.log(arguments)
},
getMessage: function (msg) {
console.log(msg.data)
},
// 发送消息给被连接的服务端
send: function (params) {
this.socket.send(params)
},
close: function () {
console.log("socket已经关闭")
},
},
destroyed() {
this.socket.onclose = this.close
}
};
</script>
<style scoped>
</style>
- 连接服务器
客户端浏览器控制台打印
2. 服务器打印(不要纠结会话id,已经是第N次连接了)
3. 发消息
到此,一个简单的案例已经实现了
我项目里还用了Spring-Security
因为项目一开始使用了token,上面的ws协议并不能像http一样携带token,我在引入token时,客户端一连接就立即断开
下面开始解决token问题
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.PostConstruct;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Component
public class WebSocketFilter implements Filter {
public static final Logger LOG = LoggerFactory.getLogger(WebSocketFilter.class);
private static final List<String> PATTERN_LIST = new ArrayList<>();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
LOG.debug("==============doFilter==============");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String servletPath = request.getServletPath();
if (matcher(servletPath)) {
//自己定义的静态变量 实际就是"Sec-Websocket-Protocol"
//HandshakeRequest.SEC_WEBSOCKET_PROTOCOL ws包里有一个一样的
String token = request.getHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL);
//这里CommonService.checkToken 是自己验证token的逻辑 验证通过就返回true
//比如前面vue [token]中的token="asdfghj"
//这里 boolean check = "asdfghj".equals(token) 返回true
// 怎么校验自己定
boolean check = CommonService.checkToken(token, response);
if (check) {
// 这里 我看网上人家是没有这一步的 但是我不设置就会连接成功就断开
response.setHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL, token);
// if pass 没有这一步就被拦截了
chain.doFilter(servletRequest, servletResponse);
}
} else {
chain.doFilter(servletRequest, servletResponse);
}
}
@PostConstruct
public void init() {
//初始化时注入 后续过滤项多了可以配置批量注入
PATTERN_LIST.add("/webSocket/**");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
public boolean matcher(String servletPath) {
AntPathMatcher matcher = new AntPathMatcher(File.separator);
return PATTERN_LIST.stream().anyMatch(pattern -> matcher.match(pattern, servletPath));
}
}
这里最主要的就是在response添加 这个
response.setHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL, token);
解决完之后 vue 写成
let token = '你的token'
this.socket = new WebSocket(this.path + username, [token])
没加token之前Request Headers是没有这个的
在过滤器中设置的Sec-Websocket-Protocol
也成功反回来了
Response Headers
最后,引入token验证之后就连接失败这个问题,卡了我两天,期间尝试了各种方式拦截token都没有成功。最终发现少了这一行代码解决response.setHeader