使用websocket开发一个音乐聊天室

2021 年 11 月 24 日

nodejs
websocket

为什么做

工作中经常看到别人使用和接触websocket、但是自己的工作又用不上、于是便想着做一个个人项目来学习websocket、恰巧看到了一个用websocket打造的音乐聊天室项目、于是便从零开始开发了这样一个音乐聊天室大厅,想记录下一个大概的个人项目成型,也顺便分享与大家。

websocket 和 http 的区别

在我们日常的开发中,接触最多的就是http协议了, http协议是用在应用层的协议,他是基于tcp协议的,http协议建立链接也必须要有三次握手才能发送信息。在这种场景下,服务端是被动的,他不能去对客户端操作与通信,只能等待客户端主动发起,这种场景也很多件,在日常开发中,部分时候需要用到轮询便可凸显这种通信在某些场景的弊端,于是websocket应运而生了。

WebSocket他是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。 建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以发送信息到服务器。而且信息当中不必在带有head的部分信息了与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。

技术选型

常常我们在自己开发一个东西是,写代码的时间是很少的,真正更多的时间是在思考,项目怎么做,用什么技术,如何组装等等,只要思考好这些问题,真正开写的时候就会很快了。

个人的习惯一直是把前后端分开来写,所以首先的想法是把项目分为前后端两端两个项目,前端选择了目前公司在使用的技术栈vue,后端则是选择了每一个前端都能快速上手的node。同时由于相对于websocket,我选择了封装更为完善的socket-io。两者区别不大。大致来讲socket-io是对websocket的封装,但也不完全正确,Socket.ioWebsocket轮询(Polling)机制以及其它的实时通信方式封装成了通用的接口,其日常使用大同小异。

  • 前端
    1. 使用前端框架 vue进行基本前端开发
    2. 使用socket-io-client 替代websocket进行双工通信
    3. 使用套件vue-socket.io-extended,对socket-iovue的一个封装集成,方便在开发中更为方便使用
  • 后端
    1. 使用node框架nestjs进行后端开发(因为之前都是用expresskoa等开发个人项目,公司项目用hapi,个人感觉,不同框架确实有不同的感受,expresskoa这类框架官方并没有帮你强行约束你的开发,没有统一的规范,会由于不同人的开发爱好不大相同,而nest的定义是一个渐进式的Node.js框架,用于构建高效,可靠和可扩展的服务器端应用程序;不要问我为什么要放图,据说放图可以提高访问量。,所以也是来尝试使用了nestjs来进行本次开发)
    2. 数据库使用了个人使用最多mysql
    3. orm使用了nestjs配套的typeorm。(个人感觉typeorm没有之前自己使用的sequelize好用),当然,作为前端,本身对后端可能不是特别了解,这也是全凭个人喜好而定了。
    4. 作为音乐聊天室,当然离不开曲库了,歌曲来源是通过爬虫获取xx音乐网站实现的

项目大致思路

要想打造一个音乐聊天室,浅而已见,需要两个东西,音乐,聊天功能,要想实现这两个功能,我们分个顺序,先实现聊天,在聊天的基础上再去实现音乐。

  1. 要想实现聊天功能,前后端的大题流程是,前端发起一个连接请求,由后端来处理连接,记录连接用户,并保留住个人的基本信息,用于分发给其他进入聊天室的用户。
  2. 项目的权限验证依然使用的jsonwebtoken但是这个的思路和我们日常的验证稍有差异
  3. 当我们连接成功后就要开始播放音乐,而要想所有人听到的歌都是同步的,那么也就意味者控制歌曲的播放需要后端来做而不是前端了,那么如何后端控制音乐的播放呢也是一个问题
  4. 同时我们需要实现哪些功能呢,聊天发文字消息、发表情、发图片、复制粘贴发送图片、点歌、切歌、顶歌、等等功能,我们逐一来实现吧

实现功能

一、前后端权限校验

我们日常使用前后端交互的时候都是会在请求头携带token,而这个操作呢,我们一般是通过axios请求拦截进行全局操作,那么我们对socket如何操作呢,首先,我们依然离不开token我们的校验最终还是要用它进行校验,所以就离不开登录,我们登录后拿到token携带在socket请求中,在前端项目中,我们一般只会维护一个socket实例,我们来看看,初始化的时候需要哪些东西吧。

socketio官网 初始化的参数基本就是这样,这里的套件有什么用呢?其实就是当我们使用这个套件后,首先$socket就挂载在了Vue的原型上,其次,我们就可以在组件使用的时候,定义一个和methods,data同级下的sockets,我们就可以和methods一样,在下面定义所有socket-io来自服务端的事件了,如果接触过wensocket,我们知道,服务端与客户端是通过事件交流的,双方会触发不同的事件去告诉对方需要做什么,在这里有个问题,我们的token加在哪里,官方文档也没有找到具体的方法,首先我们知道,这里的token并不能在axios统一全局加,那么就需要我们自己去处理了。

首先,当我们使用socket的时候,我们就要抛弃传统的http方法,开始一个新的概念,在这里,是初始化的时候就会创建的,我们在初始化的时候,用户还没登录,又怎么会有token呢,所以很明显,token的注入时机一定是连接前,并且登陆后,所以初始化这里我们就可以跳过了,来到连接阶段。

频繁的让服务端去验证会消耗不必要的性能,前端首先判断,没有token强制用户登录,之后才能去连接,如何连接呢?

我们在连接前把token放入this.$socket.client.io.opts.query,也可以放入到请求头中,看个人需求,这两点都可以,我们只是为了在请求连接的过程中,携带上token,这时我们就可以控制传入的参数,当然除了token你也可以携带更多的参数,看看有无必要,然后服务端就可以在requestquery中拿到token进行验证了。这样,我们就可以自由控制连接时机和参数了,服务端的校验和普通http无差别,校验不通过拒绝连接即可。

socket中,我们无需每次请求都携带token,我们只需要在连接的这一次携带即可,后续连接通过后,会生成一个固定的连接id,cliend.id,这是双方连接的唯一凭证信息,也是通过这个和你进行通信的,那么我们的权限就到这里就完成了。

二、聊天室消息通信交流

作为一个聊天室,最基本的功能就是聊天了,我们如何进行聊天呢,前面我们说到,socket的通信实际就是响应各种事件,简单理解就是我们定义一些方法,会在双方发送事件的过程中触发,如何发送事件呢,客户端和服务端是稍有区别的

  • 客户端 this.$socket.client.emit('CustomEvent', data)这里便是像客户端发送一个事件,名字叫做CustomEvent并且携带了data数据,那么发送完这个请求后,服务端就会执行定义的CustomEvent方法,同时接收到数据data。例如下图,接收到了一条消息

  • 服务端呢大体相同却又不同,为什么这么说呢,因为,在客户端,我们是一对一,我们的目标只有服务端,所以我们只能对服务端发送事件,而在服务端则不同,有多个客户端连接他,那么他就是多对一,他要为多人符文,所以,他面向的事件场景分为三类,

    1. 像当前连接的一个客户端发送事件通知
    1. 像除开当前连接用户的其他所有人发送消息
    1. 像包含当前连接客户端的所有用户发送消息
    

    很好理解,我们不可能所有消息都要通知给每个人,我们是需要分发给不同人消息的,有了这样一个概念,你就能快速理解了

三、基本交互流程方式

一般而言呢,我们日常的聊天内容都会存在DB项目的源码里面已经绑定了一个数据库了,这里我使用的是mysql,这些都大同小异了,我们不需要过多关注,我们来大致分析一下一个用户发送消息后需要做哪些事情。

 1. 当用户A连接进入房间的时候,首先我们要把房间的初始信息给与用户,一个基本的聊天室有哪些信息呢?
      1. 历史用户聊天消息
      2. 房间信息,房间公告,其他房间自定义设置
      3. 当前所有在线用户列表,包含用户的一些基础信息,例如性别,签名等等
      4. 歌曲信息,当前正在播放的歌曲,播放到多少秒了,从什么时候开始播放呢?【这些后面聊】
 1. 用户进入房间后发送一条消息,服务端接收到消息,首先需要把消息存入`db`以便存储历史记录,然后把此条消息再通知给所有人,然后所有客户端会接收到有新消息来了的通知,就会吧新来的消息`push`进当前的消息列表,这样不论谁发消息,都只需要往数组加就好了。
 1. 这就是一个聊天的基本流程

单纯的聊天当然不仅仅只有文字消息了,我们同样有表情,图片,或者微信中我们文字加小表情,如何实现呢,我们逐一分析

  1. 小表情,对于我们而言其实只是一个小的icon或者图片,所以我们选择的时候其实就是选择了一张图片,一般呈现形式如下

我们发现点击表情后变成了这种格式,其实如果你观察过以前的QQ你会发现很多其实都是/大笑类似这样的格式,他其实就是把图片的路径转为这样一个代键值,在发送出去的时候通过v-html或者其他方式把他再编译为一个img标签即可,这样就实现了这样的小表情和文字在一起的效果 ,发送出去如下

  1. 第二个我们需要发送表情包呢,那么同理,表情包对于我们而言也是一个图片,一个图片地址罢了,不同的是,表情包我们不需要他停留在消息输入栏,这样更简单了,直接点击表情包,就发送一个图片链接即可,而要对消息区分,只需要给消息再加上一个类型,例如text文字类型,img图片类型notice消息通知类型,这样就言简意赅了,通过不同的类型渲染不同样式就轻松区分了,例如下图

  1. 第三个便是,如果用户要发送自己的图片消息怎么做呢,我们知道,图片要发送都是粘贴过来或者发送文件的,本质呢都是发送一个文件,怎么操作呢?input中有一个事件就是粘贴事件@paste,在这其中e.target.file就可以拿到粘贴的图片信息呢,然后通过文件上传接口把文件上传到远端,通过接口拿到一个返回的图片地址,把这个图片地址发给服务端就完成了自定义的图片信息费发送,那么视频呢,当然也是同理了,这样是不是清晰了呢。

四、音乐功能

作为音乐聊天室,除了聊天,第二点当然是音乐功能了,对于h5而言,h5就是一个video或者audio,这点大家都知道就不用不说了,我们只需要src引入资源地址就可以播放了,非常简单,但是我们当然不只是为了简单听歌了,我们想要实现一个人,所有人一起点歌,然后一起按顺序播放的功能,大家进入聊天室听到的都是一个相同时间的歌曲,对于这个功能,客户端只需要两件事情,

  • 前端思路
    1. 知道现在在播放什么歌曲,歌曲的资源地址是什么。
    2. 当前播放歌曲到哪一秒了,用户进入就要从当前大家一起的这个时间开始播放,同步播放
    3. 大致流程是,进入房间或者房间信息状态,当前歌曲,歌曲开始播放时间,加载歌曲,跳到当前播放时间开始播放
    4. 需要注意的是,目前由于浏览器限制,是不能自动播放音乐的autopalay也不会生效,需要和用户有交互才能播放,所以在进入房间前,有弹窗让用户确认,实现进入房间就会播放的功能。
  • 后端思路
    1. 后端首先呢,需要歌曲资源了,我们需要用爬虫,在初始化阶段就拿到一部分歌曲作为,没人点歌的时候随机播放的音乐,这部分在源码的初始化阶段有详细注释,看个人爱好愿意初始化加载多少音乐。
    2. 播放歌曲的时间是有服务端控制的什么时候自动切换歌曲也是,所以服务端需要知道歌曲什么时间该切换,同时保证歌曲一直有,那么我们需要的是,项目启动的时候就开始播放音乐,如何操作呢,其实就是随机从数据库拿到一首歌曲,然后开始记录,记录当前的歌曲,然后当前歌曲的时间,当前歌曲的资源地址等等,用户进入房间就推送给用户,但是用户进入房间的时候怎么知道当前是多少秒呢,所以,我们从数据库拿到歌曲的时候需要记录一个时间戳timespace,用户进入房间之后,拿进入时间减去记录的时间戳就是歌曲播放时间,歌曲从这个时间播放就好了
    3. 那么什么时间切歌呢,自动的切歌当然需要歌曲播放完毕就切换啊,歌曲什么时候播放完毕呢?就是一首歌的时间呢,在拿到歌曲信息的时候也知道了歌曲时间,只要设置一个定时器,在歌曲时间这么多秒后执行切歌的方法就好了呀,同时,在切换的时候再次更新时间戳,我们就实现了一个自动切歌的功能了
    4. 当然,我们还需要用户点歌操作,如何实现呢,歌曲的搜索同样需要使用爬虫,进行搜索歌曲,搜索到歌曲之后,用户点歌会把当前歌曲id和发送给服务端,服务端会记录谁点了什么歌,当然,我们也需要有先来后到之分,所以我们需要维护一个队列,会按照顺序依次加入点歌用户的歌曲,这个时候,自动切歌就不会去数据库读取了,大致流程是,查看队列有没有用户点的歌曲,没有在数据库随机获取一个,有的话拿到队列第一首歌曲,然后切歌,再移除掉队列的歌曲,就实现了点歌自动播放了。
    5. 那么有人点的歌很难听怎么办,或者用户自己想移除自己点的歌呢,都很简单了,拿到歌曲id和用户信息,从队列里去掉就好了,大致思路就是如此呢,具体细节呢还有很多,可优化的点也有很多,期待大家提issues指出来吧

五、总结

抱着学习的态度去学习也许很慢、抱着兴趣的态度去学习就会事半功倍、把学习当做娱乐、打怪升级就会很有意思了、一个小小的个人项目分享给大家、不足的地方欢迎大家提出来、一起互相学习进步吧。

交流专区 文明发言