文章目录

一、WebSocket概述1.1 什么是WebSocket1.2 WebSocket的生命周期事件

二、WebSocket实现群聊功能2.1 服务端:注解式端点事件处理2.2 客户端:JavaScript中的WebSocket对象

三、Session、Cookie实现24小时内自动识别用户四、实验中遇到的一些问题及其解决4.1 WebSocket获取httpSession的方法4.2 WebSocket获取httpSession为空(Session不一致)的问题

实验源代码参考资料

一、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> entries = clients.entrySet();

for (Map.Entry 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://blog.csdn.net/Zany540817349/article/details/90210075 https://blog.csdn.net/csu_passer/article/details/78536060

精彩文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。