环境:SpringBoot2.4.12.
<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; } }
该类作用就是定义客户端连接的地址
@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); } }
@Configurationpublic 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> <id="enter"> <input id="nick"/><button type="button" onclick="enter()">进入</button> </div> <hr/> <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文件是我写的一个工具类,用来做连接及心跳检查用的。
到此所有的代码完毕,接下来测试
打开两个标签页,以不同的用户进入。
输入对方用户名发送消息
成功了,简单的websocket。我们生产环境还就这么完的,8g内存跑了6w的用户。
本文链接:http://www.28at.com/showinfo-26-5718-0.htmlSpringBoot使用WebSocket实现即时消息
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 停止过度设计中等规模的前端应用程序
下一篇: 提升Go的HTTP路由器的提案