前言

IM(Instant Messaging),也就是即时通讯。几乎所有对实时性要求高的应用场景都需要IM技术的运用。比如聊天、直播、弹幕、实时位置共享、协同编辑/在线文档、股票基金报价等。

本篇将带大家从零开始搭建实现一个轻量群聊的完整闭环。客户端用到的是vue+websocket通信,服务端用到是node的ws模块通信,redis用于便于快速读取在线状态等数据的存取,MongoDB聊天消息等持久数据存储。

已经实现的功能:进入聊天室,输入临时昵称用于聊天区分(前端是暂时是存在浏览器的sessionStorage里,后端作为唯一用户名存到了数据库,输入昵称保存会校验昵称是否存在,后面可以根据需要通过扩展加上登录等流程操作)

1. 正常群聊,保存消息记录到数据库

2. 用户进入离开以及发送消息都有广播提示

3. 消息发送失败的重试机制

 

了解Socket和Websocket

Socket 是IM技术的重要组成部分,Socket是为了方便使用TCP或UDP而抽象出来的一层,是对TCP/IP协议的封装,是位于应用层和传输控制层之间的一组接口

Websocket是为了满足基于Web 的日益增长的实时通信需求而产生的,模仿socket的通信能力。但是和Socket不同的是,Websocket是基于TCP的应用层协议

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

传统http通信只能由客户端发起,做不到服务器主动推送消息,如果服务器有连续的状态变化,只能用轮询,而轮询非常低效,浪费资源,所以才有了websocket

它的的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。其他特点包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443 ,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

总结:Socket是抽象接口,WebSocket与HTTP都是一样基于TCP的应用层协议。WebSocket是双向通信协议,模拟Socket,可以双向发送或接受信息。HTTP是单向的。WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候不需要HTTP协议。

 

Websocket和Node ws的用法

WebSocket 的用法比较简单 Websocket Api

var ws = new WebSocket("ws://localhost:3000"); 

ws.onopen = function(evt) { // onopen 连接成功后的回调函数
  console.log("Connection open ..."); // socket连接
  ws.send("Hello WebSockets!"); // send()方法用于向服务器发送数据
};

ws.onmessage = function(evt) { // 指定收到服务器数据后的回调函数
  console.log( "Received Message: " + evt.data); // 接收服务端的信息
  ws.close(); // 主动断开和服务器的连接
};

ws.onclose = function(evt) {// onclose 连接关闭后的回调函数
console.log("Connection closed."); // socket连接断开 
};

ws.onerror = function(evt) {// onclose 连接错误的回调函数
console.log("Connection errored."); // socket因错误接断开连接,例如有些信息不能发送 
};

// 如果要指定多个回调函数可以用addEventListener
ws.addEventListener(\'open\', function (event) {
  ws.send(\'Hello Server!\');
});

readyState属性返回实例对象的当前状态,共有四种

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

 node ws 模块用起来也不难,参照 ws Api 文档 很快就能搭起server啦

/**
 * Create HTTP server.
 */
const server = http.createServer(app)

/**
 * new WebSocket.Server(options[, callback]) 
 * @param {Object} options
 * @param {String} options.host 要绑定的服务器主机名
 *  @param {Number} options.port 要绑定的服务器端口
 *  @param  {Number} options.backlog 挂起连接队列的最大长度.
 *  @param {http.Server|https.Server} options.server一个预创建的HTTP/S服务器  
 *  @param {Function} options.verifyClient 验证传入连接的函数。
 *  @param {Function} options.handleProtocols 处理子协议的函数。
 *  @param {String} options.path 只接受与此路径匹配的连接
 *  @param {Boolean} options.noServer 启用无服务器模式
 *  @param {Boolean} options.clientTracking 是否记录连接clients
 *  @param {Boolean|Object} options.perMessageDeflate 开启关闭zlib压缩(配置)
 *  @param {Number} options.maxPayload 最大消息载荷大小(bytes)
 *  @param callback {Function}
 * 1. 创建一个新的服务器实例。必须提供端口、服务器或NoServer中的一个,否则会引发错误。
 * 2. 如果端口被设置,则自动创建、启动和使用HTTP服务器。
 * 3. 要使用外部HTTP/S服务器,只指定服务器或NoServer。此时,必须手动启动HTTP/S服务器。
 * 4. NoSver模式允许WS服务器与HTTP/S服务器完全分离。这使得,可以在多个WS服务器之间共享一个HTTP/S服务器
 */
const WebSocket = require(\'ws\')
const wss = new WebSocket.Server({
  server
})

 

IM系统

IM按场景一般分为单聊和群聊。

单聊 

一个靠谱的IM系统,其核心就是消息的可靠性,及时触达,以及系统安全性。安全性方面考虑的话需要对会话内容进行加密。

消息的可靠性,即消息的不丢失、不重复和不乱序,是IM系统中的一个难点。满足这三点,才能有一个良好的聊天体验。

1. 普通消息投递流程

举个例子: 用户A给用户B发送“你好”的文本消息,流程图如下

 

  • 用户A向服务端发送一个消息请求包msg: R
  • 服务端在成功处理后,回复用户A一个ack消息响应包msg:A
  • 如果此时用户B在线,则服务调研主动向用户B发送一个消息通知包msg:N,如果用户B不在线,则消息会存储离线

 

客户端发送消息的消息体一般包括几个部分:

  • 消息类型(必填)对于IM系统来说消息类型肯定不止一种,前后端可统一定下消息类型,便于通信接收和识别
  • 消息唯一id(必填)前端生成消息id,后端存储的时候以及接收去重等都要根据唯一id来判断
  • 消息内容(非必填)可以是用户通过输入框或者上传文件等用户交互的方式发送消息,也可以是对用户进入离开行为的监测
  • 发送时间(非必填)以服务端时间为准
  • 发送方唯一标识(必填)
  • 接收方唯一标识(如果是单聊必填)

消息体R的格式

{
  msg_type: \'TEXT\', // 消息类型 文本消息
  msg_content: \'你好\', // 消息内容
  send_time: new Date().valueOf(),
  msg_id: uuid(), // 前端生成唯一id作为消息id
  from_id: \'liuyifei\',
  to_id: \'pengyuyan\'
}

服务端返回的消息体格式msg:A和通知给B的消息体格式msg:N

{
    code: 0,  // 状态码
    message: \'接收成功\',
    results: {
        msg_type: \'ACK\',
        msg_content: \'你好\',
        msg_id: \'xxx\',
        send_time:new Date().valueOf(),
        from_id: \'liuyifei\',
        to_id: \'pengyuyan\'
    } 
}

 

用户A收到msg:A的响应的时候,把响应的msg_id从待发送队列移除

 

2. 上述消息投递流程容易出现的问题

从流程图中可以看到,发送方用户A收到msg:A后,只能说明服务端成功接收到了消息,并不能说明用户B接收到了消息。在有些场景下,可能出现msg:N通知到B的包丢失,且发送方用户A完全不知道,例如:

  • 服务器崩溃,msg:N包未发出
  • 网络抖动,msg:N包被网络设备丢弃
  • 客户端B崩溃,msg:N包未接收

要想实现应用层的消息可靠投递,必须加入应用层的确认机制,即:要想让发送方用户A确保接收方用户B收到了消息,必须让接收方用户B给一个消息的确认,这个应用层的确认的流程,与消息的发送流程类似:

 

  • 客户端B向服务端发送一个ack请求包,即ack:R
  • 服务端在成功处理后,回复用户B一个ack响应包,即ack:A
  • 服务端主动向用户A发送一个ack通知包,即ack:N
  • 图中 msg:A是对msg:R的响应 ack:R是对ack:A的响应 ack:A是对ack:R的响应
用户A: “你好” (msg:R)
服务器:我收到消息了(msg:A),我转告给B (msg:N)
用户B: 收到了消息,并回应服务器说我知道了 (ack:R)
服务器:告诉B,好了我知道你收到消息了(ack:A)
服务器:通知A,B已经收到消息了(ack:N)
 

这样完成一个闭环,服务器作为中间人 确保发送方和接收方消息收发的正常,准确的触达。

这是用户B在线的情况,如果用户B不在线的话,服务器会假装B收到消息,返回ack给A,同时把离线消息存储,下次B上线的时候会拉取离线消息。

 

如果一切都正常,这样就完成一个完整的可靠的消息触达机制,但是现实总是有很多不稳定因素存在的,容易丢失各种消息

1. msg:R,msg:A 可能丢失:
此时直接提示“发送失败”即可,问题不大;

2. msg:N,ack:R,ack:A,ack:N这四个报文都可能丢失,此时用户A都收不到期待的ack:N报文,即用户A不能确认用户B是否收到“你好”。

一个解决办法:用户A发出了msg:R,收到了msg:A之后,在一个期待的时间内,如果没有收到ack:N,用户A会尝试将msg:R重发。可能用户A同时发出了很多消息,故用户A需要在本地维护一个等待ack队列,并配合超时机制,来记录哪些消息没有收到ack:N,以定时重发。一旦收到了ack:N,说明用户B收到了“你好”消息,对应的消息将从“等待ack队列”中移除。

或者是:让用户主动点击重发,重发的时候要确认匹配msg_id以便服务端识别去重,以免消息重复。也还是消息队列方式实现。收到msg:A的响应即从等待队列中移除。

 

群聊

一个im群聊的交互流程

 

 和单聊不同的是,群聊不存在一对一的关系,但要复杂的多,一个用户发送消息,要广播给其他所有用户。这就意味着对于一个千人群来说,一个人发送的消息要推送给群里的1000个人,这里就要涉及到对高并发的处理了。

 

聊天室

版权声明:本文为leiting原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/leiting/p/12347565.html