websocket
客户端构建一个websocket实例,并且为它绑定一个需要连接到的服务器地址,当客户端连接服务端的候,会向服务端发送一个http get报文,告诉服务端需要将通信协议切换到websocket,服务端收到http请求后将通信协议切换到websocket,同时发给客户端一个响应报文,返回的状态码为101,表示同意客户端协议转请求,并转换为websocket协议。以上过程都是利用http通信完成的,称之为websocket协议握手(websocket Protocol handshake),经过握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议了。
1.pom文件添加依赖
<!--webSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.启用Springboot对WebSocket的支持
package com.lby.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author Liby
* @date 2022-04-25 16:18
* @description:
* @version:
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.核心配置:WebSocketServer
因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller
@ ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便传递之间对userId进行推送消息。
下面是具体业务代码:
package com.lby.websocket.component;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import cn.hutool.core.util.StrUtil;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Liby
* @date 2022-04-25 16:21
* @description:
* @version:
*/
@ServerEndpoint(value = "/websocket/{userId}")
@Component
public class WebSocket {
private final static Logger logger = LogManager.getLogger(WebSocket.class);
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象
*/
private static ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
private String userId;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
//加入map
webSocketMap.put(userId, this);
addOnlineCount(); //在线数加1
logger.info("用户{}连接成功,当前在线人数为{}", userId, getOnlineCount());
try {
sendMessage(String.valueOf(this.session.getQueryString()));
} catch (IOException e) {
logger.error("IO异常");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//从map中删除
webSocketMap.remove(userId);
subOnlineCount(); //在线数减1
logger.info("用户{}关闭连接!当前在线人数为{}", userId, getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("来自客户端用户:{} 消息:{}",userId, message);
//群发消息
/*for (String item : webSocketMap.keySet()) {
try {
webSocketMap.get(item).sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}*/
}
/**
* 发生错误时调用
*
* @OnError
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 向客户端发送消息
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
* 通过userId向客户端发送消息
*/
public void sendMessageByUserId(String userId, String message) throws IOException {
logger.info("服务端发送消息到{},消息:{}",userId,message);
if(StrUtil.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
logger.error("用户{}不在线",userId);
}
}
/**
* 群发自定义消息
*/
public static void sendInfo(String message) throws IOException {
for (String item : webSocketMap.keySet()) {
try {
webSocketMap.get(item).sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
}
4.SrpingCloud网关Gateway配置
如果引入gateway,需要配置网关服务路由信息
spring:
cloud:
gateway:
routes:
- id: websocket1
uri: ws://127.0.0.1:8081 #websocket配置,直接地址使用方式1
#uri: lb:ws://serviceName #websocket配置,通过nacos注册中心调用serviceName使用方式2
predicates:
- Path=/websocket
5.websocket传递token信息
建议前端使用原生websocket API请求
使用封装过的api,例如 SocketJS 会有跨域,后端也需另外配置。
websocket连接成功后,如果没有进行通信,过一段时间后会断开连接。所以还需要前端隔5或10秒发送一个心跳请求后台。
页面初始化没有成功后,还需要处理重试连接。
websocket请求头中可以包含Sec-WebSocket-Protocol这个属性,该属性是一个自定义的子协议。它从客户端发送到服务器并返回从服务器到客户端确认子协议。我们可以利用这个属性添加token。
var token='jlllwei68jj776'
var ws = new WebSocket("ws://" + url+ "/webSocketServer",[token]);
WebsocketServer服务
拿到token可以解析判断,set 到response里面,否则Gateway源码WebSocketClientHandshaker会报异常,因为response没拿到子协议
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author luoyj
* @Date 2021/6/7.
*/
@Slf4j
@Order(1)
@Component
@WebFilter(filterName = "WebSocketFilter", urlPatterns = "/websocket/app/edit")
public class WebSocketFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = ((HttpServletRequest) servletRequest).getHeader("Sec-WebSocket-Protocol");
log.info("【WebSocketFilter】response.setHeader = key:{},value:{}","Sec-WebSocket-Protocol",token);
/*if (StringUtils.isNotBlank(token)) {
response.setHeader("Sec-WebSocket-Protocol",token);
filterChain.doFilter(servletRequest, servletResponse);
}else {
throw new BizException(ErrorCode.TEAMWORK_WS_NOT_TOKEN,"websocket请求没有携带token,无法请求!");
}*/
if (StringUtils.isNotBlank(token)) response.setHeader("Sec-WebSocket-Protocol",token);
filterChain.doFilter(servletRequest, servletResponse);
}
}
6.websocket的Nginx配置
server {
listen 80;
server_name 域名;
location / {
proxy_pass http://127.0.0.1:8080/; // 代理转发地址
proxy_http_version 1.1;
proxy_read_timeout 3600s; // 超时设置
// 启用支持websocket连接
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /upload { // 静态资源地址
root /mnt/resources;
}
}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
它表明是websocket连接进入的时候,进行一个连接升级将http连接变成websocket的连接。
proxy_read_timeout; 表明连接成功以后等待服务器响应的时候,如果不配置默认为60s;
proxy_http_version 1.1; 表明使用http版本为1.1