1、发送消息
在前面我们写了一个自动回复的小例子,用到了@MessageMapping("/hello")
和@SendTo("/topic/greetings")
两个注解,其实我们还可以使用编程的方式发送消息。
其实很简单,直接引用该消息模板
@Autowired private SimpMessagingTemplate messagingTemplate;
消息模板内置了一系列方法,比如
void convertAndSendToUser(String user, String destination, Object payload) void convertAndSend(D destination, Object payload)
这俩方法会包装为消息并将其发送到给定的目的地。
2、STOMP监听
监听webSocket服务器的连接只需要实现ApplicationListener<>接口。代码如下:
@Component public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> { @Override public void onApplicationEvent(SessionConnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); //判断客户端的连接状态 switch (sha.getCommand()) { case CONNECT: System.out.println("上线"); break; case DISCONNECT: System.out.println("下线"); break; case SUBSCRIBE: System.out.println("订阅"); break; case SEND: System.out.println("发送"); break; case UNSUBSCRIBE: System.out.println("取消订阅"); break; default: break; } } }
监听的所有状态被封装在一个枚举类中,其实还有很多,这里不再一一列举。
有了上面这些知识,我们就可以基于此开发一对一聊天服务器。
3、Session存取
webSocket提供的是一个socket框架,并不会帮我们管理session,我们需要自己去编写session管理类,进行session的读写。代码如下:
@Component public class SocketSessionMap { private final static ConcurrentMap<String, String> sessionMap = new ConcurrentHashMap<>(); /** * 注册Session * @param userId * @param sessionId */ public synchronized void registerSession(String userId, String sessionId) { sessionMap.put(userId,sessionId); } /** * 移除Session * @param userId * @param sessionId */ public synchronized void removeSession(String userId, String sessionId) { sessionMap.remove(userId); } /** * 获取用户的SessionId * @param userId * @return */ public String getUserSessionId(String userId){ return sessionMap.get(userId); } /** * 获取所有Session集合 * @return */ public Map<String, String> queryAllSession(){ return sessionMap; } /** * 获取集合的大小 */ public int onlineCount(){ return sessionMap.size(); } }
4、监听注册
接着,对STOMP监听类进行扩展。
@Component public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> { @Autowired SocketSessionMap socketSessionMap; @Override public void onApplicationEvent(SessionConnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); String userId = sha.getFirstNativeHeader("id"); String sessionId = sha.getSessionId(); switch (sha.getCommand()) { case CONNECT: System.out.println("上线:" + userId + " " + sessionId); socketSessionMap.registerSession(userId, sessionId); break; default: break; } } }
服务端通过sha.getFirstNativeHeader("id")
读取到客户端的ID,这个值需要网页客户端手动在header头部信息中设置。
当服务端监听到客户端连接时,会将用户SessionId注册到Map中。
5、监听下线
这里我们使用更可靠的请求下线方式,代码如下:
@MessageMapping("/chatOut") public void sayHello(String userId) { String sessionId = socketSessionMap.getUserSessionId(userId); System.out.println("下线:" + userId + " " + sessionId); socketSessionMap.removeSession(userId,sessionId); }
当收到下线请求时,移除SessionId。
关于Session,也可以设置一个最大值,超时自动移除。
6、一对一消息处理
在一对一服务器中,主要处理的就是一对一的消息发送。大致逻辑是接收客户端消息,分析消息结构,通过SessionMap判断对方是否在线,然后发送相应内容。代码如下:
@MessageMapping("/chat") public void sayHello(Message user) { System.out.println(user.getId()+"-->"+user.getPid()+":"+user.getContent()); String userPid = String.valueOf(user.getPid()); String userId = String.valueOf(user.getId()); String sendTo = "/topic/chat/"+userPid; String content = user.getId()+":"+user.getContent(); if (socketSessionMap.getUserSessionId(userPid)!=null){ messagingTemplate.convertAndSend(sendTo, HtmlUtils.htmlEscape(content)); }else { sendTo = "/topic/chat/"+userId; content = "对方已下线"; messagingTemplate.convertAndSend(sendTo, HtmlUtils.htmlEscape(content)); } }
值得一体的是,关于用户ID的处理,这里使用的是自定义客户端地址,不同的地址表示不同的用户。最后通过convertAndSend()方法发送,这种方式比较可靠方便。
Message.java
public class Message { private int id; //用户ID private String content;//发送内容 private int pid; //发送到用户
7、聊天网页
服务端使用FreeMarker模板引擎返回html网页,代码如下:
@RequestMapping("/chat/{id}") public String chat_page(@PathVariable int id, ModelMap model) { model.addAttribute("id", id); int count = socketSessionMap.onlineCount(); model.addAttribute("count", count); return "chat"; }
通过RESTful形式的URl注册ID。
chat.html
<!DOCTYPE html > <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello WebSocket</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <link rel="shortcut icon" href="/favicon.ico" /> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/chat.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="row"> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label for="connect">WebSocket connection:</label> <button id="connect" class="btn btn-default" type="submit">Connect</button> <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect </button> </div> </form> </div> <div class="col-md-6"> <form class="form-inline"> <div class="form-group"> <label th:text="'Online:'+${count}"> </label> <input type="text" id="id" class="form-control" th:value="${id}"required> <input type="text" id="content" class="form-control" placeholder="Your name here..."required> <input type="text" id="pid" class="form-control" placeholder="Your PID"required> </div> <button id="send" class="btn btn-default" type="submit">Send</button> </form> </div> </div> <div class="row"> <div class="col-md-12"> <table id="conversation" class="table table-striped"> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody id="greetings"> </tbody> </table> </div> </div> </div> </body> </html>
chat.js
var stompClient = null; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({"id": $("#id").val()}, function (frame) { //客户端ID setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/chat/' + $("#id").val(), function (greeting) { //表明客户端地址 showGreeting(greeting.body); }, {"id": "Host_" + $("#id").val()}); }); } function disconnect() { if (stompClient !== null) { stompClient.send("/app/chatOut", {},$("#id").val()); stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.send("/app/chat", {}, JSON.stringify({ 'content': $("#content").val(), 'id': $("#id").val(), 'pid': $("#pid").val() })); showGreeting("我:" + $("#content").val()) } function showGreeting(message) { $("#greetings").append("<tr><td>" + message + "</td></tr>"); } $(function () { $("form").on('submit', function (e) { e.preventDefault(); }); $("#connect").click(function () { connect(); }); $("#disconnect").click(function () { disconnect(); }); $("#send").click(function () { sendName(); }); });
网页客户端的主要逻辑在chat.js中。非常有必要的是在stompClient.connect()方法的第
一个参数中传入header头信息,该头部信息必须设置id字段的值,因为服务端会读取该ID值,该值最终会取自URL中的参数。
其次是在用户断开链接前,会向服务端发送断开通知。
stompClient.send("/app/chatOut", {},$("#id").val());
8、程序演示
打开两个网页,URl分别为http://localhost:8080/chat/100
和http://localhost:8080/chat/101
,点击连接。
查看控制台输出:
接下来演示用户101向用户100发送消息:
再次查看控制输出:
消息成功发送!
当用户断开链接时,控制台输出为: