惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
博客园 - Franky
MyScale Blog
MyScale Blog
L
LangChain Blog
Martin Fowler
Martin Fowler
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 司徒正美
量子位
A
About on SuperTechFans
C
Check Point Blog
大猫的无限游戏
大猫的无限游戏
Last Week in AI
Last Week in AI
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
I
InfoQ
V
Visual Studio Blog
Vercel News
Vercel News
B
Blog
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
U
Unit 42

陈陈菌博客

如何在 podman 容器内使用 systemd 在 podman 运行 GUI 应用的经验总结 管理 podman 容器与镜像的导入或导出 用 xdg-dbus-proxy 解决容器的文件选择器的问题 解决 VSCodium 或 Code-Server 无法安装 GitHub Copilot 问题 不可变发行版 Fedora Silverblue 的体验记录 解决 apt 或 dnf 无法 tab 补全的问题 对 (MPA) 多页面切换实现丝滑动画的探索 让服务在非 Root 权限下使用 80 或 443 端口 AppImage 应用打包快速上手 Flatpak 使用和应用打包快速上手 Linux 内核编译 | 快速上手 | deb 使用 debootstrap 构建 Debian 系根文件系统 高度可定制的 KDE 窗口装饰器 klassy 有关 docker、podman、nspawn、bwrap 的 Linux 容器图形性能测试 创建任意 Linux 容器并支持 GUI 程序 (Docker) 创建任意 Linux 容器并支持 GUI 程序 (nspawn) 文件同步工具 rsync 的使用 玲珑 (Linyaps) 应用打包 RPM 打包指南 | 精简版 PBR 材质的各种贴图类型入门 APU | Stable-Diffusion 使用记录 如何在 Electron 上启用 WebGPU Grub 主题安装与配置 Fcitx5 主题安装与自定义 不使用 Nginx 的网站开发指南 如何自制一个简易的资源监控程序 SQL 常用 CRUD 语法记录 Debian 打包入门,与需要注意的问题 在 fedora 建立一个 Arch 容器 (bwrap)
如何用 webSocket 实现一个实时聊天应用
GlumiStudio@ · 2025-11-22 · via 陈陈菌博客

2025-11-22 14:27:57

分类: Web开发 标签: Bun


有很多人好奇我站里的那个实时留言是怎么做的,在聊这个之前,我们得说说什么是 webSocket 协议,和 HTTP 的差异在哪里。

我们熟悉的 HTTP 一般是这样:客户端主动请求服务器、然后服务器接收到请求后被动响应给客户端。且属于短连接,几乎是请求响应后立即断开连接,每一次都是 [客户端 => 服务端] 然后 [客户端 <= 服务端]

而 webSocket 是握手成功后,会建立一个长时间的连接,此时客户端和服务端都可以同时收发信息。就变成了 [客户端 <=> 服务端]

如果我们要做实时性强的应用,如果采用 HTTP 去轮询,反复建立/断开连接会有很大的开销,极其浪费资源,所以这就是为什么要用 webSocket,建立一次连接持续收发。

快速上手 webSocket


接下来我们尝试一个最简单的 webSocket,许多现代编程语言/平台都有 webSocket 模块或库,这里我使用 Bun 来演示最简单的例子。我觉得有些概念其他语言也是相通的,所以重要的是思路。

客户端 (web) 代码:

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Client</title>
</head>
<body>
    <script>
        let ws = new WebSocket('ws://localhost:8081')
        ws.onopen = (event) => {
            console.log('已建立连接')
        }
        ws.onmessage = (event) => {
            console.log('已收到消息')
            console.log(event.data)
        }
        ws.onclose = (event) => {
            console.log('已断开连接')
        }
    </script>
</body>
</html>

服务端 (bun) 代码:

import { type ServerWebSocket } from 'bun'

Bun.serve({
    port: 8080,
    routes: {
        "/": new Response(Bun.file('./index.html'))
    }
})


Bun.serve({
    port: 8081,
    fetch(req,server){
        server.upgrade(req)
    },
    websocket: {
        open: (ws: ServerWebSocket) =>{
            ws.send('hello')
        },
        message: (ws: ServerWebSocket, msg: string | Buffer<ArrayBufferLike>) => {
            
        },
        close: (ws: ServerWebSocket) => {

        }
    }
})

这里我启用了一个 8080 端口来专门托管 html (也就是客户端页面),而 8081 端口用来进行 webSocket 通信。Bun 后端的 fetch 函数会接收所有 http 请求,在这里 server.upgrade(req) 会将 http 连接升级为 webSocket 协议去通信。关于协议升级

同时我让后端服务在刚建立 webSocket 连接时给客户端发送一个 'hello' 字符串,而客户端收到将会触发 onmessage 函数,并打印了数据。其中 event.data 是接收到来自另一端的消息报文。

客户端 webSocket 这里几个函数作用:

函数触发条件
onopen当建立连接时触发
onmessage当收到数据时触发
onclose当连接关闭时触发

而 bun 服务端也是类似,对应

函数触发条件
open当建立连接时触发
message当收到数据时触发
close当连接关闭时触发

实现聊天应用


在有了一些基础认识后,对于实现聊天应用来说思路已经非常清晰了,下面是一个极简且完整例子。

客户端 (web):

<!DOCTYPE html>
<html lang="cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Client</title>
    <style>
        body,html {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .chat_box {
            position: relative;
            display: flex;
            flex-direction: column;
            align-items: center;
            border: 1px solid;
            width: 800px;
            height: 600px;
        }
        .messages {
            width: 100%;
            padding-bottom: 150px;
            overflow: auto;
        }
        .message {
            border: 1px solid;
            padding: 10px;
        }
        .input_group {
            display: flex;
            position: absolute;
            width: 100%;
            bottom: 0px;
            box-sizing: border-box;
        }
        .input_group > .send_name,
        .input_group > .send_context {
            width: 40%;
        }
        .input_group > * {
            padding: 10px;
            outline: none;
        }
        .input_group > .send_Btn {
            width: 20%;
        }
    </style>
</head>
<body>
    <div class="chat_box">
        <div class="messages"></div>
        <div class="input_group">
            <input class="send_name" type="text" placeholder="输入名称">
            <input class="send_context" type="text" placeholder="输入内容">
            <input class="send_Btn" type="button" value="发送">
        </div>
    </div>
    <script>
        let messages = document.querySelector('.messages')
        let ws = new WebSocket('ws://localhost:8081')
        ws.onopen = (event) => {
            ws.send(JSON.stringify({
                type: 'get_messages'
            }))
        }
        ws.onmessage = (event) => {
            // 解析 json 为 js 对象,并根据报文类型做出反应
            let event_data = JSON.parse(event.data)
            console.log(event_data)
            if(event_data.type=='get_messages'){
                let str = ''
                for(let item of event_data.messages){
                    str += `
                        <div class="message">
                            <div>name: ${item.name}</div>
                            <div>context: ${item.context}</div>
                        </div>
                    `
                }
                messages.innerHTML = str 
            }
            if(event_data.type=='send_msg'){
                messages.innerHTML += `
                    <div class="message">
                        <div>name: ${event_data.name}</div>
                        <div>context: ${event_data.context}</div>
                    </div>
                `
            }
            // 每次渲染内容让滚动条往下滑动
            messages.scrollTo({behavior:'smooth',top: messages.scrollHeight})
        }
        ws.onclose = (event) => {}

        let send_name = document.querySelector('.send_name')
        let send_context = document.querySelector('.send_context')
        let send_Btn = document.querySelector('.send_Btn')
        // 点击'发送'按钮将会把消息发给服务端
        send_Btn.addEventListener('click',()=>{
            ws.send(JSON.stringify({
                type: 'send_msg',
                name: send_name.value,
                context: send_context.value,
                time: new Date().getTime()
            }))
        })
    </script>
</body>
</html>

后端 (Bun):

import { type ServerWebSocket } from 'bun'
import fs from 'node:fs'

Bun.serve({
    port: 8080,
    routes: {
        "/": new Response(Bun.file('./index.html'))
    }
})

let clients = new Set<ServerWebSocket>()

// 获取聊天列表
async function get_messages(){
    if(!fs.existsSync(`./messages`)){
        fs.mkdirSync(`./messages`)
    }
    let files = fs.readdirSync('./messages')
    let array = []
    for(let file of files){
        let info = await Bun.file(`./messages/${file}`).json()
        array.push(info)
    }
    return array
}

Bun.serve({
    port: 8081,
    fetch(req,server){
        server.upgrade(req)
    },
    websocket: {
        open: (ws: ServerWebSocket) =>{
            // 当有新连接建立时,将连接对象添加到 clients 集合中
            clients.add(ws)
        },
        message: async (ws: ServerWebSocket, msg: string | Buffer<ArrayBufferLike>) => {
            let data = JSON.parse(msg.toString())
            if(data.type=='get_messages'){
                ws.send(JSON.stringify({
                    type: 'get_messages',
                    messages: await get_messages()
                }))
            }
            // 如果有消息传来,转而广播给所有对象,且将消息以 json 形式存储在服务端
            if(data.type=='send_msg'){
                await Bun.write(`./messages/${data.name}-${data.time}.json`,JSON.stringify({
                    name: data.name,
                    context: data.context,
                    time: data.time
                },null,4))
                clients.forEach(client =>{
                    client.send(JSON.stringify({
                        type: 'send_msg',
                        name: data.name,
                        context: data.context,
                        time: data.time
                    }))
                })
            }
        },
        close: (ws: ServerWebSocket) => {
            // 断开连接后,把连接对象从 clients 集合中移除
            clients.delete(ws)
        }
    }
})

这里服务端通过 json 文件存储消息(也可以使用数据库),服务端如果收到 get_messages 消息就只会给当前连接端发送 get_messages 类型数据,而如果是 send_msg 类型,就只做消息广播也就是给所有连接端都发送 send_msg 类型消息。而客户端也做类似的操作如:收到某消息就去更新页面,这样一来就根据不同消息类型做出不同回应和操作,并且这是实时的。

当然,这里还有非常多优化空间,比如消息的时间排序、字段判空、字符转义处理、需要区分不同的人增加 UID、支持 wss (SSL安全支持),Token 鉴权、断开重连、连接池管理等这里就不细讲了,本帖只帮助快速入门,其他的我可以下次有空再写几篇来具体讲讲。