使用SocketIO实现聊天室

更新时间: 2024-11-22 13:26:15

昨天已经用ws和原生websocket实现了websocket的聊天室,但是有个严重的弊端,无法知道客户端有没有意外断开。因此选用了内置心跳机制和重连功能的socket.io

# 什么是socket.io

Socket.IO是由 Guillermo Rauch 开发的,旨在解决WebSocket在不同浏览器和网络环境中的兼容性问题。它通过提供同意的API,使得开发者可以轻松实现实时双向通信,而不必担心底层传输协议的差异。

主要特点:

  • 跨平台支持: 支持Node.js、Python、Java、.NET等多种语言平台
  • 自动回退机制: 如果WebSocket不可用,Socket.IO会自动回退到其他协议(如XHR轮询、JSONP轮询)
  • 事件驱动:基于事件的变成模型,支持自定义事件
  • 命名空间:支持命名空间,可以在同一连接中区分不同的逻辑通道
  • 房间机制:支持房间(Rooms)功能,可以实现群组通信
  • 连接管理:内置心跳机制和重连功能,保证连接的稳定性和可靠性
  • 二进制支持: 支持发送和接收二进制数据

架构:Socket.IO由两部分组成:

  • Socket.IO Server:在服务器端运行,处理客户端连接、消息传递和事件管理
  • Socket.IO Client:在客户端运行,负责建立连接、发送和接收消息

# 集成Socket.IO

  1. 安装服务端的socket.io
    npm install socket.io

  2. 初始化

// index.js 
const express = require('express')
const {createServer} = require('node:http')
const {Server} = require('socket.io')  

const app = express() 
const server = createServer(app)
const io = new Server(server)  

io.on('connection', (socket) => {
    console.log('a user connected')
})

server.listen(3000, () => {
    console.log('server running at http://localhost:3000')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

请注意,这里通过传递server对象来初始化socket.io的新实例。然后监听socket的connection事件并记录到控制台

  1. 安装客户端库 socket.io-client npm install socket.io-client

  2. 客户端连接

<template>
  <div class="main">
    <h3>WebSocket 聊天室:</h3>
    <div id="responseTest"></div>
    <input type="text" v-model="message" style="width:300px;">
    <input type="button" value="发送消息" @click="send(message)">
  </div>
</template>

<script setup>
  import {ref, onMounted} from "vue"
  import {io} from  "socket.io-client/dist/socket.io.js" 
  const message = ref('')
  let socket = null

  onMounted(() => {
    socket = io("http://192.168.2.122:3000")
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 解决跨域问题
    注意,这样写客户端连接的时候会有跨域问题,我们在服务端解决一下跨域问题:
const io = new Server(server,{
    cors: {
      origin: "*"
    }
})
1
2
3
4
5
  1. 监听disconnect事件
io.on('connection', (socket) => {
  console.log('a user connected');
  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
});
1
2
3
4
5
6

现在多次刷新浏览器,就能监听到disconnect事件了

# 客户端发出事件

点击按钮的时候,让客户端发送 chat message事件




















 
 
 
 


<template>
  <div class="main">
    <h3>WebSocket 聊天室:</h3>
    <div id="responseTest"></div>
    <input type="text" v-model="message" style="width:300px;">
    <input type="button" value="发送消息" @click="send(message)">
  </div>
</template>

<script setup>
  import {ref, onMounted} from "vue"
  import {io} from  "socket.io-client/dist/socket.io.js" 
  const message = ref('')
  let socket = null

  onMounted(() => {
    socket = io("http://192.168.2.122:3000")
  })

  function send(text) {
    socket.emit('chat message', text)
    message.value = ''
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 服务端接收事件并广播

我们的下一个目标是将事件从服务器发送给其他用户

为了向每个人发送事件,Socket.IO为我们提供了 io.emit()方法

io.emit('hello', 'world')
1

如果想向除发送socket的人之外的所有人发送消息,我们需要加上broadcast

io.on('connection', (socket) => {
    socket.broadcast.emit('hi')
})
1
2
3

不过为了简单起见,我们现在将消息发送给每个人,包括发件人

io.on('connection', (socket) => {
    console.log('a user connected')

    socket.on('chat message',(msg) => {
        io.emit('chat message', "message => "+msg)
    })

    socket.on('disconnect', () => {
        console.log('user disconnected')
    })
})
1
2
3
4
5
6
7
8
9
10
11

# 客户端接收事件并显示

socket.on('chat message',(msg) => {
    html.value += '<br>'+msg
})
1
2
3

到这里效果如下:

# API概述

在继续之前,我们快速浏览一下Socket.IO 提供的API

# 通用

以下方法对于客户端和服务器均可用

  • emit
    可以使用socket.emit()向另一端发送任何数据
// 客户端
socket.emit('hello', 'world')
1
2
// 服务端
io.on('connection',(socket) => {
    socket.on('hello', (arg) => {
        console.log(arg) // world
    })
})
1
2
3
4
5
6

可以发送任意数量的参数,并且支持所有可序列化的数据结构,包括二进制对象,如ArrayBuffer、TypedArray或Buffer(仅限Node.js)

// 客户端 
socket.emit('hello', 1, '2', {3: '4', 5: Unit8Array.from[6]})
1
2
// 服务端
io.on('connection', (socket) => {
    socket.on('hello', (arg1, arg2, arg3) => {
        console.log(arg1) // 1
        console.log(arg2) // '2'
        console.log(arg3) // {3: '4', 5: <Buffer 06>}
    })
})
1
2
3
4
5
6
7
8

提示

不要在对象上调用JSON.stringify()

// 错误  
socket.emit('hello', JSON.stringify({name: 'John'}))  

// 正确
socket.emit('hello', {name: 'John'})
1
2
3
4
5
  • 确认

尽管事件很好用,但在某些情况下您可能需要更经典的请求- 响应API, 在Socket.IO中,此功能称为“确认”

它有两种情况:

  1. 带有回调
    可以给emit()添加一个回调作为最后一个参数,一旦对方确认了该事件,就会调用回调:
// 客户端
socket.timeout(5000).emit('request', {foo :'bar'}, 'baz', (err, response) => {
    if(err) {
        // 服务端没有在规定的时间内给出确认  
    } else {
        console.log(response.status) // ok
    }
})
1
2
3
4
5
6
7
8
// 服务端  
io.on('connection', (socket) => {
    socket.on('request', (arg1, arg2, callback) => {
        console.log(arg1) // {foo:'bar} 
        console.log(arg2) // 'baz'
        callback({
            status: 'ok'
        })
    })
})
1
2
3
4
5
6
7
8
9
10
  1. 带着Promise
    emitWithAck() 方法提供相同的功能,但返回一个Promise,一旦对方确认事件,该Promise就会 resolve。
// 客户端  
try {
    const resoponse = await socket.timeout(5000).emitWithAck('request', {foo: 'bar'}, 'baz')
    console.log(response.status) // 'ok'
} catch (e) {
    // 服务端没有在规定的时间内给出确认  
}
1
2
3
4
5
6
7
// 服务端 
io.on('connection', (socket) => {
    socket.on('request', (arg1, arg2, callback) => {
        console.log(arg1) // {foo: 'bar'}
        console.log(arg2) // 'baz'
        callback({
            status:'ok'
        })
    })
})
1
2
3
4
5
6
7
8
9
10

注意

不支持Promise的环境需要添加一个polyfill 或使用向babel这样的编译器才能使用此功能

  • 捕获所有事件
    可以捕获所有的事件,调试程序的时候很好用
// 发送人
socket.emit('hello', 1, '2', {3: '4', 5: Uint8Array.from[6]})
1
2
// 接收人 
socket.onAny((eventName, ...args) => {
    console.log(eventNeme) // 'hello'
    console.log(args) // [1, '2', {3: '4', 5: ArrayBuffer(1) [6]}]
})
socket.onAnyOutgoing((eventName, ...args => {
    console.log(eventNeme) // 'hello'
    console.log(args) // [1, '2', {3: '4', 5: ArrayBuffer(1) [6]}]
}))
1
2
3
4
5
6
7
8
9

# server API

  • 广播
    之前学过的,可以使用io.emit()向所有连接的客户端广播事件
io.emit('hello', 'world')
1

  • 房间
    房间是socket可以加入和离开的任意通道。它可以用于向连接的客户端子集广播事件:
io.on('connection', (socket) => {
    // 加入 名字叫 some room 的房间  
    socket.join('some room') 

    // 广播给所有在 some room房间里的客户端  
    io.to('some room').emit('hello', 'world')

    // 广播给所有不在 some room 房间里的客户端  
    io.except('some room').emit('hello', 'world')

    // 离开房间  
    socket.leave('some room')
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 处理断开连接

现在,我们重点说下Socket.IO的两个非常重要的点:

  1. Socket.IO 客户端并不总是处理连接状态
  2. Socket.IO 服务器不存储任何事件

注意

即使在稳定的网络上,也不可能永远保持连接

这意味着您的应用程序需要能够在暂时断开连接后将客户端的本地状态与服务器上的全局状态同步

客户端将在短暂延迟后尝试重新连接,但是,对于该客户端来说,在断开连接期间任何错过的事件都将实际上丢失

在我们的聊天应用程序的上下文中,断开连接的客户端可能会错过一些消息

接下来我们来改进一下

# 连接状态恢复

此功能将临时存储服务器发送的所有事件,并在客户端重新连接时尝试恢复客户端的状态:

  • 恢复其房间
  • 发送任何错过的事件

必须在服务端启用:

const io = new Server(server, {
    connectionStateRecovery: {}
})
1
2
3

为什么默认情况下不启用它?

  • 它并不总是有效,例如,服务器突然崩溃或重新启动,则可能无法保存客户端状态
  • 扩展时并不总是可以启用此功能

话虽这么说,但这确实是一个很棒的功能呢个,因为您不必在临时断开连接后同步客户端的状态

接下来我们探索一下更通用的解决方案

# 简单的给用户分一下房间

io.on('connection', (socket) => {
    console.log('a user connected')

    let room = ''

    socket.on('change room', roomName => {
        if(roomName){
            socket.join(roomName)
            if(room) {
                socket.leave(room)
            }
            socket.emit('chat message', '加入房间:'+roomName)
            room = roomName 
        }else{
            if(room){
                socket.leave(room)
                room = ''
                socket.emit('chat message', '离开房间')
            }
        }
    })

    socket.on('chat message',(msg) => {
        console.log(room)
        if(room){
            io.to(room).emit('chat message', (room) + " => "+msg)
        }else{
            io.emit('chat message', ("公屏") + " => "+msg)
        }
    })

    socket.on('disconnect', () => {
        console.log('user disconnected')
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35