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

1,060 阅读6分钟

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

目录

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

原理简述

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

webscoket

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

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

配置socket.io

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

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

  • 引入ws服务
const ws = require('socket.io');
const io = ws(server, {
  path: '/chat',
  transports: [
    'polling',
    'websocket'
  ]
})

常用方法

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

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

    //disconnect
    socket.on('disconnect', () => {
        console.log('a user disconnected!');
    })
}
  • 加入和离开房间
// join room
socket.join(roomId);

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

简易程序

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!');
  })

});

前端方面

  • 引入socket.io.js文件
<script src="./js/socket.io.js"></script>
  • 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>

房间界面:

<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>
  • 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;
}
  • 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');
    }

}

房间部分的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();
})

效果体验

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

  • 进入房间

首先,输入你自己的昵称,选择好头像和房间,点击进入房间按钮对话。 在这里插入图片描述

  • 发送消息

然后,输入消息内容,点击发送按钮,或者按Enter回车也可以。 在这里插入图片描述

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

这是jerry登录后的界面 在这里插入图片描述

这是mark看到的jerry发来的消息 在这里插入图片描述

  • 退出登录

如果聊天结束,可以点击右上方退出聊天室。 在这里插入图片描述

这是jerry退出登录后, mark看到的界面 在这里插入图片描述

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

  • 这是mark登录以后记录的信息 在这里插入图片描述

  • 这是jerry登录以后记录的信息 在这里插入图片描述

  • 这是jerry退出以后记录的信息 在这里插入图片描述

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