WebSocket

当我们创建聊天界面,需要建立客户端和服务器的双向连接时,这时,我们就需要使用到WebSocketWebSocket 对象提供了用于创建和管理 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.ioServer 类的构造函数来创建 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.ioServer 类的构造函数来创建 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 方法适用于简单的文本消息传递,而不需要自定义事件或复杂的数据。