后端搭建WebSocket
1.Maven引用jar包
引用spring boot依赖的websocket
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.配置bean
如果用springboot内置tomcat启动项目的,需要配置这个Bean;
如果是外部的启动容器,则不需要配置
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.配置返还DTO
如果返回给前端的信息是实体,就需要配置这个
import com.alibaba.fastjson.JSONObject;
import me.zhengjie.modules.socket.rest.WebSocketResDto;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class ServerEncoder implements Encoder.Text<WebSocketResDto> {
//WebSocketResDto 就是返回的实体
@Override
public String encode(WebSocketResDto webSocketResDto) throws EncodeException {
return JSONObject.toJSONString(webSocketResDto);
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
4.编写WebSocket
刚刚配置的ServerEncoder 在注解 @ServerEndpoint中使用
import com.alibaba.fastjson.JSONObject;
import me.zhengjie.modules.socket.util.ApplicationContextRegister;
import me.zhengjie.modules.socket.util.ENUM_MESSAGE_TYPE;
import me.zhengjie.modules.socket.util.ServerEncoder;
import me.zhengjie.modules.system.util.ENUM_TOKEN_AES_KEY;
import me.zhengjie.modules.system.util.SymmetricEncoder;
import me.zhengjie.test.domain.BcUser;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
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.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint(value = "/ws/{userId}",encoders = {ServerEncoder.class})
public class WebSocketController {
//静态变量,用来记录当前在线连接数
private static int onlineCount = 0;
//使用Map来存放每个用户的连接,其中Key为用户标识
private static ConcurrentHashMap<String, WebSocketController> webSocketMap = new ConcurrentHashMap<String, WebSocketController>();
//客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(WebSocketController.class);
/**
* 连接建立成功调用的方法
* @param session 客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId){
logger.info("进入WebSocket--onOPen,userID:"+userId);
this.session = session;
//加入map中
webSocketMap.put(userId,this);
//获取业务Service处理类
ApplicationContext applicationContext = ApplicationContextRegister.getApplicationContext();
BcUserService bc = applicationContext.getBean(BcUserService.class);
addOnlineCount(); //在线数加1
//第一次进来需要处理的业务
sendUser(userId,bc);
logger.info("有新连接加入!当前在线人数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("userId") String userId){
logger.info("进入onClose,userID:"+userId);
//从map中删除
webSocketMap.remove(userId);
subOnlineCount(); //在线数减1
logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 当用户余额发生变动时,其他地方的业务处理可以直接调用该方法来发送socket
* 因为该方法不支持传参实体,所以用json转换
*/
@OnMessage
public void onMessage(String jsonStr) {
try {
WebSocketResDto resDto = JSONObject.parseObject(jsonStr, WebSocketResDto.class);
//如果该属性有值就是心跳连接
if(!StringUtils.isBlank(resDto.getHeartbeat())){
logger.info("进入WebSocketTest--心跳,token:"+resDto.getUserId());
String userId = resDto.getUserId();
//获取业务Service处理类
ApplicationContext applicationContext = ApplicationContextRegister.getApplicationContext();
BcUserService bc = applicationContext.getBean(BcUserService.class);
String userId="";
sendUser(userId,bc);
}else{
sendMessage(resDto);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
logger.info("发生错误");
error.printStackTrace();
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketController.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketController.onlineCount--;
}
/**
* 该方法用来处理业务
*/
public void sendMessage(WebSocketResDto resDto) throws IOException {
try {
if(resDto.getMessageType()==ENUM_MESSAGE_TYPE.USER.getType()){
String userId= resDto.getUserId();
logger.info("个人用户发送信息,user_id:"+userId);
//发送最新数据给用户
WebSocketController webSocketTest = webSocketMap.get(userId);
if(webSocketTest!=null){
webSocketTest.session.getBasicRemote().sendObject(resDto);
logger.info("个人用户信息发送完成!");
}else{
logger.info("无法查询到该用户的连接,无法发送数据!ID:"+userId);
}
}else if(resDto.getMessageType()==ENUM_MESSAGE_TYPE.SYSTEM.getType()){
logger.info("-----发送系统信息------");
//给所有用户推送信息
for (WebSocketController webSocketController : webSocketMap.values()) {
webSocketController.session.getBasicRemote().sendObject(resDto);
}
logger.info("-----发送系统信息完成------");
}
} catch (Exception e) {
e.printStackTrace();
logger.info("发送信息异常!");
logger.info(e.toString());
}
}
//初始连接就发送用户信息到前端
public void sendUser(String userId,BcUserService bc){
try {
logger.info("发送余额至前端 userId"+userId);
BcUser bcUser = bc.findOneById(Integer.parseInt(userId));
WebSocketController webSocketTest = webSocketMap.get(userId);
if(bcUser!=null && webSocketTest!=null){
logger.info("开始发送");
WebSocketResDto resDto=new WebSocketResDto();
resDto.setBalance(bcUser.getBalance());
webSocketTest.session.getBasicRemote().sendObject(resDto);
}else{
logger.info("发送失败! bcUser:"+bcUser+"----webSocketTest:"+webSocketTest);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5.浏览器使用webSocket
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
<script>
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
//建立连接,这里的/websocket ,是Servlet中注解中的那个值
websocket = new WebSocket("ws://localhost:81/ws/1");
}else {
alert('当前浏览器 Not support websocket');
}
//连接发生错误的回调方法
websocket.onerror = function () {
console.log("WebSocket连接发生错误");
//如果发生错误,就重连一次
websocket = new WebSocket("ws://localhost:81/ws/1");
};
//连接成功建立的回调方法
websocket.onopen = function () {
console.log("WebSocket连接成功");
//连接成功时,开启心跳
heartCheck.reset().start();
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
//收到消息时,再开启一次心跳
heartCheck.reset().start();
console.log(event);
console.log(event.data);
}
//连接关闭的回调方法
websocket.onclose = function () {
console.log("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭WebSocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//心跳检测
var heartCheck = {
timeout: 1000 * 60, //1分钟发一次心跳
timeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
return this;
},
start: function(){
var self = this;
console.log("心跳检测");
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
console.log("发送心跳检测");
websocket.send("{'heartbeat':'true','token':'1'}");
}, this.timeout)
}
}
</script>
加入心跳检测,是为了保持连接的稳定。
6.总结
该方法只支持ws协议的WebSocket!!!
因为之前项目加了了HTTPS协议(看上一篇),然后我又找不到wss协议的弄法,于是把后台监控HTTPS协议请求改了下:
这样 HTTPS就只会监控请求地址为 /api/* 的接口,然后给项目开启了HTTP的端口,这样WebSocket又能走ws协议的,项目接口访问也能走HTTPS!!!*
最后,是后台使用到的ApplicationContext工具类代码
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
@Lazy(false)
public class ApplicationContextRegister implements ApplicationContextAware{
private static ApplicationContext APPLICATION_CONTEXT;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
APPLICATION_CONTEXT=applicationContext;
}
public static ApplicationContext getApplicationContext() {
return APPLICATION_CONTEXT;
}
}