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

推荐订阅源

T
Threatpost
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
Engineering at Meta
Engineering at Meta
爱范儿
爱范儿
博客园 - Franky
博客园 - 【当耐特】
MyScale Blog
MyScale Blog
雷峰网
雷峰网
月光博客
月光博客
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
Know Your Adversary
Know Your Adversary
宝玉的分享
宝玉的分享
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
L
LangChain Blog
K
Kaspersky official blog
P
Palo Alto Networks Blog
P
Privacy & Cybersecurity Law Blog
美团技术团队
Scott Helme
Scott Helme
B
Blog RSS Feed
T
Threat Research - Cisco Blogs
博客园_首页
L
LINUX DO - 热门话题
腾讯CDC
C
CERT Recently Published Vulnerability Notes
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
V
V2EX
Martin Fowler
Martin Fowler
T
The Exploit Database - CXSecurity.com
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
Latest news
Latest news
S
Schneier on Security
AWS News Blog
AWS News Blog

陈陈菌博客

如何在 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) 在 fedora 上建立一个 debian 容器 (bwrap) i3wm | 自制一个极简的屏幕亮度调节器 在 Linux 下启用内存压缩技术 zram 搭建一个本地轻量级聊天 Ai 我又又又重新上线了博客 如何通过命令行管理 wine 容器 (包括 proton) Btrfs 文件系统创建 swap 文件的方法 如何创建适用于 Linux 桌面的程序启动文件 在 Linux 控制 intel CPU 调度的方法 Arch Linux 的基本安装
如何用 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 鉴权、断开重连、连接池管理等这里就不细讲了,本帖只帮助快速入门,其他的我可以下次有空再写几篇来具体讲讲。