一、WebSocket概述
1.1 什么是WebSocket
WebSocket是一种在单个TCP连接上进行全双工通信的网络协议。它是为了在Web浏览器和Web服务器之间提供实时、双向的通信而设计的。传统的HTTP协议是一种单向通信协议,客户端发送请求,服务器响应,然后连接就关闭了。而WebSocket允许在客户端和服务器之间建立持久连接,使得双方可以通过该连接随时发送数据。
WebSocket协议通过在HTTP握手阶段使用Upgrade头来升级连接,使其成为全双工通信通道。一旦升级完成,WebSocket连接就保持打开状态,允许双方在任何时候发送数据。
WebSocket协议的特点包括:
- 全双工通信: 客户端和服务器之间可以同时发送和接收数据,而不需要等待响应。
- 低延迟: 由于连接保持打开状态,可以更快地传输数据,适用于实时性要求较高的应用,如在线游戏、聊天应用等。
- 跨域支持: 与AJAX请求不同,WebSocket允许跨域通信,提供更大的灵活性。
WebSocket 端点通常会触发一些生命周期事件,这些事件可以用于处理数据、管理连接的状态等。
1.2 WebSocket的生命周期事件
- onOpen 事件: 在端点建立新WebSocket连接时并且在任何其他事件发生之前,将触发onOpen 事件。在这个事件中,可以执行一些初始化操作,例如记录连接信息、添加到连接池等。
- onMessage 事件: 当端点接收到客户端发送的消息时,将触发onMessage 事件。在这个事件中,可以处理接收到的消息并根据需要做出相应的反应。
- onError 事件: 当在 WebSocket 连接期间发生错误时,将触发onError事件。在这个事件中可以处理错误情况。
- onClose 事件: 当连接关闭时,将触发onClose事件。在这个事件中可以执行一些清理工作,例如从连接池中移除连接、记录连接关闭信息等。
二、WebSocket实现群聊功能
2.1 服务端:注解式端点事件处理
在服务端使用@ServerEndpoint注解将 Java 类声明成 WebSocket 服务端端点。
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
@Component // SpringBoot 的组件注解
public class ChatServer
对于注解式服务端端点,WebSocket API中的生命周期事件要求使用以下方法级注解:@OnOpen @OnMessage @OnError @OnClose。
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
@Component
public class ChatServer {
// WebSocker 生命周期函数: 连接建立时调用
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 从 EndpointConfig 中获取之前从握手时获取的 httpSession
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String userName = (String) this.httpSession.getAttribute("username");
// 保存已登录用户 session
clients.put(userName, session);
log.info(userName + "已与服务器建立连接");
}
// WebSocker 生命周期函数: 收到消息时调用
@OnMessage
public void onMessage(String message) {
log.info("服务器收到消息: " + message);
// 服务器群发消息
groupSend(message);
}
// WebSocker 生命周期函数: 连接断开时调用
@OnClose
public void onClose() {
String userName = (String) this.httpSession.getAttribute("username");
clients.remove(userName);
log.info(userName + "已与服务器断开连接");
}
......
}
其中,服务端在收到消息时,在OnMessage事件中向客户端群发消息
// WebSocker 生命周期函数: 收到消息时调用
@OnMessage
public void onMessage(String message) {
log.info("服务器收到消息: " + message);
// 服务器群发消息
groupSend(message);
}
private void groupSend(String message) {
// 遍历所有连接,向客户端群发消息
message = this.httpSession.getAttribute("username") + ": " + message;
Set<Map.Entry<String, Session>> entries = clients.entrySet();
for (Map.Entry<String, Session> client : entries) {
Session session = client.getValue();
try {
session.getBasicRemote().sendText(message); // 发送消息
} catch (IOException e) {
}
}
}
2.2 客户端:JavaScript中的WebSocket对象
在客户端使用JavaScript的WebSocket对象作为客户端端点
// new WebSocket("ws://url")
ws = new WebSocket("ws://localhost:8848/chat");
对于JavaScript的WebSocket对象,其生命周期事件为onopen,onmessage,onerror和onclose。
ws.onmessage = function (msg) {
let message = msg.data; // 获取服务端发送的信息
// 将消息显示到页面中
let li = document.createElement('li');
li.textContent = message;
let messages = document.getElementById('messages');
messages.appendChild(li);
window.scrollTo(0, document.body.scrollHeight);
}
在客户端使用WebSocket对象的send(message)方法向服务端发送消息
三、Session、Cookie实现24小时内自动识别用户
在服务端登录验证的handler中,创建携带验证信息的Cookie(这里图方便直接携带了用户名,实际应该使用根据用户UID加密过的token)并返回给客户端浏览器,设定有效期为24h。
同时,在会话域Session 中保存用户名以便 WebSocket 获取(Servlet在新建Session时也会返回给客户端带有JSESSIONID的Cookie):
@RestController
public class UserController {
@PostMapping("/login")
public String login(@RequestBody UserEntity user, HttpServletRequest request, HttpServletResponse response) {
if(loginCheck(user)) {
Cookie cookie = new Cookie("username", user.getUserName());
cookie.setMaxAge(24 * 60 * 60); // 设置 Cookie 的有效时间为 24h
response.addCookie(cookie);
// 在 session 中设置 userName 以便 WebSocket 获取
request.getSession().setAttribute("username", user.getUserName());
return "success";
} else {
return "failed";
}
}
......
}
此后24h内,客户端浏览器访问服务端时会携带以上Cookie,即使会话连接断开,服务端也可以根据该Cookie直接验证用户信息并重新在Session中保存,实现24h内用户免登录。若会话未断开,直接从Session中即可获取用户信息。
// 拦截器:登录校验, 不通过则跳转到登录界面
@Component
public class LoginProtectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 先使用 session 进行登录校验
String username = (String) request.getSession().getAttribute("username");
if(username != null){
return true; // 放行
}
// session 校验失败则用 cookie校验
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
if("username".equals(cookie.getName())){
// 根据 cookie获取 userName
String userName = cookie.getValue();
// 在 session 中设置 userName
request.getSession().setAttribute("username", userName);
return true; // 放行
}
}
// 校验失败 跳转到登录界面
response.sendRedirect("/login.html");
return false;
}
}
上面是在拦截器中进行登陆验证,需要对拦截器进行配置。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginProtectInterceptor loginProtectInterceptor;
// 登录验证拦截器配置
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 访问聊天室前需要登录验证
registry.addInterceptor(loginProtectInterceptor).addPathPatterns("/chat.html");
}
}
四、实验中遇到的一些问题及其解决
4.1 WebSocket获取httpSession的方法
实验中需获取Session中的用户信息,由于WebSocket与Http协议的不同,故需要在WebSocket中故在获取HttpSession,这里参考了以下链接中的方法。
https://blog.csdn.net/Zany540817349/article/details/90210075
在@ServerEndpoint注解的源代码中,可以看到要求一个ServerEndpointConfig接口下的Configurator子类,该类中有个modifyHandshake方法,这个方法可以修改在握手时的操作,将httpSession加进webSocket的配置中。
因此继承这个ServerEndpointConfig.Configurator子类,重写其modifyHandshake方法:
/** 继承 ServerEndpointConfig.Configurator 类
* 重写其中的 modifyHandshake 方法
* 在建立连接时将当前会话的 httpSession 加入到 webSocket 的 Server端的配置中
*/
@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession=(HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
将继承类加入到@ServerEndpoint注解的configurator属性中,这样即可在EndpointConfig中获取HttpSession:
@Component
@ServerEndpoint(value = "/chat", configurator= GetHttpSessionConfigurator.class)
public class ChatServer {
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// 从 EndpointConfig 中获取之前从握手时获取的 httpSession
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
String userName = (String) this.httpSession.getAttribute("username");
......
4.2 WebSocket获取httpSession为空(Session不一致)的问题
4.1中的获取httpSession的方法是正确的,但是在modifyHandshake中getHttpSession()时会报空指针异常,这里参考了以下链接,发现是服务端url写错。
https://blog.csdn.net/csu_passer/article/details/78536060
在前端连接WebSocket的时候,我的代码是这样的:
new WebSocket("ws://127.0.0.1:8848/chat");
但是浏览器的地址栏是
http://localhost:8848/chat.html
链接中解释说如果不使用同一个host,则会创建不同的连接请求,将WebSocket中服务端地址修改为与浏览器地址栏一致,则可以正确获取到httpSession。
new WebSocket("ws://localhost:8848/chat");
实验源代码
https://gitee.com/amadeuswyk/ustc-courses-net-web-socket/tree/master/文章来源:https://www.toymoban.com/news/detail-774095.html
参考资料
https://blog.csdn.net/Zany540817349/article/details/90210075
https://blog.csdn.net/csu_passer/article/details/78536060文章来源地址https://www.toymoban.com/news/detail-774095.html
到了这里,关于使用WebSocket方式能将群聊信息实时群发给所有在线用户的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!