功能说明:多人聊天系统,主要功能点:
1、当你登陆成功后,可以看到所有在线用户(实际开发可以通过redis实现,我这边仅仅用map集合)
2、实现群聊功能,我发送消息,大家都可以看到。
先看案例效果:
这里面有关在线人数有个bug,就是在线用户会被覆盖,lisi登陆的话,zhangsan在线信息就丢来,xiaoxiao登陆,lisi就丢来,这主要原因是因为我放的是普通集合,所以在线用户数据是无法共享
所以只能显示最后显示的用户,如果放到redis就不会有这个问题。
@Controller public class UserChatController { @Autowired private WebSocketService ws; /** * 1、登陆时,模拟数据库的用户信息 */ //模拟数据库用户的数据 public static MapuserMap = new HashMap (); static{ userMap.put("zhangsan", "123"); userMap.put("lisi", "456"); userMap.put("wangwu", "789"); userMap.put("zhaoliu", "000"); userMap.put("xiaoxiao", "666"); } /** *2、 模拟用户在线进行页面跳转的时候,判断是否在线 * (这个实际开发中肯定存在redis或者session中,这样数据才能共享) * 这里只是简单的做个模拟,所以暂且用普通map吧 */ public static Map onlineUser = new HashMap<>(); static{ //key值一般是每个用户的sessionID(这里表示admin用户一开始就在线) onlineUser.put("123",new User("admin","888")); } /** *3、 功能描述:用户登录接口 */ @RequestMapping(value="login", method=RequestMethod.POST) public String userLogin( @RequestParam(value="username", required=true)String username, @RequestParam(value="pwd",required=true) String pwd, HttpSession session) { //判断是否正确 String password = userMap.get(username); if (pwd.equals(password)) { User user = new User(username, pwd); String sessionId = session.getId(); //用户登陆成功就把该用户放到在线用户中... onlineUser.put(sessionId, user); //跳到群聊页面 return "redirect:/group/chat.html"; } else { return "redirect:/group/error.html"; } } /** *4、 功能描述:用于定时给客户端推送在线用户 */ @Scheduled(fixedRate = 2000) public void onlineUser() { ws.sendOnlineUser(onlineUser); } /** *5、 功能描述 群聊天接口 * message 消息体 * headerAccessor 消息头访问器,通过这个获取sessionId */ @MessageMapping("/group/chat") public void topicChat(InMessage message, SimpMessageHeaderAccessor headerAccessor){ //这个sessionId是在HttpHandShakeIntecepter拦截器中放入的 String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString(); //通过sessionID获得在线用户信息 User user = onlineUser.get(sessionId); message.setFrom(user.getUsername()); ws.sendTopicChat(message); } }
/** * WebSocket握手请求的拦截器. 检查握手请求和响应, 对WebSocketHandler传递属性 * 可以通过这个类的方法获取resuest,和response */ public class HttpHandShakeIntecepter implements HandshakeInterceptor{ //在握手之前执行该方法, 继续握手返回true, 中断握手返回false. 通过attributes参数设置WebSocketSession的属性 //这个项目只在WebSocketSession这里存入sessionID @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Mapattributes) throws Exception { System.out.println("【握手拦截器】beforeHandshake"); if(request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request; HttpSession session = servletRequest.getServletRequest().getSession(); String sessionId = session.getId(); System.out.println("【握手拦截器】beforeHandshake sessionId="+sessionId); //这里将sessionId放入SessionAttributes中, attributes.put("sessionId", sessionId); } return true; } //在握手之后执行该方法. 无论是否握手成功都指明了响应状态码和相应头(这个项目没有用到该方法) @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("【握手拦截器】afterHandshake"); if(request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)request; HttpSession session = servletRequest.getServletRequest().getSession(); String sessionId = session.getId(); System.out.println("【握手拦截器】afterHandshake sessionId="+sessionId); } } }
/** * 功能描述:频道拦截器 ,类似管道,可以获取消息的一些meta数据 */ public class SocketChannelIntecepter extends ChannelInterceptorAdapter{ /** * 在完成发送之后进行调用,不管是否有异常发生,一般用于资源清理 */ @Override public void afterSendCompletion(Message> message, MessageChannel channel, boolean sent, Exception ex) { System.out.println("SocketChannelIntecepter->afterSendCompletion"); super.afterSendCompletion(message, channel, sent, ex); } /** * 在消息被实际发送到频道之前调用 */ @Override public Message> preSend(Message> message, MessageChannel channel) { System.out.println("SocketChannelIntecepter->preSend"); return super.preSend(message, channel); } /** * 发送消息调用后立即调用 */ @Override public void postSend(Message> message, MessageChannel channel, boolean sent) { System.out.println("SocketChannelIntecepter->postSend"); StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);//消息头访问器 if (headerAccessor.getCommand() == null ) return ;// 避免非stomp消息类型,例如心跳检测 String sessionId = headerAccessor.getSessionAttributes().get("sessionId").toString(); System.out.println("SocketChannelIntecepter -> sessionId = "+sessionId); switch (headerAccessor.getCommand()) { case CONNECT: connect(sessionId); break; case DISCONNECT: disconnect(sessionId); break; case SUBSCRIBE: break; case UNSUBSCRIBE: break; default: break; } } /** * 连接成功 */ private void connect(String sessionId){ System.out.println("connect sessionId="+sessionId); } /** * 断开连接 */ private void disconnect(String sessionId){ System.out.println("disconnect sessionId="+sessionId); //用户下线操作 UserChatController.onlineUser.remove(sessionId); } }
既然写了两个拦截器,那么肯定需要在配置信息里去配置它们。
@EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** *配置基站 */ public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/endpoint-websocket").addInterceptors(new HttpHandShakeIntecepter()).setAllowedOrigins("*").withSockJS(); } /** * 配置消息代理(中介) * enableSimpleBroker 服务端推送给客户端的路径前缀 * setApplicationDestinationPrefixes 客户端发送数据给服务器端的一个前缀 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic","/chat"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors( new SocketChannelIntecepter()); } @Override public void configureClientOutboundChannel(ChannelRegistration registration) { registration.interceptors( new SocketChannelIntecepter()); } }
登陆页面和群聊页面就不细聊,贴上代码就好。
index.html
index.html
chat.html
chat.html
app.js
var stompClient = null; //一加载就会调用该方法 function connect() { var socket = new SockJS('/endpoint-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); //订阅群聊消息 stompClient.subscribe('/topic/chat', function (result) { showContent(JSON.parse(result.body)); }); //订阅在线用户消息 stompClient.subscribe('/topic/onlineuser', function (result) { showOnlieUser(JSON.parse(result.body)); }); }); } //断开连接 function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } //发送聊天记录 function sendContent() { stompClient.send("/app/group/chat", {}, JSON.stringify({'content': $("#content").val()})); } //显示聊天记录 function showContent(body) { $("#record").append(""); } //显示实时在线用户 function showOnlieUser(body) { $("#online").html(" " + body.content + " "+new Date(body.time).toLocaleTimeString()+" "); } $(function () { connect();//自动上线 $("form").on('submit', function (e) { e.preventDefault(); }); $( "#disconnect" ).click(function() { disconnect(); }); $( "#send" ).click(function() { sendContent(); }); }); " + body.content + " "+new Date(body.time).toLocaleTimeString()+"
gitHub源码:https://github.com/yudiandemingzi/websocket
我只是偶尔安静下来,对过去的种种思忖一番。那些曾经的旧时光里即便有过天真愚钝,也不值得谴责。毕竟,往后的日子,还很长。不断鼓励自己,
天一亮,又是崭新的起点,又是未知的征程(上校1)