spring-boot websocket实现聊天功能

一,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

不注意看的话很难注意到,导致本地正常,一部署就发现不能正常通讯

代码示例

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值