有很多人好奇我站里的那个实时留言是怎么做的,在聊这个之前,我们得说说什么是 webSocket 协议,和 HTTP 的差异在哪里。
我们熟悉的 HTTP 一般是这样:客户端主动请求服务器、然后服务器接收到请求后被动响应给客户端。且属于短连接,几乎是请求响应后立即断开连接,每一次都是 [客户端 => 服务端] 然后 [客户端 <= 服务端]
而 webSocket 是握手成功后,会建立一个长时间的连接,此时客户端和服务端都可以同时收发信息。就变成了 [客户端 <=> 服务端]
如果我们要做实时性强的应用,如果采用 HTTP 去轮询,反复建立/断开连接会有很大的开销,极其浪费资源,所以这就是为什么要用 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'))
}
})
interface client {
ws: ServerWebSocket
}
let clients = new Set<client>()
// 获取聊天列表
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) =>{
// 当有新连接建立时,将连接对象添加到 set 集合中
clients.add({
ws: 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.ws.send(JSON.stringify({
type: 'send_msg',
name: data.name,
context: data.context,
time: data.time
}))
})
}
},
close: (ws: ServerWebSocket) => {}
}
})
这里服务端只做消息广播,然后服务端通过 json 文件存储消息(也可以使用数据库), 并且根据不同类型消息做出回应,实现了实时消息的收发。
当然,这里还有非常多优化空间,比如消息的时间排序、字段判空、字符转义处理、需要区分不同的人增加 UID、支持 wss (SSL安全支持),Token 鉴权、断开重连、连接池管理等这里就不细讲了,本帖只帮助快速入门,其他的我可以下次有空再写几篇来具体讲讲。