引言
在日常开发中,我们经常会遇到需要使用 WebSocket 实现实时通信的场景。WebSocket 协议提供了一个持久的连接,使得服务器可以主动向客户端发送消息。然而,WebSocket 本身基于事件的消息处理方式并不总是那么直观,尤其是当我们习惯了使用 async/await
这样的同步编程风格时。
最近,我尝试使用 GPT 来编写代码,这个过程让我对 Promise
的用法有了新的理解。具体来说,我希望将 WebSocket 的发布订阅模式转换为 async/await
模式。换言之,我希望能够从 onMessageXXX
形式的事件处理,转变为 const res = await xxx
这样的同步等待模式。这样的需求听起来可能有些奇怪,毕竟如果需要 await
风格的请求,为什么不直接使用 HTTP 服务呢?对此,我也只能苦笑,因为刚开始时我并没有想到这一点。请不要责怪我,毕竟每个人都有学习和成长的过程。
解决方案
建立 WebSocket 连接
以下是一个简单的 WebSocket 连接函数,它会在连接失败时每隔一秒尝试重连:
javascript
const connectWebSocket = async () => {
while (retryCount < maxRetries) {
try {
ws = new WebSocket("ws://localhost:8765");
ws.on("open", () => console.log('WebSocket connection opened.'));
ws.on("error", (e) => {
console.error(`Failed to connect: ${e}, retrying...`);
ws.close();
});
ws.on("message", (res) => {
console.log('Received message:', res);
});
} catch (e) {
await new Promise((resolve) => setTimeout(resolve, 1000));
retryCount++;
}
}
};
异步等待连接成功
为了将这种模式转换为 async/await
,我们需要解决两个问题:
- 如何知道 WebSocket 连接已经成功建立,并在建立成功后返回一个发送消息的函数。
- 在发送请求后,如何利用
onmessage
事件处理函数将消息返回。
针对第一个问题,我们可以通过将 try
代码块包装在 Promise
中来解决,如下所示:
javascript
const connectWebSocket = async () => {
while (retryCount < maxRetries) {
try {
ws = new WebSocket("ws://localhost:8765");
await new Promise((resolve, reject) => {
ws.on("open", () => {
console.log('WebSocket connection opened.');
resolve();
});
ws.on("error", (e) => {
console.error(`Failed to connect: ${e}, retrying...`);
ws.close();
reject(e);
});
});
return (message) => ws.send(message);
} catch (e) {
await new Promise((resolve) => setTimeout(resolve, 1000));
retryCount++;
}
}
};
这样,当我们调用 const sendMessage = await connectWebSocket()
时,得到的 sendMessage
函数就可以用来发送消息了。
异步等待消息回复
对于第二个问题,我们希望能够通过 const res = await reply("你好")
的方式获取响应。最初,我尝试使用 ws.onceMessage
来处理,但这在并发请求时会遇到问题,因为除了第一个请求外,其他请求的响应监听器会在第一个事件发生后被移除。
为了解决这个问题,我们可以为每个 WebSocket 请求创建一个唯一的识别码,并维护一个请求-响应映射。这样,即使并发发送多个请求,每个响应也可以根据其唯一识别码正确地分配到对应的请求。
javascript
const connectWebSocket = async () => {
// ...之前的代码
await new Promise((resolve, reject) => {
// ...之前的代码
ws.on("message", (res) => {
const data = JSON.parse(res);
const handler = responseHandlers[data.key];
if (handler) {
handler(data); // 调用对应 key 的解析函数
delete responseHandlers[data.key]; // 移除已调用的解析函数
}
});
});
return (text, key) => new Promise((resolve, reject) => {
responseHandlers[key] = resolve;
ws.send(JSON.stringify({ text, key }));
});
};
在 onmessage
事件中,我们解析消息并调用对应 key
的处理函数,确保每个请求都能得到正确的响应。
完整代码如下:
javascript
const connectWebSocket = async () => {
while (retryCount < maxRetries) {
try {
ws = new WebSocket("ws://localhost:8765");
await new Promise((resolve, reject) => {
ws.on("open", resolve);
ws.on("error", (e) => {
console.error(`Failed to connect: ${e}, retrying...`);
ws.close();
reject(e);
});
ws.on("message", (res) => {
const data = JSON.parse(res);
const handler = responseHandlers[data.key];
if (handler) {
handler(data); // 调用对应 key 的解析函数
delete responseHandlers[data.key]; // 移除已调用的解析函数
}
});
});
return (text, key) =>
new Promise((resolve, reject) => {
responseHandlers[key] = resolve; // 存储解析函数以便稍后调用
ws.send(JSON.stringify({ text, key }));
});
} catch (e) {
await new Promise((resolve) => setTimeout(resolve, 1000));
retryCount++;
}
}
总结
通过这次的探索,我们成功地将 WebSocket 的发布订阅模式转换为了基于 async/await
风格的同步处理模式。这不仅提高了代码的可读性和易用性,还支持了并发请求处理。这个过程再次证明了编程中的创造性解决方案和不断的迭代改进是多么重要。无论我们的起点如何,总有改进和优化的空间。而技术,就是这样一步步前进的。
最后
这个需求是我在开发微信机器人项目的时候遇见的,已经实现了自动回复,实时天气,语音功能,绘画功能,定时功能,项目已开源,如果有感兴趣的小伙伴,欢迎来star。
顺便宣传一下另一个开源脚手架项目
create-neat
。已有130+star,也可加v:a2171077189一起学习。