WebSocket

当我们创建聊天界面,需要建立客户端和服务器的双向连接时,这时,我们就需要使用到WebSocket,WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API,来帮助我们实现聊天对话的功能。

构造函数

**WebSocket()**构造函器会返回一个 WebSocket对象。

1
var aWebSocket = new WebSocket(url [, protocols]);

参数

  • url

    要连接的 URL;这应该是 WebSocket 服务器将响应的 URL。

  • protocols (可选)

    一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议(例如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

实例属性

WebSocket.readyState

返回当前 WebSocket的链接状态,只读。

1
var readyState = WebSocket.readyState;

返回的值

以下其中之一

  • 0(WebSocket.CONNECTING)

    正在链接中

  • 1 (WebSocket.OPEN)

    已经链接并且可以通讯

  • 2 (WebSocket.CLOSING)

    连接正在关闭

  • 3 (WebSocket.CLOSED)

    连接已关闭或者没有链接成功

实例方法

WebSocket.close()

WebSocket.close() 方法关闭 WebSocket 连接或连接尝试(如果有的话)。如果连接已经关闭,则此方法不执行任何操作。

语法

1
WebSocket.close();
参数
  • code可选

    一个数字状态码,它解释了连接关闭的原因。如果没有传这个参数,默认使用 1005。CloseEvent的允许的状态码见状态码列表。

  • reason可选

    一个人类可读的字符串,它解释了连接关闭的原因。这个 UTF-8 编码的字符串不能超过 123 个字节。

抛出的异常
  • INVALID_ACCESS_ERR

    一个无效的code

  • SYNTAX_ERR

    reason 字符串太长(超过 123 字节)

WebSocket.send()

WebSocket.send() 方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的 data bytes 的大小来增加 bufferedAmount的值。若数据无法传输(例如数据需要缓存而缓冲区已满)时,套接字会自行关闭。

语法

1
WebSocket.send("Hello server!");
参数
  • data

    用于传输至服务器的数据。它必须是以下类型之一:

    • USVString

      文本字符串。字符串将以 UTF-8 格式添加到缓冲区,并且 bufferedAmount 将加上该字符串以 UTF-8 格式编码时的字节数的值。

    • ArrayBuffer

      你可以使用一有类型的数组对象发送底层二进制数据;其二进制数据内存将被缓存于缓冲区,bufferedAmount 将加上所需字节数的值。

    • Blob

      Blob 类型将队列 blob 中的原始数据以二进制中传输。 bufferedAmount 将加上原始数据的字节数的值。

    • ArrayBufferView

      你可以以二进制帧的形式发送任何 JavaScript 类数组对象 ;其二进制数据内容将被队列于缓冲区中。值 bufferedAmount 将加上必要字节数的值。

异常
  • INVALID_STATE_ERR

    当前连接未处于 OPEN 状态。

  • SYNTAX_ERR

    数据是一个包含未配对代理 (unpaired surrogates) 的字符串。

事件

WebSocket.onclose

WebSocket.onclose 属性返回一个事件监听器,这个事件监听器将在 WebSocket 连接的readyState 变为 CLOSED时被调用,它接收一个名字为“close”的 CloseEvent 事件。

语法

1
2
3
WebSocket.onclose = function(event) {
console.log("WebSocket is closed now.");
};

WebSocket: 错误事件

当websocket的连接由于一些错误事件的发生 (例如无法发送一些数据) 而被关闭时,一个error事件将被引发。

1
2
3
4
5
6
7
8
9
// Create WebSocket connection
// 创建一个 WebSocket 连接
const socket = new WebSocket("ws://localhost:8080");

// Listen for possible errors
// 监听可能发生的错误
socket.addEventListener("error", function (event) {
console.log("WebSocket error: ", event);
});

WebSocket: message event

message 事件会在 WebSocket 接收到新消息时被触发。

1
2
3
4
5
6
7
// 创建一个 WebSocket 连接
const socket = new WebSocket("ws://localhost:8080");

// 监听消息
socket.addEventListener("message", function (event) {
console.log("Message from server ", event.data);
});

WebSocket: 建立连接

open 事件会在 WebSocket 建立连接时被触发。

1
2
3
4
// Connection opened
socket.addEventListener("open", function (event) {
socket.send("Hello Server!");
});

使用了WebSocket的基本样例

服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({port: 3000} );

wss.on('connection', ws => {
console.log('有人进来了');

ws.on('message', data=>{
ws.send(data+'hi!');
console.log(data)
})

ws.on('close', ()=>{
console.log('有人走了');
})
});

客户端

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
<!--index.html-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
const ws = new WebSocket('ws://localhost:3000');

ws.addEventListener('open', ()=>{
console.log('连接上服务器了');
ws.send('hello!');
});

ws.addEventListener('message',({data})=>{
console.log(data)
})
</script>
</body>
</html>

使用了socket.io的基本样例

服务器

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
//server.js
const express = require('express'); //这里使用了express框架
const app = express();
const http = require('http'); // 导入 http 模块
const server = http.createServer(app); // 创建 HTTP 服务器实例,并将其与 Express 应用程序实例关联
const { Server } = require("socket.io");
const io = new Server(server)

app.get('/',(req,res)=>{
res.sendFile(__dirname + '/index.html');
})

io.on('connection',socket=>{
console.log('有人进来了');
socket.on('chat message',msg=>{
console.log(msg);
io.emit('chat message',msg);
})
socket.on('disconnect',()=>{
console.log('有人出去了');
})
})

server.listen('3000',()=>{
console.log('服务器开启了')
})

客户端

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
36
37
38
<!--index.html-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form>
<input type="text">
<button>发送</button>
</form>
<ul></ul>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const form = document.querySelector('form');
const input = document.querySelector('input');
const ul = document.querySelector('ul')

form.addEventListener('submit',e=>{
e.preventDefault()
if (input.value){
socket.emit('chat message',input.value);
input.value = '';
}
});
socket.on('chat message',msg=>{
const li = document.createElement('li');
li.textContent = msg;
ul.appendChild(li);
});
</script>
</body>
</html>
1
<script src="/socket.io/socket.io.js"></script> 

注意:<script src="/socket.io/socket.io.js"></script> 这段代码表示从服务器的 /socket.io 路径下获取 socket.io.js 文件,并在客户端加载该文件,以便使用 Socket.IO 客户端功能。具体的文件路径和名称可能会因服务器配置和部署方式而有所不同。因为这个服务器和客户端在一个接口,所以可以正常使用

在vue中使用socket.io的基本样例

服务器

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
//server.js
const express = require('express'); //这里使用了express框架
const app = express();
const http = require('http'); // 导入 http 模块
const server = http.createServer(app); // 创建 HTTP 服务器实例,并将其与 Express 应用程序实例关联
const { Server } = require("socket.io");
const io = new Server(server,{
cors: {
origin: "http://localhost:5173"
}
})

app.get('/',(req,res)=>{
res.send("hello world!")
})

io.on('connection',socket=>{
console.log('有人进来了');
socket.on('chatMessage',msg=>{
console.log(msg);
io.emit('chatMessage',msg);
})
socket.on('disconnect',()=>{
console.log('有人出去了');
})
})

server.listen('3000',()=>{
console.log('服务器开启了')
})

注意1:CORS策略是一种浏览器安全机制,用于限制跨域请求。在使用跨域请求时,一定要在服务器上配置正确的CORS头信息,以允许来自客户端http://localhost:5173的跨域请求。

注意2:但我直接将express函数创建的app传给socket.io包的Server函数时,发生了报错

1
2
3
4
5
6
7
8
const express = require('express'); //这里使用了express框架
const app = express();
const { Server } = require("socket.io");
const io = new Server(app,{
cors: {
origin: "http://localhost:5173"
}
})
1
Error: You are trying to attach socket.io to an express request handler function. Please pass a http.Server instance.

虽然 Express 构建在 http.Server 之上,但它并不是 http.Server 的直接实例。在使用 socket.io 时,需要将 http.Server 实例作为参数传递给 socket.io 构造函数,而不是 Express 应用程序实例。

但是这样写也可以实现服务器功能,不使用Express框架,直接使用http创建http实例

1
2
3
4
5
6
7
8
const http = require('http'); // 导入 http 模块
const server = http.createServer(); // 创建 HTTP 服务器实例,并将其与 Express 应用程序实例关联
const { Server } = require("socket.io");
const io = new Server(server,{
cors: {
origin: "http://localhost:5173"
}
})

这两种写法的区别在于 HTTP 服务器实例的创建方式不同:

  1. 第一种写法:

    1
    2
    3
    4
    5
    6
    7
    8
    const http = require('http');
    const server = http.createServer();
    const { Server } = require("socket.io");
    const io = new Server(server, {
    cors: {
    origin: "http://localhost:5173"
    }
    });

    在这种写法中,使用了 http.createServer() 方法创建了一个空的 HTTP 服务器实例 server,然后将其传递给 socket.io 的 Server 类的构造函数来创建 socket.io 服务器实例 io。

  2. 第二种写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const express = require('express');
    const app = express();
    const http = require('http');
    const server = http.createServer(app);
    const { Server } = require("socket.io");
    const io = new Server(server, {
    cors: {
    origin: "http://localhost:5173"
    }
    });

    在这种写法中,除了导入 http 模块外,还导入了 express 框架。首先创建了一个 Express 应用程序实例 app,然后使用 http.createServer(app) 方法将其与 HTTP 服务器实例 server 关联,最后将 server 实例传递给 socket.io 的 Server 类的构造函数来创建 socket.io 服务器实例 io。

这两种写法的区别在于是否使用了 Express 应用程序。如果你的项目中已经使用了 Express 框架,并且希望将 WebSocket 连接集成到 Express 应用程序中,那么第二种写法是更常见和推荐的方式。它允许你在同一个应用程序中处理 HTTP 请求和 WebSocket 连接,并共享同一个端口。

如果你只需要一个独立的 HTTP 服务器实例,并且不需要与 Express 应用程序进行集成,那么第一种写法就足够了。

总结:

  • 第一种写法创建了一个独立的空的 HTTP 服务器实例,适用于不需要与 Express 应用程序集成的情况。
  • 第二种写法使用了 Express 框架,将 Express 应用程序与 HTTP 服务器实例关联,适用于在同一个应用程序中处理 HTTP 请求和 WebSocket 连接的情况。这种方式更常见和推荐,因为它提供了更多的灵活性和集成性。

客户端

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
36
37
38
39
40
41
42
//socket.js
//封装使用
import { reactive } from "vue";
import { io } from "socket.io-client";

// 创建一个响应式对象来存储状态
export const state = reactive({
connected: false, // WebSocket连接状态
fooEvents: [], // 存储"foo"事件的数据
barEvents: [], // 存储"bar"事件的数据
chatMessage:[] // 存储"chatMessage"事件的数据
});

// 创建一个Socket.IO客户端实例
export const socket = io("http://localhost:3000");

// 监听"connect"事件,当连接建立时触发
socket.on("connect", () => {
state.connected = true; // 更新连接状态为已连接
});

// 监听"disconnect"事件,当连接断开时触发
socket.on("disconnect", () => {
state.connected = false; // 更新连接状态为未连接
});

//通过监听这些事件,能获得从服务器端传来的数据,并将数据储存在响应式对象state中
// 监听"foo"事件,当接收到"foo"事件时触发
socket.on("foo", (...args) => {
state.fooEvents.push(args); // 将事件的数据添加到fooEvents数组中
});

// 监听"bar"事件,当接收到"bar"事件时触发
socket.on("bar", (...args) => {
state.barEvents.push(args); // 将事件的数据添加到barEvents数组中
});

// 监听"chatMessage"事件,当接收到"chatMessage"事件时触发
socket.on("chatMessage", (...args) => {
state.chatMessage.push(args); // 将事件的数据添加到chatMessage数组中
console.log(args);
});
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
//在vue组件中调用这个api
<template>
<p>State: {{ state.connected }}</p>
<button @click="connect()">Connect</button>
<button @click="disconnect()">Disconnect</button>
<form @submit.prevent="onSubmit">
<input v-model="value" />

<button type="submit" :disabled="isLoading">Submit</button>
</form>
</template>

<script setup>
import { state, socket } from "../API/WebSocketAPI.js";
import {ref} from "vue";

let isLoading = ref(false);
let value = ref("");

function onSubmit(){
isLoading.value = true; // 设置加载状态为true

// 使用socket实例发送带有超时的事件
socket.timeout(5000).emit("chatMessage", value.value, ()=>{
isLoading.value = false
})
}
function connect (){
socket.connect();
console.log(state.connected)
}
function disconnect(){
socket.disconnect();
}
</script>
1
2
3
socket.timeout(5000).emit("create-something", value.value, ()=>{
isLoading.value = false
})

在这段代码中:

  1. socket.timeout(5000):通过socket.timeout()方法设置了一个超时时间为5秒。这意味着如果在5秒内没有收到服务器对该事件的响应,将触发超时处理。
  2. emit("create-something", value.value, () => { ... }):使用.emit()方法发送一个名为”create-something”的自定义事件,并传递value作为事件的数据参数。同时,提供了一个回调函数作为最后一个参数。
    • 当服务器接收到”create-something”事件并处理完成后,可以选择通过回调函数来执行额外的逻辑。
    • 在这段代码中,回调函数简单地将this.isLoading设置为false,表示请求已完成,加载状态被置为false。
  3. 这里推荐使用socket.emit,因为:
    • emit 方法:emit 方法用于发送具有自定义事件名称的消息。它可以用于发送任意类型的数据,并且可以附带回调函数来处理服务器对该消息的响应。
    • send 方法:send 方法是 Socket.IO 的一种简化形式,用于发送文本消息。它只能发送字符串类型的数据,而无法发送复杂的 JavaScript 对象。与 emit 方法不同,send 方法不需要指定事件名称,它将使用默认的 “message” 事件。因此,send 方法适用于简单的文本消息传递,而不需要自定义事件或复杂的数据。