webSocket快速入门

webSocket快速入门

一、WebSocket 是什么?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。

简单说,它能让客户端(浏览器)和服务器之间实时双向通信。

对比传统 HTTP

|--------|-----------|-----------------|
| 特性 | HTTP | WebSocket |
| 连接方式 | 请求-响应 | 持久连接 |
| 通信方向 | 客户端 → 服务端 | 双向 |
| 实时性 | 差,需要轮询 | 极好 |
| 传输协议 | HTTP/1.1 | ws:// 或 wss:// |
| 应用场景 | 普通网页请求 | 实时聊天、通知、监控数据推送等 |

二、Vue + Spring Boot 实现 WebSocket (基础)

1 springboot

依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-websocket</artifactId>

</dependency>

编写 WebSocket 配置类

复制代码
@Configuration
@EnableWebSocket  // 开启websocket
public class WebSocketConfig implements WebSocketConfigurer {
// 注册websocket处理器
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/ws")
                .setAllowedOrigins("*"); // 允许跨域
    }
}

编写处理器

复制代码
// 自定义websocket处理器
public class MyWebSocketHandler extends TextWebSocketHandler {
    // 保存所有连接的session
    private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
    // 当有新的连接时,将session添加到sessions中
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        System.out.println("新连接:" + session.getId());
    }
    // 处理消息
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String msg = message.getPayload();
        System.out.println("收到消息:" + msg);
        // 回复客户端
        session.sendMessage(new TextMessage("服务器收到:" + msg));
    }
    // 连接关闭时
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
        System.out.println("连接关闭:" + session.getId());
    }

    // 向所有连接推送消息
    public static void broadcast(String message) throws Exception {
        for (WebSocketSession session : sessions) {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(message));
            }
        }
    }
}

创建控制类

复制代码
@RestController
@RequestMapping("/api/test")
public class TestController {

    @GetMapping("/push")
    public String pushMessage(@RequestParam String msg) throws Exception {
        MyWebSocketHandler.broadcast("服务端推送:" + msg);
        return "ok";
    }
}

2 vue

创建工具类

复制代码
let ws = null;
let reconnectTimer = null;
let url = "ws://localhost:8081/ws";

function createWebSocket(onMessage) {
    if (ws) return; // 防止重复创建
    // 创建 WebSocket 连接
    ws = new WebSocket(url);

    ws.onopen = () => {
        console.log("WebSocket 已连接");
    };
    // 接收服务器消息
    ws.onmessage = (event) => {
        if (onMessage) onMessage(event.data);
    };

    ws.onclose = () => {
        console.log(" WebSocket 已关闭,尝试重连...");
        ws = null;
        reconnect(); // 自动重连
    };

    ws.onerror = (err) => {
        console.error("WebSocket 出错:", err);
        ws.close();
    };
}
// 自动重连
function reconnect() {
    if (reconnectTimer) return;
    reconnectTimer = setTimeout(() => {
        createWebSocket();
        reconnectTimer = null;
    }, 3000);
}
// 发送消息
function sendMsg(msg) {
    if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(msg);
    } else {
        console.warn("WebSocket 未连接,消息未发送");
    }
}
// 关闭 WebSocket 连接
function closeWebSocket() {
    if (ws) {
        ws.close();
        ws = null;
    }
}

export default {
    createWebSocket,
    sendMsg,
    closeWebSocket
};

写一个简单的页面来调用

复制代码
<template>
  <div>
    <h2>WebSocket 测试</h2>
    <p>收到消息:{{ message }}</p>
    <input v-model="input" />
    <button @click="send">发送</button>
  </div>
</template>

<script>
import wsService from "@/utils/websocket";

export default {
  data() {
    return {
      message: "",
      input: ""
    };
  },
  mounted() {
    wsService.createWebSocket((msg) => {
      this.message = msg;
    });
  },
  beforeUnmount() {
    wsService.closeWebSocket();
  },
  methods: {
    send() {
      wsService.sendMsg(this.input);
      this.input = "";
    }
  }
};
</script>

注意:运行后, WebSocket 服务地址为:
ws://localhost:8081/ws

我们可以测试一下,启动vue、springboot项目:

可以看到前后端控制台都打印出了websocket连接成功的日志。

发送消息后后端也能及时的收到消息:

三、websocket 高级封装

在一般的项目中通常会:

  1. 使用 Pinia/Vuex 管理 WebSocket 状态(是否在线、消息队列等);
  2. 支持 心跳检测
  3. 支持 断线重连 + 消息缓存
  4. 区分不同业务消息类型(如 chat, notice, system)。

这里我们为了直接支持

a.自动重连

b.心跳检测

c.区分业务消息类型(如 chat, notice, system)

d.统一管理连接状态

来修改一下websocket的工具类封装:

复制代码
let ws = null;
let heartbeatTimer = null;
let reconnectTimer = null;
let reconnectAttempts = 0;

const HEARTBEAT_INTERVAL = 5000; // 心跳间隔 5s
const MAX_RECONNECT_ATTEMPTS = 10;
const WS_URL = "ws://localhost:8081/ws"; // 后端地址

// === 初始化 WebSocket 连接 ===
function createWebSocket(onMessageCallback) {
    if (ws && ws.readyState === WebSocket.OPEN) return;

    ws = new WebSocket(WS_URL);

    ws.onopen = () => {
        console.log("WebSocket 连接成功");
        reconnectAttempts = 0;
        startHeartbeat();
    };

    ws.onmessage = (event) => {
        try {
            const data = JSON.parse(event.data);
            handleMessage(data, onMessageCallback);
        } catch (e) {
            console.warn("收到非JSON消息:", event.data);
        }
    };

    ws.onclose = () => {
        console.warn("WebSocket 已关闭,尝试重连...");
        stopHeartbeat();
        reconnect();
    };

    ws.onerror = (err) => {
        console.error("WebSocket 出错:", err);
        ws.close();
    };
}

// === 心跳机制 ===
function startHeartbeat() {
    stopHeartbeat();
    heartbeatTimer = setInterval(() => {
        send({ type: "ping" });
    }, HEARTBEAT_INTERVAL);
}

function stopHeartbeat() {
    if (heartbeatTimer) {
        clearInterval(heartbeatTimer);
        heartbeatTimer = null;
    }
}

// === 重连机制 ===
function reconnect() {
    if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
        console.error("重连次数过多,停止尝试");
        return;
    }

    reconnectAttempts++;
    const delay = reconnectAttempts * 2000; // 递增延迟
    console.log(`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`);

    reconnectTimer = setTimeout(() => {
        createWebSocket();
    }, delay);
}

// === 消息分发 ===
function handleMessage(data, callback) {
    if (!data.type) {
        console.warn("未知消息类型:", data);
        return;
    }

    switch (data.type) {
        case "chat":
            console.log("聊天消息:", data.content);
            break;
        case "notice":
            console.log("通知:", data.content);
            break;
        case "system":
            console.log("系统消息:", data.content);
            break;
        case "pong":
            console.log("心跳响应");
            break;
        default:
            console.log("其他类型消息:", data);
    }

    if (callback) callback(data);
}

// === 发送消息 ===
function send(msg) {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        console.warn("WebSocket 未连接,发送失败:", msg);
        return;
    }
    ws.send(JSON.stringify(msg));
}

// === 主动关闭 ===
function close() {
    stopHeartbeat();
    if (ws) {
        ws.close();
        ws = null;
    }
}

export default {
    createWebSocket,
    send,
    close
};

创建一个新的页面:

复制代码
<template>
  <div class="p-4">
    <h2>💬 WebSocket 聊天示例</h2>
    <p>收到的消息:{{ lastMessage }}</p>

    <div class="mt-4">
      <input v-model="message" placeholder="输入消息..." />
      <select v-model="type">
        <option value="chat">聊天</option>
        <option value="notice">通知</option>
        <option value="system">系统</option>
      </select>
      <button @click="sendMsg">发送</button>
    </div>
  </div>
</template>

<script>
import wsService from "@/utils/websocket";

export default {
  data() {
    return {
      message: "",
      type: "chat",
      lastMessage: ""
    };
  },
  mounted() {
    wsService.createWebSocket((msg) => {
      this.lastMessage = `${msg.type}: ${msg.content}`;
    });
  },
  beforeUnmount() {
    wsService.close();
  },
  methods: {
    sendMsg() {
      wsService.send({
        type: this.type,
        content: this.message,
        from: "前端用户"
      });
      this.message = "";
    }
  }
};
</script>

修改后端的处理数据方式:

复制代码
    // 处理消息
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String msg = message.getPayload();
        System.out.println("收到消息:" + msg);
/**
 *websocket高级封装
 *区分不同业务消息类型
 *支持 心跳检测
 */
        // 解析前端JOSN
        JSONObject json = new JSONObject(msg);
        String type = json.optString("type");
        // 构造回复数据
        JSONObject data = new JSONObject();
        data.put("type", type);
        data.put("content","服务器已收到:"+ json.optString("content"));

        //模拟心跳回应
        if ("ping".equals(type)) {
            data.put("type", "pong");
        }



        // 回复客户端
        session.sendMessage(new TextMessage(json.toString()));
    }

发送后可以看到:

四、广播(全员发)和私聊(发给指定userId )功能

那么问题来了,广播和私聊功能是如何在websocket中实现的?

这里,我们可通过记录连接成功的用户(也就是在线的用户)来进行,在前端拼接websocket url时,可以加上用户唯一的/id 来分辨不同的用户。。

如:

ws://localhost:8080/ws/1

这时在后端截取id来获取在线用户。

后端修改config

复制代码
@Configuration
@EnableWebSocket  // 开启websocket
public class WebSocketConfig implements WebSocketConfigurer {
// 注册websocket处理器
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/ws/{userId}")
                .setAllowedOrigins("*"); // 允许跨域
    }
}





public class MyWebSocketHandler extends TextWebSocketHandler {

    // 保存所有连接:userId -> session
    private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    // 当有新的连接时,将session添加到sessions中
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        // 获取用户ID  ws://localhost:8081/ws/1
        String path = session.getUri().getPath();
        System.out.println("新连接:" + path);
        String userId = path.substring(path.lastIndexOf("/") + 1);
        sessions.put(userId, session);
        System.out.println("新连接:" + userId + " (sessionId=" + session.getId() + ")");
    }
    // 处理消息
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String msg = message.getPayload();
        System.out.println("收到消息:" + msg);

        // 默认逻辑:回复收到的内容
//        JSONObject json = new JSONObject(msg);
//        String content = json.optString("content");
//
//        JSONObject reply = new JSONObject();
//        reply.put("content", "服务器已收到:" + content);
//        reply.put("type", "system");
//        session.sendMessage(new TextMessage(reply.toString()));
    }
    // 连接关闭时
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.entrySet().removeIf(entry -> entry.getValue().equals(session));
        System.out.println("连接关闭:" + session.getId());
    }


    // 专用方法分发


    /**
     * 广播
     */
    public static void broadcast(String from, String message,String type) throws Exception {
        JSONObject data = new JSONObject();
        data.put("from", from);
        data.put("type", type);
        data.put("content", message);
        // 发送给所有连接
        for (WebSocketSession session : sessions.values()) {
            if (session.isOpen()) {
                session.sendMessage(new TextMessage(data.toString()));
            }
        }
        System.out.println("广播消息:" + message);
    }

    /**
     * 私聊
     */
    public static void sendToUser(String from, String to, String message, String type) throws Exception {
        WebSocketSession target = sessions.get(to);
        if (target != null && target.isOpen()) {
            JSONObject data = new JSONObject();
            data.put("from", from);
            data.put("type", type);
            data.put("to", to);
            data.put("content", message);
            // 发送给指定连接
            target.sendMessage(new TextMessage(data.toString()));
            System.out.println("私聊消息:" + from + " → " + to + ":" + message);
        } else {
            System.out.println("用户 " + to + " 不在线或不存在");
        }
    }
}

控制器区分不同业务:

复制代码
@RestController
@RequestMapping("/api/test")
public class TestController {

//    @GetMapping("/push")
//    public String pushMessage(@RequestParam String msg) throws Exception {
//        MyWebSocketHandler.broadcast("服务端推送:" + msg);
//        return "ok";
//    }


    // 广播
    @PostMapping("/broadcast")
    public void broadcast(@RequestBody Map<String, String> body) throws Exception {
        MyWebSocketHandler.broadcast(body.get("from"), body.get("content"), body.get("type"));
    }
    // 私聊
    @PostMapping("/private")
    public void privateMsg(@RequestBody Map<String, String> body) throws Exception {
        MyWebSocketHandler.sendToUser(body.get("from"), body.get("to"), body.get("content"), body.get("type"));
    }
}

前端封装websocket 时,获取id并拼接:

创建广播和私聊方法:

复制代码
// WebSocket 工具模块
let ws = null; // WebSocket 实例
let heartbeatTimer = null;  // 心跳定时器
let reconnectTimer = null;  // 重连定时器
let reconnectAttempts = 0;  // 重连尝试次数

const HEARTBEAT_INTERVAL = 10000; // 心跳间隔 10s
const MAX_RECONNECT_ATTEMPTS = 10;
const WS_URL = "ws://localhost:8081/ws"; // 后端地址

// === 初始化 WebSocket 连接 ===
function createWebSocket(onMessageCallback, uid) {
    return new Promise((resolve, reject) => {
        if (!uid) {
            console.error("缺少用户ID,WebSocket无法建立连接");
            reject("缺少用户ID");
            return;
        }

        // 如果已存在连接则直接返回
        if (ws && ws.readyState === WebSocket.OPEN) {
            console.log("WebSocket 已连接");
            resolve();
            return;
        }

        // 拼装 URL
        const id = `${WS_URL}/${uid}`;
        ws = new WebSocket(id);

        ws.onopen = () => {
            console.log(`用户 ${uid} WebSocket 连接成功`);
            reconnectAttempts = 0;
            startHeartbeat();
            resolve(); // 通知外部连接成功
        };

        ws.onmessage = (event) => {
            console.log("收到消息:", event.data);
            try {
                const data = JSON.parse(event.data);
                handleMessage(data, onMessageCallback);
            } catch (e) {
                console.warn("收到非JSON消息:", event.data);
            }
        };

        ws.onclose = () => {
            console.warn("WebSocket 已关闭,尝试重连...");
            stopHeartbeat();
            reconnect(() => createWebSocket(onMessageCallback, uid));
        };

        ws.onerror = (err) => {
            console.error("WebSocket 出错:", err);
            ws.close();
            reject(err); // 通知外部连接失败
        };
    });
}

// === 心跳机制 ===
function startHeartbeat() {
    stopHeartbeat();
    heartbeatTimer = setInterval(() => {
        send({ type: "ping" });
    }, HEARTBEAT_INTERVAL);
}

// === 停止心跳 ===
function stopHeartbeat() {
    if (heartbeatTimer) {
        clearInterval(heartbeatTimer);
        heartbeatTimer = null;
    }
}

// === 重连机制 ===
function reconnect(reconnectAction) {
    if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
        console.error("重连次数过多,停止尝试");
        return;
    }

    reconnectAttempts++;
    const delay = reconnectAttempts * 2000; // 递增延迟
    console.log(`${delay / 1000}s 后重连第 ${reconnectAttempts} 次...`);

    reconnectTimer = setTimeout(() => {
        if (typeof reconnectAction === "function") {
            reconnectAction();
        }
    }, delay);
}

// === 消息分发 ===
function handleMessage(data, callback) {
    if (!data.type) {
        console.warn("未知消息类型:", data);
        return;
    }

    switch (data.type) {
        case "chat":
            console.log("聊天消息:", data.content);
            break;
        case "notice":
            console.log("通知:", data.content);
            break;
        case "system":
            console.log("系统消息:", data.content);
            break;
        case "private":
            console.log("私聊消息:", data);
            break;
        case "broadcast":
            console.log("广播消息:", data);
            break;
        case "ping":
            console.log("心跳响应");
            break;
        default:
            console.log("其他类型消息:", data);
    }

    // 回调交给 Vue 层处理
    if (callback) callback(data);
}

// === 发送消息 ===
function send(msg) {
    if (!ws || ws.readyState !== WebSocket.OPEN) {
        console.warn("WebSocket 未连接,发送失败:", msg);
        return;
    }
    ws.send(JSON.stringify(msg));
}

// === 广播 ===
function broadcast(from, msg) {
    send({ type: "broadcast", from, content: msg });
}

// === 私聊 ===
function privateChat(from, to, msg) {
    send({ type: "private", from, to, content: msg });
}

// === 主动关闭 ===
function close() {
    stopHeartbeat();
    if (ws) {
        ws.close();
        ws = null;
    }
}

// === 导出 ===
export default {
    createWebSocket,
    send,
    close,
    broadcast,
    privateChat
};

创建聊天/接收页面:

复制代码
<template>
  <div class="p-4">
    <h2 class="text-xl font-bold mb-4">💬 WebSocket 聊天示例</h2>

    <div class="mb-4">
      <p>当前用户ID:<strong>{{ from }}</strong></p>
      <p>WebSocket状态:<span>{{ wsConnected ? "✅ 已连接" : "❌ 未连接" }}</span></p>
    </div>

    <!-- 消息显示区 -->
    <div class="border rounded p-4 h-64 overflow-y-auto bg-gray-50 mb-4">
      <div
          v-for="(msg, index) in messages"
          :key="index"
          :class="[
        'mb-2 p-2 rounded',
        msg.type === 'broadcast'
          ? 'bg-green-100'
          : msg.from === from
          ? 'bg-blue-100 text-right'
          : 'bg-white'
      ]"
      >
        <p class="text-sm">
          <strong>{{ msg.from === from ? "我" : msg.from }}</strong>:
          {{ msg.content }}
        </p>
      </div>
    </div>

    <!-- 私聊输入区 -->
    <div class="space-y-2 mb-4">
      <h3 class="font-semibold">📨 私聊消息</h3>
      <input
          v-model="to"
          placeholder="输入对方用户ID"
          class="border p-2 w-full"
      />
      <input
          v-model="privateMsg"
          placeholder="输入要发送的私聊内容"
          class="border p-2 w-full"
      />
      <button
          @click="sendPrivate"
          class="bg-blue-500 text-white px-4 py-1 rounded"
      >
        发送私聊
      </button>
    </div>

    <!-- 广播输入区 -->
    <div class="space-y-2">
      <h3 class="font-semibold">📢 广播消息</h3>
      <input
          v-model="broadcastMsg"
          placeholder="输入要广播的内容"
          class="border p-2 w-full"
      />
      <button
          @click="sendBroadcast"
          class="bg-green-500 text-white px-4 py-1 rounded"
      >
        发送广播
      </button>
    </div>
  </div>
</template>

<script>
  import wsService from "@/utils/websocket";
  import axios from "axios";

  export default {
    data() {
      return {
        from: "1", // 当前用户ID(测试用)
        to: "",
        privateMsg: "",
        broadcastMsg: "",
        messages: [],
        wsConnected: false
      };
    },
    mounted() {
      // 创建 WebSocket 连接(如 ws://localhost:8081/ws/1)
      wsService.createWebSocket(
          // 接收消息时的回调
          (msg) => {
            this.messages.push(msg);
          },
          // 当前用户 ID
          this.from,
      )
      .then(() => {
        this.wsConnected = true;
      })
    },

    beforeUnmount() {
      wsService.close();
    },
    methods: {
      // 发送私聊消息
      async sendPrivate() {
        if (!this.to || !this.privateMsg) {
          alert("请填写对方ID和消息内容");
          return;
        }
        await axios.post("/api/api/test/private", {
          from: this.from,
          to: this.to,
          type: "chat",
          content: this.privateMsg
        });
        this.privateMsg = "";
      },

      // 发送广播消息
      async sendBroadcast() {
        if (!this.broadcastMsg) {
          alert("请输入广播内容");
          return;
        }
        await axios.post("/api/api/test/broadcast", {
          from: this.from,
          type: "chat",
          content: this.broadcastMsg
        });
        this.broadcastMsg = "";
      }
    }
  };
</script>

<style scoped>
  input {
    border-radius: 6px;
  }
</style>

效果:

这时我们可以用几个页面模拟几个用户:

写两个页面,修改from

复制代码
data() {
  return {
    from: "2", // 当前用户ID(测试用)
    to: "",
    privateMsg: "",
    broadcastMsg: "",
    messages: [],
    wsConnected: false
  };
},

打开页面后,查看后端日志:发现有两个设备在线:

用户2给用户1发送信息:

用户1广播信息:

相关推荐
AALoveTouch6 小时前
大麦网抢票:基于Wireshark协议分析
网络·测试工具·wireshark
爱奥尼欧6 小时前
【Linux笔记】网络部分——socket 编程 TCP实现多台虚拟机使用指令访问云服务器
linux·服务器·网络
luopandeng6 小时前
amd npt技术 对比 intel ept 技术
java·linux·网络
迎風吹頭髮7 小时前
UNIX下C语言编程与实践60-UNIX TCP 套接字关闭:close 与 shutdown 函数的区别与使用场景
c语言·网络·unix
梁辰兴7 小时前
计算机操作系统:进程同步
网络·缓存·操作系统·进程·进程同步·计算机操作系统
hazy1k7 小时前
K230基础-录放视频
网络·人工智能·stm32·单片机·嵌入式硬件·音视频·k230
AORO20258 小时前
适合户外探险、物流、应急、工业,五款三防智能手机深度解析
网络·人工智能·5g·智能手机·制造·信息与通信
white-persist8 小时前
XXE 注入漏洞全解析:从原理到实战
开发语言·前端·网络·安全·web安全·网络安全·信息可视化
风清再凯8 小时前
01-iptables防火墙安全
服务器·网络·安全