SpringBoot使用WebSocket实现即时消息

环境:SpringBoot2.3.9.RELEASE


依赖

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

定义消息类型

  • 抽象消息对象
public class AbstractMessage {
	/**
	 * 	消息类型
	 */
	protected String type ;
	
	/**
	 * 	消息内容
	 */
	protected String content ;
	/**
	 * 	消息日期
	 */
	protected String date ;
}

消息对象子类

1、Ping检查消息

public class PingMessage extends AbstractMessage {
	public PingMessage() {}
	public PingMessage(String type) {
		this.type = type ;
	}
}

2、系统消息

public class SystemMessage extends AbstractMessage {
	public SystemMessage() {}
	public SystemMessage(String type, String content) {
		this.type = type ;
		this.content = content ;
	}
}

3、点对点消息

public class PersonMessage extends AbstractMessage {
	private String fromName ;
	private String toName ;
}

消息类型定义

public enum MessageType {
	
	/**
	 * 	系统消息 0000;心跳检查消息 0001;点对点消息2001
	 */
	SYSTEM("0000"), PING("0001"), PERSON("2001") ;
	
	private String type ;
	
	private MessageType(String type) {
		this.type = type ;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}
	
}

WebSocket服务端点

该类作用就是定义客户端连接的地址

@ServerEndpoint(value = "/message/{username}", 
	encoders = {WsMessageEncoder.class},
	decoders = {WsMessageDecoder.class},
	subprotocols = {"gmsg"},
	configurator = MessageConfigurator.class)  
@Component  
public class GMessageListener {  
  
    public static ConcurrentMap<String, UserSession> sessions = new ConcurrentHashMap<>();
    private static Logger logger = LoggerFactory.getLogger(GMessageListener.class) ;
  
    private String username ;
    
    @OnOpen  
    public void onOpen(Session session, EndpointConfig config, @PathParam("username") String username){
    	UserSession userSession = new UserSession(session.getId(), username, session) ;
    	this.username = username ;
		sessions.put(username, userSession) ;
		logger.info("【{}】用户进入, 当前连接数:{}", username, sessions.size()) ; 
    }  
  
    @OnClose  
    public void onClose(Session session, CloseReason reason){  
    	UserSession userSession = sessions.remove(this.username) ;
    	if (userSession != null) {
    		logger.info("用户【{}】, 断开连接, 当前连接数:{}", username, sessions.size()) ;
    	}
    }
    
    @OnMessage
    public void pongMessage(Session session, PongMessage message) {
    	ByteBuffer buffer = message.getApplicationData() ;
    	logger.debug("接受到Pong帧【这是由浏览器发送】:" + buffer.toString());
    }
    
    @OnMessage
    public void onMessage(Session session, AbstractMessage message) {
    	if (message instanceof PingMessage) {
    		logger.debug("这里是ping消息");
    		return ;
    	}
    	if (message instanceof PersonMessage) {
    		PersonMessage personMessage = (PersonMessage) message ;
    		if (this.username.equals(personMessage.getToName())) {
    			logger.info("【{}】收到消息:{}", this.username, personMessage.getContent());
    		} else {
    			UserSession userSession = sessions.get(personMessage.getToName()) ;
    			if (userSession != null) {
    				try {
						userSession.getSession().getAsyncRemote().sendText(new ObjectMapper().writeValueAsString(message)) ;
					} catch (JsonProcessingException e) {
						e.printStackTrace();
					}
    			}
    		}
    		return ;
    	}
    	if (message instanceof SystemMessage) {
    		logger.info("接受到消息类型为【系统消息】") ; 
    		return ;
    	}
    }
    
    @OnError
    public void onError(Session session, Throwable error) {
    	logger.error(error.getMessage()) ;
    }
}

WsMessageEncoder.java类
该类的主要作用是,当发送的消息是对象时,该如何转换

public class WsMessageEncoder implements Encoder.Text<AbstractMessage> {
	private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ;
	@Override
	public void init(EndpointConfig endpointConfig) {
	}
	@Override
	public void destroy() {
	}
	@Override
	public String encode(AbstractMessage tm) throws EncodeException {
		String message = null ;
		try {
			message = new ObjectMapper().writeValueAsString(tm);
		} catch (JsonProcessingException e) {
			logger.error("JSON处理错误:{}", e) ;
		}
		return message;
	}
}

WsMessageDecoder.java类
该类的作用是,当接收到消息时如何转换成对象。

public class WsMessageDecoder implements  Decoder.Text<AbstractMessage> {

	private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ;
	private static Set<String> msgTypes = new HashSet<>() ;
	
	static {
		msgTypes.add(MessageType.PING.getType()) ;
		msgTypes.add(MessageType.SYSTEM.getType()) ;
		msgTypes.add(MessageType.PERSON.getType()) ;
	}
	@Override
	@SuppressWarnings("unchecked")
	public AbstractMessage decode(String s) throws DecodeException {
		AbstractMessage message = null ;
		try {
			ObjectMapper mapper = new ObjectMapper() ;
			Map<String,String> map = mapper.readValue(s, Map.class) ;
			String type = map.get("type") ;
			switch(type) {
				case "0000":
					message = mapper.readValue(s, SystemMessage.class) ;
					break;
				case "0001":
					message = mapper.readValue(s, PingMessage.class) ;
					break;
				case "2001":
					message = mapper.readValue(s, PersonMessage.class) ;
					break;
			}
		} catch (JsonProcessingException e) {
			logger.error("JSON处理错误:{}", e) ;
		}
		return message ;
	}

	// 该方法判断消息是否可以被解码(转换)
	@Override
	@SuppressWarnings("unchecked")
	public boolean willDecode(String s) {
		Map<String, String> map = new HashMap<>() ;
		try {
			map = new ObjectMapper().readValue(s, Map.class);
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		logger.debug("检查消息:【" + s + "】是否可以解码") ;
		String type = map.get("type") ;
		if (StringUtils.isEmpty(type) || !msgTypes.contains(type)) {
			return false ;
		}
		return true ;
	}
	@Override
	public void init(EndpointConfig endpointConfig) {
	}
	@Override
	public void destroy() {
	}
}

MessageConfigurator.java类
该类的作用是配置服务端点,比如配置握手信息

public class MessageConfigurator extends ServerEndpointConfig.Configurator {
	private static Logger logger = LoggerFactory.getLogger(MessageConfigurator.class) ;
	@Override
	public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
		logger.debug("握手请求头信息:" + request.getHeaders());
		logger.debug("握手响应头信息:" + response.getHeaders());
		super.modifyHandshake(sec, request, response);
	}	
}

WebSocke配置类

@Configuration
public class WebSocketConfig {
	
	@Bean
    public ServerEndpointExporter serverEndpointExporter (){  
        return new ServerEndpointExporter();  
    }  
	
}

当以jar包形式运行时需要配置该bean,暴露我们配置的@ServerEndpoint;当我们以war独立tomcat运行时不能配置该bean。

前端页面

<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <meta name="Author" content="">
  <meta name="Keywords" content="">
  <meta name="Description" content="">
  <script src="g-messages.js?v=1"></script>
  <title>WebSocket</title>
  <style type="text/css">
  </style>
  <script>
	let gm = null ;
	let username = null ;
	function ListenerMsg({url, protocols = ['gmsg'], options = {}}) {
		if (!url){ 
			throw new Error("未知服务地址") ;
		}
		gm = new window.__GM({
			url: url,
			protocols: protocols
		}) ;
		gm.open(options) ;
	}
	ListenerMsg.init = (user) => {
		if (!user) {
			alert("未知的当前登录人") ;
			return ;
		}
		let url = `ws://localhost:8080/message/${user}` ;
		let msg = document.querySelector("#msg")
		ListenerMsg({url, options: {
			onmessage (e) {
				let data = JSON.parse(e.data) ;
				let li = document.createElement("li") ;
				li.innerHTML = "【" + data.fromName + "】对你说:" + data.content ;
				msg.appendChild(li) ;
			}
		}}) ;
	}
	function enter() {
		username = document.querySelector("#nick").value ;
		ListenerMsg.init(username) ;
		document.querySelector("#chat").style.display = "block" ;
		document.querySelector("#enter").style.display = "none" ;
		document.querySelector("#cu").innerText = username ;
	}
	function send() {
		let a = document.querySelector("#toname") ;
		let b = document.querySelector("#content") ;
		let toName = a.value ;
		let content = b.value ;
		gm.sendMessage({type: "2001", content, fromName: username, toName}) ;
		a.value = '' ;
		b.value = '' ;
	}
  </script>
 </head>
 <body>
	<div id="enter">
		<input id="nick"/><button type="button" onclick="enter()">进入</button>
	</div>
	<hr/>
	<div id="chat" style="display:none;">
		当前用户:<b id="cu"></b><br/>
		用户:<input id="toname" name="toname"/><br/><br/>
		内容:<textarea id="content" rows="3" cols="22"></textarea><br/>
		<button type="button" onclick="send()">发送</button>
	</div>
	<div>
		<ul id="msg">
		</ul>
	</div>
 </body>
</html>

这里有个g-messages.js文件是我写的一个工具类,用来做连接及心跳检查用的。由于内容比较多,有需要的关注我发私信我。

到此所有的代码完毕,接下来测试

测试

打开两个标签页,以不同的用户进入。

SpringBoot使用WebSocket实现即时消息

 

SpringBoot使用WebSocket实现即时消息

 

输入对方用户名发送消息

SpringBoot使用WebSocket实现即时消息

 

SpringBoot使用WebSocket实现即时消息

 

成功了,简单的websocket。我们生产环境还就这么完的,8g内存跑了6w的用户。

需要g-message.js的关注我发私信哦。

完毕!!!

给个关注+转发呗谢谢

SpringBoot使用WebSocket实现即时消息

 

SpringBoot使用WebSocket实现即时消息

 

SpringBoot使用WebSocket实现即时消息

 

SpringBoot使用WebSocket实现即时消息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值