Node聊天室和socket.io原理与功能总结

导语:前几天做了一个简易的聊天室,实现了聊天功能,聊天内容可以发送文本,图片,音频,视频,表情包等内容,支持一对一聊天,群组聊天。现在就之前的聊天室功能做一个简单的梳理和总结。

# 目录

  • 原理简述
  • 功能开发
  • 效果体验

# 原理简述

这次使用了socket.io这个工具包进行通信。

# webscoket

html5中有websocket的功能,参考这篇文章《html知识总结之WebSocket》 (opens new window)了解更多基础知识。

WebSocket是一种在单个 TCP 连接上进行全双工通信的协议, 能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

课外科普:

通信分类分为并行通信,串行通信,同步/异步,单工/双工,半双工/全双工。

  • 并行通信指的是数据的各位同时在多根数据线上发送或接收。控制简单,传输速度快;由于传输线较多,适用于短距离通信。
  • 串行通信是指数据的各位在同一根数据线上逐位发送和接收。控制复杂,传输速度慢;只需要一根数据线,适用于远距离通信。
    • 根据对数据流的分界、定时以及同步方案方法不同,可分为和同步串行通信方式和异步通信方式。
    • 根据串行数据的传输方向,我们可以将通信分为单工,半双工,双工。
      • 单工:是指数据传输仅能沿一个方向,不能实现反向传输。
      • 半双工:是指数据传输可以沿两个方向,但需要分时进行传输。
      • 全双工:是指数据可以同时进行双向传输。

# socket.io

socket.io是基于websocket协议的一套成熟的解决方案。优点是性能好,支持多平台;缺点是传输的数据并不完全遵循websocket协议, 这就要求客户端和服务端都必须使用socket.io的解决方案。

# 区别

  • http和webscoket都是基于tcp;
  • http建立的是短连接;
  • websocket建立的是长连接;

# 功能开发

现在就这个功能进行分析并且开发前端和后端内容,先来打通后端部分,为前端连接socket服务作准备。

下面这个展示的是最基础的聊天室,包括以下几个功能:

  • 多人聊天
  • 显示用户名和人数
  • 回车发送
  • 到顶部

# 后端方面

这里就是如何在node中建立socket服务器。

# 安装依赖包

npm install -g express-generator
express --view=ejs chat
cd chat
npm install
npm install socket.io
1
2
3
4
5

# 配置socket.io

打开bin/www文件,在var server = http.createServer(app);下面一行写入以下内容。

下面内容就分开介绍各个内容,不做全部代码粘贴。

  • 引入ws服务
const ws = require('socket.io');
const io = ws(server, {
  path: '/chat',
  transports: [
    'polling',
    'websocket'
  ]
})
1
2
3
4
5
6
7
8

# 常用方法

  • 连接和断开连接
io.on('connection', socket => {

    console.log('a user connected!');

    //disconnect
    socket.on('disconnect', () => {
        console.log('a user disconnected!');
    })
}
1
2
3
4
5
6
7
8
9
  • 加入和离开房间
// join room
socket.join(roomId);

// leave room
socket.leave(roomId);
1
2
3
4
5
  • 接受消息
socket.on('event name', data => {
    // data
}
1
2
3
  • 发送消息
socket.emit('event name', {
    // some data
});
1
2
3
  • 向其他人广播
socket.broadcast.emit('event name', {
    // some data
});
1
2
3
  • 向某个房间发送消息
io.to(roomId).emit('event name', {
    // some data
})
1
2
3

# 简易程序

let roomInfo = {};

io.on('connection', socket => {
  let roomId = socket.handshake.query.roomId;

  // user login
  socket.on('login', data => {
    
    socket.join(roomId);

    if (!(roomId in roomInfo)) {
      roomInfo[roomId] = [];
    }

    let names = [];
    let users = roomInfo[roomId];
    if (users.length) {
      for (let i = 0; i < users.length; i++) {
        names.push(users[i].name);
      }
      if (!(names.includes(data.user))) {
        users.push({
          id: socket.id,
          name: data.name,
          avatar: data.avatar
        })
      }
    } else {
      roomInfo[roomId].push({
        id: socket.id,
        name: data.name,
        avatar: data.avatar
      });
    }

    console.log('roomInfo: ', roomInfo);

    io.to(roomId).emit('system', {
      name: data.name,
      users: roomInfo[roomId]
    })

  })

  // client msg
  socket.on('message', data => {
    io.to(roomId).emit('chat', data);
  })

  // leave room
  socket.on('leave', data => {
    let users = roomInfo[roomId];
    if (users && users.length) {
      for (let i = 0; i < users.length; i++) {
        const user = users[i];
        if (data.name == user.name) {
          users.splice(i, 1);
        }
        
      }
    }

    socket.leave(roomId);

    io.to(roomId).emit('logout', {
      name: data.name,
      users: roomInfo[roomId]
    })

    console.log('roomInfo: ', roomInfo);

  })

  socket.on('disconnect', () => {
    console.log('a user disconnect!');
  })

});
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

# 前端方面

  • 引入socket.io.js文件
<script src="./js/socket.io.js"></script>
1
  • html部分

登录界面:

<div class="chat">
  <h2>XQ聊天室</h2>
  <form class="chat-form">
      <p>
          <label for="name">昵称:</label>
          <input type="text" id="name" name="name" placeholder="请输入用户名" required>
      </p>
      <p>
          <label for="avatar">头像:</label>
          <select name="avatar" id="avatar" required>
              <option value="avatar1">头像1</option>
              <option value="avatar2">头像2</option>
              <option value="avatar3">头像3</option>
          </select>
      </p>
      <p>
          <label for="roomId">房间:</label>
          <select name="roomId" id="roomId" required>
              <option value="1">房间1</option>
              <option value="2">房间2</option>
              <option value="3">房间3</option>
          </select>
      </p>
      <p>
          <input type="submit" value="进入房间">
      </p>
  </form>
</div>
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

房间界面:

<div class="room">
    <div class="room-header">
        <h3>XQ聊天室(<span class="count">0</span>)</h3>
        <button class="logout">退出</button>
    </div>
    <div class="room-nav">
        <small>在线人数:</small>
        <span id="room-users">暂无成员</span>
    </div>
    <ul class="room-content">
    </ul>
    <div class="room-footer">
        <input class="room-ipt" type="text" placeholder="随便写点儿吧">
        <input class="room-btn" type="submit" value="发送">
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • css部分
body {
    margin: 0;
    padding: 0;
    background: #f9f9f9;
}

h1,h2,h3,h4,h5,h6,p {
    margin: 0;
}

ul,li {
    margin: 0;
    padding: 0;
    list-style: none;
}

.chat {
    box-sizing: border-box;
    margin: 50px auto;
    padding: 20px;
    width: 300px;
    height: auto;
    background: #fff;
}

.chat.active {
    display: none;
}

.chat h2 {
    margin-bottom: 10px;
    font-size: 18px;
    line-height: 1.5;
    text-align: center;
}

.chat-form p {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 5px 0;
    font-size: 15px;
    line-height: 35px;
}

.chat-form p label {
    width: 50px;
}

.chat-form p input,
.chat-form p select {
    flex: 1;
    box-sizing: border-box;
    padding: 0 10px;
    height: 30px;
    border: 1px solid #ccc;
    outline: none;
    background: none;
}

.chat-form p input:focus,
.chat-form p select:focus {
    box-shadow: 0 0 5px #ccc;
}

.room {
    display: none;
    width: 100%;
    height: 100vh;
    overflow: hidden;
}

.room.active {
    display: flex;
    flex-direction: column;
}

.room-header {
    position: relative;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    padding: 0 15px;
    height: 60px;
    background: #111;
    color: #fff;
}

.room-header h3 {
    font-size: 18px;
    text-align: left;
}

.room-header button {
    width: 50px;
    height: 50px;
    background: none;
    color: #fff;
    outline: none;
    border: none;
    text-align: right;
}

.room-nav {
    box-sizing: border-box;
    padding: 20px 15px;
    line-height: 30px;
    font-size: 14px;
}

.room-nav small,
.room-nav span {
    font-size: 14px;
}

.room-nav span {
    color: rgb(6, 90, 146);
}

.room-content {
    flex: 1;
    padding: 15px 0;
    background: #fff;
    border-top: 1px solid #ccc;
    border-bottom: 1px solid #ccc;
    background: #eee;
    overflow-x: hidden;
    overflow-y: auto;
}

.room-content li {
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
    box-sizing: border-box;
    padding: 15px 10px;
    margin-bottom: 10px;
    width: 100%;
}

.room-content li .room-user {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.room-content li img {
    display: inline-block;
    width: 40px;
    height: 40px;
}

.room-content li span {
    font-size: 14px;
}

.room-des {
    position: relative;
    margin-top: 5px;
    margin-left: 10px;
    box-sizing: border-box;
    padding: 3px 5px;
    font-size: 14px;
    line-height: 30px;
    background: #ccc;
    border-radius: 5px;
}

.room-des::before,
.room-des::after {
    content: '';
    position: absolute;
    top: 10px;
    width: 0;
    height: 0;
    border: 5px solid transparent;
}

.room-des::before {
    display: inline-block;
    left: -10px;
    border-right: 5px solid #ccc;
}

.room-des::after {
    display: none;
    right: -10px;
    border-left: 5px solid #fff;
}

.room-me {
    flex-direction: row-reverse;
}

.room-me .room-des {
    margin-left: 0;
    margin-right: 10px;
    background: #fff;
}

.room-me .room-des::before {
    display: none;
}

.room-me .room-des::after {
    display: inline-block;
}

.room-content .system {
    justify-content: center;
    align-items: center;
    padding: 0;
    height: 35px;
    line-height: 35px;
}

.system p {
    box-sizing: border-box;
    padding: 0 5px;
    font-size: 14px;
    text-align: center;
    border-radius: 5px;
    background: #ccc;
}

.room-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 50px;
    background: #fff;
    border-top: 1px solid #ccc;
}

.room-footer .room-ipt {
    margin-top: 1px;
    box-sizing: border-box;
    padding: 10px;
    width: 80%;
    height: 48px;
    background: none;
    border: 1px solid transparent;
    outline: none;
}

.room-footer .room-ipt:focus {
    border: 1px solid #ccc;
    box-shadow: 0 0 5px #ccc;
}

.room-footer .room-btn {
    width: 19%;
    height: 100%;
    background: rgb(2, 54, 112);
    border: 1px solid #ccc;
    outline: none;
    font-size: 15px;
    color: #fff;
}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
  • js部分

登录界面的js

let chat = document.querySelector('.chat');
let chatForm = document.querySelector('.chat-form');
let user = document.querySelector('#name');
let avatar = document.querySelector('#avatar');
let roomId = document.querySelector('#roomId');

// io
let socket = io.connect('/', {
    path: '/chat'
});

// login
chatForm.onsubmit = function(){
    let userInfo = {
        name: user.value,
        avatar: `/img/${avatar.value}.webp`,
        roomId: roomId.value
    }
    localStorage.setItem('userInfo', JSON.stringify(userInfo));
    checkLogin();
    return false;
};

checkLogin();

function checkLogin () {

    let userInfo = localStorage.getItem('userInfo');
    userInfo = JSON.parse(userInfo);

    if (userInfo && userInfo.name) {
        chat.classList.add('active');
        room.classList.add('active');
        socket.emit('login', userInfo);

    } else {
        chat.classList.remove('active');
        room.classList.remove('active');
    }

}
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

房间部分的js

let room = document.querySelector('.room');
let logout = document.querySelector('.logout');
let count = document.querySelector('.count');
let roomUsers = document.querySelector('#room-users');
let roomContent = document.querySelector('.room-content');
let roomIpt = document.querySelector('.room-ipt');
let roomBtn = document.querySelector('.room-btn');

// 退出登录
logout.addEventListener('click', function(){
    let userInfo = localStorage.getItem('userInfo');
    userInfo = JSON.parse(userInfo);
    socket.emit('leave', userInfo);
    alert('退出成功!');
    localStorage.removeItem('userInfo');
    checkLogin();
})

roomIpt.addEventListener('keyup', sendMsg, false);
roomBtn.addEventListener('click', sendMsg, false);

// 发送消息 
function sendMsg (e) {
    if (e.type === 'click' || e.code === 'Enter') {
        let val = roomIpt.value;
        if (val == '') {
            alert('聊天内容不能为空!');
            return false;
        }
        let userInfo = localStorage.getItem('userInfo');
        userInfo = JSON.parse(userInfo);
        userInfo.msg = val;
        roomIpt.value = '';
        socket.emit('message', userInfo);
        goBot();
    }
}

goBot();

// 到底部
function goBot () {
    roomContent.scrollTop = roomContent.scrollHeight;
}

// 系统消息提示
function welcome (user = 'mark', type = 1) {
    roomContent.innerHTML += `
        <li class="system">
            <p>系统消息:<strong>${user}</strong>${type == 1 ? '来到' : '离开'}本房间!</p>
        </li>
    `;
    goBot();
}

// 系统消息
socket.on('system', data => {
    let strs = '';
    welcome(data.name);
    count.innerText = data.users.length;
    for (const item of data.users) {
        strs += item.name + ',';
    }
    roomUsers.innerText = '';
    roomUsers.innerText += strs;
})


// 退出提醒
socket.on('logout', data => {
    let strs = '';
    welcome(data.name, 2);
    count.innerText = data.users.length;
    for (const item of data.users) {
        strs += item.name + ',';
    }
    roomUsers.innerText = '';
    roomUsers.innerText += strs;
})

// 接受消息
socket.on('chat', data => {
    let userInfo = localStorage.getItem('userInfo');
    userInfo = JSON.parse(userInfo);
    let isUser = data.name == userInfo.name;
    roomContent.innerHTML += `
        <li ${isUser ? 'class="room-me"' : ''}>
            <div class="room-user">
                <img class="room-avatar" src="/chatroom/${(isUser ? userInfo.avatar : data.avatar ) || '/img/avatar1.webp'}" alt="">
                <span class="room-name">${isUser ? userInfo.name : data.name }</span>
            </div>
            <p class="room-des">${data.msg}</p>
        </li>
    `;
    goBot();
})
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 效果体验

终于做好了,接下来来体验一下网上冲浪--XQ聊天室的美好生活吧!

  • 进入房间

首先,输入你自己的昵称,选择好头像和房间,点击进入房间按钮对话。

chat

  • 发送消息

然后,输入消息内容,点击发送按钮,或者按Enter回车也可以。

chat

可以打开一个隐私无痕窗口或者新的游览器,打开网址,输入另一个测试账号进行

这是jerry登录后的界面

chat

这是mark看到的jerry发来的消息

chat

  • 退出登录

如果聊天结束,可以点击右上方退出聊天室。

chat

这是jerry退出登录后, mark看到的界面

chat

再来看一下后端打印出的用户信息。

  • 这是mark登录以后记录的信息

chat

  • 这是jerry登录以后记录的信息

chat

  • 这是jerry退出以后记录的信息

chat

以上就是一个简易的聊天室和node中websocket的知识总结。

分享至:

  • qq
  • qq空间
  • 微博
  • 豆瓣
  • 贴吧