聊天室模块用到了socket.io包, 遇到的坑还是挺多的, 特此记录于此.

更新


[2019-7-27]

  • Initial release

[2019-7-29]

Changed

  • 更新代码格式

[2019-7-30]

Added

  • 新增问题刷新服务器后, socket.io会发送多次数据
  • 新增问题socket.io无法实时接收发送信息, 需要同时刷新服务器和客户端

[2019-8-12]

Added

  • 新增问题客户端的socket监听事件会触发 2^n 次
  • 新增问题在componentDidMount中设置的socket.on监听, 只能作用于一个会话, 因为componentDidMount只执行了一次, 变化的只是传递的路由参数.

[2019-8-19]

Added

  • 新增问题一次单聊会话中, 接收方的所有好友都能收到本次独立会话的消息

[2019-8-31]

Added

  • 新增问题Firefox下报Websocket无法连接的错误

[2019-9-22]

Added

  • 新增问题重复创建了多个重复的websocket连接

[2019-9-25]

Added

  • 新增问题socket.removeAllEventListener引发的惨案

记录


1. Access to XMLHttpRequest at ‘http://localhost:8888/socket.io/?EIO=3&transport=polling&t=MmmlqWJ' from origin ‘http://localhost:3000' has been blocked by CORS policy: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

相关依赖:

  • socket.io@^2.2.0,
  • socket.io-client@^2.2.0,
  • @types/socket.io@^2.1.2,
  • @types/socket.io-client@^1.4.32,

产生原因:

  • 原因一

使用koa-cors库的时候, 如果内部设置了credentials: trueorigin: *, 则会出现此报错.

  • 原因二

server.js开启服务器并监听端口的方式有误, 如果使用了socket.io, 会使用另外一种监听方式.

解决方式:

  • 解决一

参考思否的文章: axios的cookie跨域以及相关配置.

展开代码
1
2
3
4
app.use(require('koa-cors')({
maxAge: 60 * 60 * 24 * 4,
+ origin: '*',
}));
  • 解决二

参考脚本之家的相关文章: 详解如何使用koa实现socket.io官网的例子

不使用socket.io的例子:

展开代码
1
2
3
4
5
import * as Koa from 'koa';

const app: Koa = new Koa();

app.listen(8888);

使用了socket.io的例子:

展开代码
1
2
3
4
5
6
7
8
9
import * as Koa from 'koa';
import * as IO from 'socket.io'
import * as http from 'http';

const app: Koa = new Koa();
const server: http.Server = http.createServer(app.callback());
const io: IO.Server = IO(server);

server.listen(8888);

附上前台的socket.io-client的连接方式:

展开代码
1
2
3
4
5
6
import * as IO from 'socket.io-client';

const socket = IO('ws://localhost:8888');

socket.emit();
socket.on();

2. 刷新服务器后, socket.io会发送多次数据(前台会打印多次)

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

产生原因:

起初是想将socket有关的逻辑抽离成单独的模块, 所以直接按照如下的方式来使用:

  • server.ts
展开代码
1
2
3
4
5
6
7
8
9
10
11
import * as IO from 'socket.io';

import { handleChat } from './controller/chat/create/chatCreate';


const app: Koa = new Koa();
const server: Http.Server = Http.createServer(app.callback());
const io: IO.Server = IO(server);

// 自己抽离的模块, 所有聊天室相关的逻辑均放在此处处理
handleChat(io.of('/chat'));
  • chatCreate.ts
展开代码
1
2
3
4
5
6
7
8
9
10
import * as IO from 'socket.io';

export function handleChat(chatNamespace: IO.Namespace) {
// 关键点在这里
chatNamespace.on('connection', () => {
socket.on('sendChatSingleMessage', async (messageInfo: ISingleChatMessageProps) => {
// ...
});
});
};

这样就导致了一个问题, 那就是每次刷新服务器, 都会追加一个新的connection监听事件, 逐步递增, 所以就出现了每次刷新之后, 请求执行多次的问题.

解决办法:

参考cnode社区的一篇讨论: https://cnodejs.org/topic/518e0a7563e9f8a5427cefa6

很简单, 只需要将io.on('connection', xxx)提取到server.ts中就行了:

展开代码
1
2
3
io
.of('/chat')
.on('connection', handleChat);

3. socket.io无法实时接收发送信息, 需要同时刷新服务器和客户端

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

产生原因:

在前台使用socket.io-client的方式有问题:

展开代码
1
2
3
4
5
6
7
8
9
10
11
import * as IO from 'socket.io-client';

// 只初始化一次, 就会出现这个问题
const chatSocket = IO('ws://localhost:8888/chat');


function handleClientToSent() {
chatSocket.on('connect', () => {
...
});
}

解决办法:

只需要将初始化新的连接相关代码, 放入点击事件中即可:

展开代码
1
2
3
4
5
6
7
8
9
import * as IO from 'socket.io-client';

function handleClientToSent() {
const chatSocket = IO('ws://localhost:8888/chat');

chatSocket.on('connect', () => {
...
});
}

4. 客户端的socket监听事件会触发2^n

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

产生原因:

每一次socket.on, 都会添加一个新的callback到事件列表, 进而导致执行多次.

解决办法:

socket.on监听之前, 使用socket.removeAllListeners()移除掉所有的监听事件.

展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
React.useEffect(() => {
// ? 先移除所有的监听器, 避免出现指数增长的情况
chatSocket.removeAllListeners();

// * 接收聊天信息
// ? 不能在componentDidMount时监听, 只会监听一个聊天会话
chatSocket.on('receiveChatSingleMessage', (message: any) => {
setState({
...state,
singleChatInfo: {
...state.singleChatInfo,
message: state.singleChatInfo.message.concat(message),
},
});
});
});

5. 在componentDidMount中设置的socket.on监听, 只能作用于一个会话, 因为componentDidMount只执行了一次, 变化的只是传递的路由参数

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

产生原因:

视图组件(单聊群聊)只挂载了一次, 并且在componentWillUnmount之时不会卸载, 所以componentDidMount只执行一次, 进而监听的事件总是第一个聊天视图.

解决办法:

React.useEffect中监听, 即在组件每次更新之后监听即可:

展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
React.useEffect(() => {
// ? 先移除所有的监听器, 避免出现指数增长的情况
chatSocket.removeAllListeners();

// * 接收聊天信息
// ? 不能在componentDidMount时监听, 只会监听一个聊天会话
chatSocket.on('receiveChatSingleMessage', (message: any) => {
setState({
...state,
singleChatInfo: {
...state.singleChatInfo,
message: state.singleChatInfo.message.concat(message),
},
});
});
});

6. 一次单聊会话中, 接收方的所有好友都能收到本次独立会话的消息

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

出现原因:

后端使用了io.emit, 是向所有连接了(ws://localhost:8888/chat)的socket发射消息, 所以导致所有的前台用户都能接收到消息

解决方式:

前台根据接收到的消息的chat_id, 进行过滤, 并追加到自己的消息列表:

展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
chatSocket.on('receiveChatSingleMessage', (message: any) => {
// 过滤当前chatId的会话消息
const chatId = props.match.params.id;
const newMessage = message.chat_id === chatId
? state.singleChatInfo.message.concat(message)
: state.singleChatInfo.message;

setState({
...state,
singleChatInfo: {
...state.singleChatInfo,
message: newMessage,
},
});
});

7. Firefox下报Websocket无法连接的错误

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

出现原因:

火狐处于自身的安全策略, 对Websocket的连接较为严格, 所以会导致其出现下面的错误:

展开代码
1
2
3
Firefox 无法建立到 ws://localhost:8888/socket.io/?EIO=3&transport=websocket&sid=XPfob2_McBjC9WniAAEl 服务器的连接。

载入页面时与 ws://localhost:8888/socket.io/?EIO=3&transport=websocket&sid=XPfob2_McBjC9WniAAEl 的连接中断。

解决方式:

  1. 首先按照网上的步骤来, 通过火狐的about:config页, 修改默认的设置
  2. 接着需要在代码上修改, 由于我在组件componentWillUnmount之时, 手动关闭了socket连接, 所以导致了火狐报错, 但是ChromeIE显示正常, 很奇怪. 如果想正常使用, 将componentWillUnmount的代码删掉即可.

8. 重复创建了多个重复的websocket连接

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

问题描述:

之前一直没注意, 直到今天看了下Chrome控制台有关Ws的内容, 吓了一跳, 每一次刷新页面, 都会重复地创建多个websocket连接, 赶紧看了下文档:

正常情况下, 只会创建一个连接, 可以通过forceNew配置项来强制创建新的连接.

出现原因:

看了下代码, 问题就出在前端代码的连接部分, 我在每个组件的state中都初始化了一个Socket, 详情可以看下: commit@6fe6437e146a472dcf398ecc4b919df1fe692b5c

导致每次组件componentDidMount都会初始化一个新的链接, 可以参考官方文档:

https://socket.io/docs/client-api/#io-url-options

文档有一句话说的很清楚:

Note: reusing the same namespace will also create two connections

解决方式:

很简单, 将所有的创建socket相关的逻辑放置于单个文件, 使用时引入即可, 避免创建多个.

详情可查看: https://github.com/ddzy/ts-web-diary/blob/master/src/services/websocket.tsx

9. socket.removeAllEventListener引发的惨案

相关依赖:

  • socket.io@^2.2.0
  • socket.io-client@^2.2.0
  • @types/socket.io@^2.1.2
  • @types/socket.io-client@^1.4.32

问题描述:

今天调试聊天室功能的时候, 发现一个问题: 发送单聊消息的时候, 聊天历史列表不能同步更新

出现原因:

细心排查了一下, 发现了问题所在:

展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
React.useEffect(() => {
_setSingleChatMessageInfo();
});

function _setSingleChatMessageInfo() {
// 先移除所有的监听器, 避免出现指数增长的情况
chatIOClient.removeEventListener('receiveChatSingleMessage');

chatIOClient.on('receiveChatSingleMessage', (message: any) => {
const chatId = props.match.params.id;
const newMessage = message.chat_id === chatId
? state.singleChatInfo.message.concat(message)
: state.singleChatInfo.message;

setState({
...state,
singleChatInfo: {
...state.singleChatInfo,
message: newMessage,
},
isMessageSend: true,
});
});
}

问题就处在chatIOClient.removeEventListener这句代码上, 由于每次接收或者发送一个单聊消息, 都会监听一个新的事件回调, 所以我在每次监听之前, 先清除掉所有旧的处理器. 而由于聊天历史列表用的是同一个Socket连接, 所以就导致了其中监听的事件被一并清除.

解决方式:

使用removeEventListener()代替, 只注销掉当前的事件监听器即可.

修改后的代码:

展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function _setSingleChatMessageInfo() {
// 先移除所有的监听器, 避免出现指数增长的情况
chatIOClient.removeEventListener('receiveChatSingleMessage');

// * socket处理接收聊天信息
// ? 不能在componentDidMount时监听, 只会监听一个聊天会话
chatIOClient.on('receiveChatSingleMessage', (message: any) => {
// 过滤当前chatId的会话消息
const chatId = props.match.params.id;
const newMessage = message.chat_id === chatId
? state.singleChatInfo.message.concat(message)
: state.singleChatInfo.message;

setState({
...state,
singleChatInfo: {
...state.singleChatInfo,
message: newMessage,
},
isMessageSend: true,
});
});
}