使用WebSocket方式能将群聊信息实时群发给所有在线用户

一、WebSocket概述

1.1 什么是WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的网络协议。它是为了在Web浏览器和Web服务器之间提供实时、双向的通信而设计的。传统的HTTP协议是一种单向通信协议,客户端发送请求,服务器响应,然后连接就关闭了。而WebSocket允许在客户端和服务器之间建立持久连接,使得双方可以通过该连接随时发送数据。

WebSocket协议通过在HTTP握手阶段使用Upgrade头来升级连接,使其成为全双工通信通道。一旦升级完成,WebSocket连接就保持打开状态,允许双方在任何时候发送数据。

WebSocket协议的特点包括:

  1. 全双工通信: 客户端和服务器之间可以同时发送和接收数据,而不需要等待响应。
  2. 低延迟: 由于连接保持打开状态,可以更快地传输数据,适用于实时性要求较高的应用,如在线游戏、聊天应用等。
  3. 跨域支持: 与AJAX请求不同,WebSocket允许跨域通信,提供更大的灵活性。

WebSocket 端点通常会触发一些生命周期事件,这些事件可以用于处理数据、管理连接的状态等。

1.2 WebSocket的生命周期事件

  1. onOpen 事件: 在端点建立新WebSocket连接时并且在任何其他事件发生之前,将触发onOpen 事件。在这个事件中,可以执行一些初始化操作,例如记录连接信息、添加到连接池等。
  2. onMessage 事件: 当端点接收到客户端发送的消息时,将触发onMessage 事件。在这个事件中,可以处理接收到的消息并根据需要做出相应的反应。
  3. onError 事件: 当在 WebSocket 连接期间发生错误时,将触发onError事件。在这个事件中可以处理错误情况。
  4. 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://blog.csdn.net/Zany540817349/article/details/90210075
https://blog.csdn.net/csu_passer/article/details/78536060

  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用Java实现WebSocket群聊,可以使用Java API中的javax.websocket包。具体实现步骤如下: 1. 创建一个WebSocket服务器端点类,实现javax.websocket.Endpoint接口,并重写onOpen、onClose、onError、onMessage等方法。 2. 在onOpen方法中,获取Session对象,并将其存储到一个集合中,用于后续广播消息。 3. 在onMessage方法中,遍历存储Session的集合,向每个Session发送消息。 4. 在onClose方法中,从存储Session的集合中移除关闭的Session。 下面是一个简单的示例代码: ```java import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.MessageHandler; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; @ServerEndpoint("/chat") public class ChatEndpoint extends Endpoint { private static Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>()); @Override public void onOpen(Session session, EndpointConfig config) { sessions.add(session); session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String message) { broadcast(message); } }); } @Override public void onClose(Session session, CloseReason closeReason) { sessions.remove(session); } @Override public void onError(Session session, Throwable throwable) { throwable.printStackTrace(); } private static void broadcast(String message) { for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值