一,maven pom.xml的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- <version>1.16.20</version> -->
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.2</version>
</dependency>
<!-- 验证码波纹 -->
<dependency>
<groupId>com.jhlabs</groupId>
<artifactId>filters</artifactId>
<version>2.0.235</version>
</dependency>
<!-- alibaba json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
</dependencies>
<!-- 打包spring boot应用 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>mybatis generator</id>
<phase>deploy</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 允许移动生成的文件 -->
<verbose>false</verbose>
<!-- 是否允许覆盖 -->
<overwrite>false</overwrite>
<configurationFile>
src/main/resources/mybatis-generator.xml
</configurationFile>
</configuration>
</plugin>
<!-- 指定jdk版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- jdk版本,spring-boot最好依赖jdk8,否则spring-boot版本过高会造成不兼容 -->
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArguments>
<verbose />
<bootclasspath>${java.home}/lib/rt.jar${path.separator}${java.home}/lib/jce.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<!-- 将maven将java项目依赖包一起打入一个jar包内 -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass></mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
二:,核心代码:实现WebSocketMessageBrokerConfigurer接口,注册授权拦截器与用户ID
1,每次连接首先进入AuthInterceptor拦截器通过HttpRequest获取seesion中自定义字段判断客户是否登录,未登录则不让连接
2,紧接着进入PrincipalHandler冲中取出sesson的自定义字段作为唯一ID,返回给Principal,该ID会作为点对点发给客户端的凭证
3,通过WebSocketHandlerDecoratorFactory可以统计在线人数
package com.lsh.springboot.js.monitor.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
import com.lsh.springboot.js.monitor.socket.AuthInterceptor;
import com.lsh.springboot.js.monitor.socket.LshPrincipal;
import com.lsh.springboot.js.monitor.socket.PrincipalHandler;
import com.lsh.springboot.js.monitor.socket.SocketManager;
/**
* 功能说明: Socket配置类<br>
* 系统版本: v1.0<br>
* 开发人员: @author liansh<br>
* 开发时间: 2020年3月6日<br>
*/
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private PrincipalHandler principalHandler;
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/socket").setAllowedOrigins("*").//
addInterceptors(authInterceptor).//
setHandshakeHandler(principalHandler).//
withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
/** 前缀:queue 点对点 topic 广播 user 点对点 */
registry.enableSimpleBroker("/queue", "/topic");
registry.setUserDestinationPrefix("/user");
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
LshPrincipal principal = (LshPrincipal) session.getPrincipal();
log.info("有人连接啦 userId = {}", principal);
if (principal != null) {
// 身份校验成功,缓存socket连接
SocketManager.add(principal.getName(), session);
}
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
LshPrincipal principal = (LshPrincipal) session.getPrincipal();
log.info("有人退出连接啦 userId = {}", principal);
if (principal != null) {
// 身份校验成功,移除socket连接
SocketManager.remove(principal.getName());
}
super.afterConnectionClosed(session, closeStatus);
}
};
}
});
}
}
package com.lsh.springboot.js.monitor.action;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.socket.WebSocketSession;
import com.lsh.springboot.js.monitor.enums.MessCharTypeEnum;
import com.lsh.springboot.js.monitor.enums.MessContTypeEnum;
import com.lsh.springboot.js.monitor.service.IMessService;
import com.lsh.springboot.js.monitor.service.modelObject.MessInfoMO;
import com.lsh.springboot.js.monitor.socket.SocketManager;
/**
* 功能说明: WebSocket<br>
* 系统版本: v1.0<br>
* 开发人员: @author liansh<br>
* 开发时间: 2019年10月23日<br>
*/
@Slf4j
@Controller
public class SocketAction {
@Autowired
private IMessService messService;
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public SocketAction(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
/**
* 定时推送消息
*/
// @Scheduled(fixedRate = 5000)
public void callback() {
// 发现消息
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
messagingTemplate.convertAndSend("/callback", "定时推送消息时间: " + df.format(new Date()));
}
/** 服务器指定用户进行推送 */
@ResponseBody
@RequestMapping("/sendUser")
public String sendUser(String userId) {
log.info("userId = {} ,对其发送您好", userId);
WebSocketSession webSocketSession = SocketManager.get(userId);
if (webSocketSession != null) {
/** 主要防止broken pipe */
messagingTemplate.convertAndSendToUser(userId, "/queue/sendUser", "您好");
}
return "1";
}
/** 广播,服务器主动推给连接的客户端 */
@RequestMapping("/sendTopic")
public void sendTopic() {
messagingTemplate.convertAndSend("/topic/sendTopic", "大家晚上好");
}
/** 客户端发消息,服务端接收 */
@MessageMapping("/sendServer")
public void sendServer(String message) {
log.info("message:{}", message);
}
/** 客户端发消息,大家都接收,相当于直播说话 */
@MessageMapping("/sendAllUser")
@SendTo("/topic/sendTopic")
public String sendAllUser(String message) {
// 也可以采用template方式
return message;
}
/** 点对点用户聊天,这边需要注意,由于前端传过来json数据,所以使用@RequestBody */
@MessageMapping("/sendMyUser")
public void sendMyUser(@RequestBody Map<String, String> map) {
String acceId = map.get("acceId");
MessInfoMO messInfoMO = new MessInfoMO();
messInfoMO.setSendId(Integer.parseInt(map.get("sendId")));
messInfoMO.setAcceId(Integer.parseInt(acceId));
messInfoMO.setMessAcceType(MessCharTypeEnum.getMessCharTypeEnumByName(map.get("messSendType")));
messInfoMO.setMessCont(map.get("messCont"));
messInfoMO.setMessContType(MessContTypeEnum.getMessContTypeEnum(map.get("messContType")));
messService.insert(messInfoMO);
WebSocketSession webSocketSession = SocketManager.get(acceId);
if (webSocketSession != null) {
log.info("userId = {}", acceId);
messagingTemplate.convertAndSendToUser(acceId, "/queue/sendUser", messInfoMO);
}
}
}
三:客户端通过SockJS实现与服务端通信
var stompClient;
function socketInit() {
var url = indexConfig.url + '/socket';
var socket = new SockJS(url);
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// 订阅服务端推送(广播)
stompClient.subscribe("/user/queue/sendUser", function(response) {
var body = JSON.parse(response.body);
console.log("subscribe:" + body);
if (acceMessAction != "") {
acceMessAction(body);
}
});
});
}
// 发送给服务端,传对方ID,可以实现点对点
function send(data, url) {
stompClient.send(url || "/sendMyUser", {}, JSON.stringify(data));
}
// 断开连接
function disconnect() {
stompClient.disconnect();
}
四:遇到的问题:
1,若通过nginx配置域名,需要在http头部区域增加
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
否则会导致代码报:Handshake failed due to invalid Upgrade header: null
不注意看的话很难注意到,导致本地正常,一部署就发现不能正常通讯