1. 常见的消息推送方式
2.WebSocket API
3.基于WebSocket的实战(实时聊天室)
这里以解析后端代码为主,前端不作为重点,若想复现项目,请从作者的仓库中拉取代码
WebSocket-chatRoom: 基于WebSocket协议实现一个简单的聊天室
项目架构如下:
最后一个为@onclose(图上写错了)
3.1 基础环境搭建
Resut实体类
@Data public class Result { private boolean flag; private String message; }
用户信息实体类
@Data public class User { private String userId; private String username; private String password; }
用户登录与获取用户信息的实现
@RestController @RequestMapping("user") public class UserController { /** * 登陆 * @param user 提交的用户数据,包含用户名和密码 * @param session * @return */ @PostMapping("/login") public Result login(@RequestBody User user, HttpSession session) { Result result = new Result(); if(user != null && "123".equals(user.getPassword())) { result.setFlag(true); //将数据存储到session对象中 session.setAttribute("user",user.getUsername()); } else { result.setFlag(false); result.setMessage("登陆失败"); } return result; } /** * 获取用户名 * @param session * @return */ @GetMapping("/getUsername") public String getUsername(HttpSession session) { String username = (String) session.getAttribute("user"); return username; } }
3.2 WebSocket配置
WebsocketConfig
配置类
@Configuration public class WebsocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
@Configuration
注解:表明这个类是一个配置类,它可以包含一个或多个@Bean
方法,这些方法返回的对象会被Spring容器管理。serverEndpointExporter()
方法:该方法被@Bean
注释标记,表示它返回的对象(在这个例子中是ServerEndpointExporter
实例)将被Spring容器作为bean管理。ServerEndpointExporter
的作用是扫描并注册所有使用了@ServerEndpoint
注解的类,使得它们可以处理WebSocket连接。
GetHttpSessionConfig
配置类
/** * ServerEndpointConfig.Configurator 是一个抽象类,提供了在WebSocket握手阶段进行自定义配置的机会。通过继承这个类,我们可以在握手过程中执行额外的操作,如获取HTTP会话等。 */ @Configuration public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { //获取HttpSession对象 HttpSession httpSession= (HttpSession) request.getHttpSession(); //将httpSession对象保存起来 //我们将获取到的HttpSession对象存储在UserProperties集合中。这样做的目的是为了让后续的WebSocket消息处理方法能够访问到这个HTTP会话信息,例如用于身份验证或会话跟踪。 sec.getUserProperties().put(HttpSession.class.getName(),httpSession); } }
- 继承自
ServerEndpointConfig.Configurator
:ServerEndpointConfig.Configurator
是一个抽象类,提供了在WebSocket握手阶段进行自定义配置的机会。通过继承这个类,我们可以在握手过程中执行额外的操作,如获取HTTP会话等。 modifyHandshake
方法:这是Configurator
类中的一个重写方法,它允许我们在WebSocket握手阶段对连接进行修改。具体来说,在这里我们做了两件事:- 获取
HttpSession
对象:通过调用request.getHttpSession()
,我们可以从握手请求中获得当前的HTTP会话。这在需要将WebSocket连接与特定用户的HTTP会话关联起来时非常有用。 - 将
HttpSession
对象保存到UserProperties
中:通过sec.getUserProperties().put(HttpSession.class.getName(), httpSession)
,我们将获取到的HttpSession
对象存储在UserProperties
集合中。这样做的目的是为了让后续的WebSocket消息处理方法能够访问到这个HTTP会话信息,例如用于身份验证或会话跟踪。
- 获取
总结
这两段配置共同实现了以下功能:
- 自动扫描并注册所有使用了
@ServerEndpoint
注解的类,使其成为WebSocket端点。 - 在WebSocket握手阶段,获取当前用户的HTTP会话信息,并将其与WebSocket连接关联起来,以便在后续的消息交换中可以利用这些会话数据。
这种方式特别适用于需要在WebSocket通信中保持用户状态的应用场景,比如实时聊天应用、在线游戏等。通过这种方式,开发者可以确保WebSocket连接与用户的HTTP会话紧密关联,从而实现更安全、个性化的服务。
3.3 消息的处理
定义两个消息对象
/** * 用于封装浏览器发送给服务端的消息数据 */ @Data public class Message { private String toName; private String message; }
/** * 用来封装服务端给浏览器发送的消息数据 */ @Data public class ResultMessage { private boolean isSystem; private String fromName; private Object message;//如果是系统消息是数组 }
定义消息的工具类
public class MessageUtils { /** * @param isSystemMessage 是否是系统消息。只有广播消息才是系统消息。如果是私聊消息的话,就不是系统消息 * @param fromName 给谁发消息,如果是系统消息的话,这个参数不需要指定 * @param message 消息的具体内容 * @return */ public static String getMessage(boolean isSystemMessage,String fromName, Object message) { ResultMessage result = new ResultMessage(); result.setSystem(isSystemMessage); result.setMessage(message); if(fromName != null) { result.setFromName(fromName); } return JSON.toJSONString(result); } }
定义消息的处理类
@ServerEndpoint(value="/chat",configurator = GetHttpSessionConfig.class) @Component public class ChatEndpoint { //开一个线程安全的Map private static final Map<String,Session> onlineUsers=new ConcurrentHashMap<>(); private HttpSession httpSession; /** * 广播系统消息 * @param message */ private void broadcastAllUsers(String message){ try { //遍历map Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet(); for (Map.Entry<String, Session> entry : entries) { //获取到所有用户对应的session对象 Session session=entry.getValue(); //发送对象 session.getBasicRemote().sendText(message); } }catch (IOException e) { e.printStackTrace(); } } /** * 返回所有在线用户的用户名集合。 * @return */ public Set getFriends() { Set<String> set = onlineUsers.keySet(); return set; } /** * 建立WebSocket连接后调用 * @param session */ @OnOpen public void onOpen(Session session, EndpointConfig config){ //config与配置类中的sec是一个对象 //将session保存 this.httpSession=(HttpSession)config.getUserProperties().get(HttpSession.class.getName()); String user=(String) this.httpSession.getAttribute("user"); onlineUsers.put(user,session); //广播消息,将登录的所有用户推给所有的用户 String message = MessageUtils.getMessage(true, null,getFriends()); broadcastAllUsers(message); } /** * 浏览器发送消息到服务端,该方法被调用 * @param message */ @OnMessage public void onMessage(String message) throws IOException { //将消息推送给指定的用户 Message msg = JSON.parseObject(message, Message.class); //获取消息接收方的用户名 String toName = msg.getToName(); String mess=msg.getMessage(); //获取消息接收方用户对象的session Session session=onlineUsers.get(toName); String user=(String) httpSession.getAttribute("user"); String message1 = MessageUtils.getMessage(false, user,mess); session.getBasicRemote().sendText(message1); } /** * 断开WebSocket连接时被调用 * @param session */ @OnClose public void onClose(Session session){ //从onlineUsers中移除当前用户的session对象(用户退出) String user=(String) httpSession.getAttribute("user"); onlineUsers.remove(user); //通知其他所有用户,当前用户下线 String message = MessageUtils.getMessage(true, null,getFriends()); broadcastAllUsers(message); } }
下面是对代码的每一步思路进行详细解释:
类定义与注解
@ServerEndpoint(value="/chat", configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {
@ServerEndpoint(value="/chat", configurator = GetHttpSessionConfig.class)
: 这个注解表明ChatEndpoint
类是一个WebSocket端点,监听路径为/chat
。configurator = GetHttpSessionConfig.class
指定了一个配置器,用于获取HTTP会话信息。@Component
: 这个注解将ChatEndpoint
类声明为Spring的一个组件,这样Spring容器可以自动发现并管理它。
成员变量
private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();
private HttpSession httpSession;
onlineUsers
: 一个线程安全的Map,键是用户名,值是对应的WebSocket Session对象。使用ConcurrentHashMap
确保多线程环境下的安全性。httpSession
: 存储当前用户的HTTP会话对象,用于获取用户的相关信息(如用户名)。
广播系统消息方法
private void broadcastAllUsers(String message) {
try {
Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();
for (Map.Entry<String, Session> entry : entries) {
Session session = entry.getValue();
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
- 目的: 向所有在线用户广播一条消息。
- 步骤:
- 获取
onlineUsers
中所有的条目(即用户名和Session的映射)。 - 遍历每个条目,获取对应的Session对象。
- 使用
session.getBasicRemote().sendText(message)
向每个用户发送消息。 - 捕获并打印可能发生的IO异常。
- 获取
获取在线好友列表方法
public Set getFriends() {
Set<String> set = onlineUsers.keySet();
return set;
}
- 目的: 返回当前在线用户的用户名集合。
- 步骤:
- 调用
onlineUsers.keySet()
获取所有在线用户的用户名集合。 - 返回这个集合。
- 调用
处理连接建立事件方法
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String user = (String) this.httpSession.getAttribute("user");
onlineUsers.put(user, session);
String message = MessageUtils.getMessage(true, null, getFriends());
broadcastAllUsers(message);
}
- 目的: 当客户端与服务器建立WebSocket连接时执行的操作。
- 步骤:
- 从
config.getUserProperties()
中获取HTTP会话对象,并赋值给this.httpSession
。 - 从
httpSession
中获取当前用户的用户名。 - 将用户名和对应的Session对象存入
onlineUsers
中。 - 构造一条包含当前在线用户信息的消息。
- 调用
broadcastAllUsers(message)
向所有在线用户广播这条消息。
- 从
处理接收到的消息方法
@OnMessage
public void onMessage(String message) throws IOException {
Message msg = JSON.parseObject(message, Message.class);
String toName = msg.getToName();
String mess = msg.getMessage();
Session session = onlineUsers.get(toName);
String user = (String) httpSession.getAttribute("user");
String message1 = MessageUtils.getMessage(false, user, mess);
session.getBasicRemote().sendText(message1);
}
- 目的: 当客户端发送消息到服务器时执行的操作。
- 步骤:
- 解析客户端发送的JSON格式的消息,将其转换为
Message
对象。 - 从
Message
对象中提取接收方的用户名toName
和实际消息内容mess
。 - 根据接收方的用户名从
onlineUsers
中获取对应的Session对象。 - 从
httpSession
中获取当前发送消息的用户的用户名。 - 构造一条包含发送者和消息内容的消息。
- 使用
session.getBasicRemote().sendText(message1)
将消息发送给接收方。
- 解析客户端发送的JSON格式的消息,将其转换为
处理连接关闭事件方法
@OnClose
public void onClose(Session session) {
String user = (String) httpSession.getAttribute("user");
onlineUsers.remove(user);
String message = MessageUtils.getMessage(true, null, getFriends());
broadcastAllUsers(message);
}
- 目的: 当客户端断开WebSocket连接时执行的操作。
- 步骤:
- 从
httpSession
中获取当前用户的用户名。 - 从
onlineUsers
中移除该用户的Session对象。 - 构造一条包含当前在线用户信息的消息。
- 调用
broadcastAllUsers(message)
向所有在线用户广播这条消息。
- 从
总结
整个类的核心功能是维护一个在线用户列表,并在用户上线、下线或发送消息时进行相应的处理和通知。通过这些方法,多个客户端之间可以通过服务器转发消息,实现简单的即时通讯功能。