1、WebSocket 前置基础

1.1 WebSocket 与 Http对比

登录模块 (1)

  • 连接方式
    • HTTP: HTTP 是基于请求-响应模式的协议。客户端发送请求,服务器响应请求。每次请求都需要建立新的连接,完成数据传输后连接断开。
    • WebSocket: WebSocket 是一种全双工协议,建立连接后,客户端和服务器之间可以进行双向通信,无需每次都建立新的连接。
  • 数据传输方式
    • HTTP: HTTP 使用明文传输数据,数据以消息的形式进行传输。
    • WebSocket: WebSocket 使用二进制帧传输数据,可以有效地减少数据传输量。
  • 效率
    • HTTP: HTTP 的效率受到连接建立和关闭的开销影响,尤其是在频繁请求的情况下。
    • WebSocket: WebSocket 由于保持连接,可以避免每次请求的连接建立和关闭开销,效率更高。
  • 实时性
    • HTTP: HTTP 无法实现实时通信,因为每次请求都需要等待响应。
    • WebSocket: WebSocket 可以实现实时双向通信,适合需要实时数据交互的应用场景。

1.2 WebSocket 工作原理

  • 握手阶段: 客户端向服务器发起一个 WebSocket 握手请求,请求升级 HTTP 连接为 WebSocket 连接。握手请求包含一些特定的头部信息,例如 Connection: Upgrade, Upgrade: websocket, Sec-WebSocket-Key 等。
  • 建立连接: 服务器接收到握手请求后,会进行验证,如果验证成功,会发送一个 WebSocket 握手响应,表明连接成功建立。
  • 数据传输: 连接建立后,客户端和服务器就可以通过 WebSocket 进行双向的数据传输了。数据传输采用帧的形式,每个帧包含 opcode、掩码、数据长度、数据等信息。
  • 关闭连接: 当客户端或服务器一方需要关闭连接时,可以通过发送一个关闭帧来关闭连接。

1.3 WebSocket 应用场景

  • 实时聊天: WebSocket 非常适合用来构建实时聊天应用,因为可以实时地将消息传递给所有参与者。
  • 游戏开发: 在多人在线游戏中,WebSocket 可以用来实时同步游戏状态、玩家位置、聊天信息等数据。
  • 股票行情: WebSocket 可以用来实时推送股票行情信息,让用户能够及时了解最新的市场动态。
  • 数据监控: WebSocket 可以用来实时监控数据,例如网站流量、服务器状态、传感器数据等。

2、代码案例

源码地址 ---> websocket: websocket demo (gitee.com)

2.1 前端代码

<!DOCTYPE html>
<html lang="zh">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<head>
    <title>WebSocket 页面</title>
</head>
<body>
WebSocket Simple Demo

<p>登录用户名:</p>
<input type="text" id="usernameInput" />
<button onclick="login()" id = "login">登录</button>
<button onclick="logout()" id = "logout">退出登录</button>
<p>输入要发送的消息(eg. tom:msg 给tom发送msg):</p>
<input type="text" id="messageInput" />
<button onclick="sendMessage()" id="send">发送</button>

<p>接收到的消息:</p>
<button onclick="clearMsg()">清空消息</button>
<div id="messageOutput"></div>

<script>
    document.getElementById('login').disabled = false;
    document.getElementById('logout').disabled = true;
    document.getElementById('send').disabled = true;
    let ws; // 全局变量存储 WebSocket 连接

    // 连接 WebSocket
    function login() {
        const username = document.getElementById('usernameInput').value;
        if (username === null || username.trim() === '') {
            alert('请输入用户名!');
            return
        } else {
            // 用户名有效,进行其他操作
            console.log(`用户名是:${username}`);
        }
        document.getElementById('login').disabled = true;
        document.getElementById('logout').disabled = false;
        document.getElementById('send').disabled = false;
        ws = new WebSocket('ws://localhost:8080/simple/' + username);

        // 连接成功
        ws.onopen = function(event) {
            alert("登录成功")
        };
        ws.onclose = function(event) {
            document.getElementById('usernameInput').value = '';
            document.getElementById('login').disabled = false;
            document.getElementById('logout').disabled = true;
            document.getElementById('send').disabled = true;
        }

        // 收到消息
        ws.onmessage = function(event) {
            const message = event.data;
            document.getElementById('messageOutput').innerHTML += message + '<br>';
        };

        // 连接错误
        ws.onerror = function(event) {
            console.error('WebSocket 连接错误!');
        };
    }

    function clearMsg() {
        document.getElementById('messageOutput').innerHTML  = '';
    }

    // 退出登录
    function logout() {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.close()  // 连接关闭
            document.getElementById('usernameInput').value = '';
            document.getElementById('login').disabled = false;
            document.getElementById('logout').disabled = true;
            document.getElementById('send').disabled = true;
            alert("退出成功")
        } else {
            console.warn('WebSocket 连接未建立,无法发送消息');
        }
    }

    // 发送消息
    function sendMessage() {
        const message = document.getElementById('messageInput').value;
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(message);
            document.getElementById('messageInput').value = '';
        } else {
            console.warn('WebSocket 连接未建立,无法发送消息');
        }
    }
</script>
</body>
</html>

2.2 spring后端代码

maven依赖

<properties>
    <java.version>17</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncodi
    <spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

config 配置

package com.xy.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author 谢阳
 * @version 1.0
 * @date 2024-07-11 14:35
 * @description 自动注册使用@ServerEndpoint注解
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocket类

package com.xy.websocket.ws;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author 谢阳
 * @version 1.0
 * @date 2024-07-11 14:41
 * @description
 */
@ServerEndpoint("/simple/{name}")
@Slf4j
@Component
@EnableScheduling
public class SimpleEndpoint {

    private static final Map<String, Session> sessions = new HashMap<>();

    private final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @OnOpen
    public void onOpen(Session session,@PathParam("name") String name) {
        sessions.put(name, session);
        log.info("注册用户 [{}],同时在线数 [{}]",name,sessions.size());
    }

    @OnMessage
    public void onMessage(String message, Session session,@PathParam("name") String name) {
        String[] split = message.split(":");
        if (split.length == 2) {
            Session sendSession = sessions.get(split[0]);
            if (sendSession != null) {
                LocalDateTime now = LocalDateTime.now();
                String format = DTF.format(now);
                sendSession.getAsyncRemote().sendText(format + " " + name + ":" + split[1]);
            } else {
                session.getAsyncRemote().sendText("用户未上线");
            }
        } else{
            session.getAsyncRemote().sendText("服务端收到消息");
        }
    }

    @OnClose
    public void onClose(Session session, @PathParam("name") String name)  throws IOException {
        int oldSize = sessions.size();
        Session remove = sessions.remove(name);
        log.info("在线数 [{}],退出用户 [{}],剩余在线 [{}] 个",oldSize,name,sessions.size());
        remove.close();
    }

    @Scheduled(cron = "*/20 * * * * ?")
    public void task() {
        LocalDateTime now = LocalDateTime.now();
        String format = DTF.format(now);
        sessions.forEach((k, v) -> {
            String string = Arrays.stream(sessions.keySet().toArray()).collect(Collectors.toList()).toString();
            v.getAsyncRemote().sendText(format + " 同时在线数 [" + sessions.size()+ "] 个" + ", 在线用户 " + string);
        });
    }
}