网络编程-基于Websocket聊天室(IM)系统
阅读原文时间:2023年07月09日阅读:1

目录

  1. 浏览器支持的socket编程,轻松维持服务端的长连接

  2. 基于TCP可靠传输协议之上的协议,无需开发者关心通讯细节。

  3. 提供了高度抽象的编程接口,业务开发成本较低。

  4. 没有同源限制,客户端可以与任意服务器通信。

  5. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

客户端的简单示例

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) {
  console.log("Connection open ...");
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};

在整个 IM 系统的实现上深度用到了网络、数据库、缓存、加密、消息队列等后端必备知识。架构设计中也在大规模分布式、高并发、一致性架构设计等方面有众多成熟的解决方案。

要打造一套“实时、安全、稳定”的 IM 系统,需要深入思考很多个地方,尤其是作为整个实时互动业务的基础设施,扩展性、可用性、安全性等方面都需要有较高的保障。

2.1.使用者眼中的聊天系统

如果站在一个使用者的角度从直观体验上来看,一个简单的聊天系统大概由以下元素组成:用户账号、账号关系、联系人列表、消息、聊天会话。

  • 聊天的参与需要用户,所以需要有一个用户账号,用来给用户提供唯一标识,以及头像、昵称等可供设置的选项。
  • 账号和账号之间通过某些方式(比如加好友、互粉等)构成账号间的关系链
  • 你的好友列表或者聊天对象的列表,我们称为联系人的列表,其中你可以选择一个联系人进行聊天互动等操作。
  • 在聊天互动这个环节产生了消息
  • 同时你和对方之间的聊天消息记录就组成了一个聊天会话,在会话里能看到你们之间所有的互动消息。

2.2.开发者眼中的聊天系统

  • 客户端:一般是用户用于收发消息的终端设备,内置的客户端程序和服务端进行网络通信,用来承载用户的互动请求和消息接收功能。

  • 接入服务:可以认为是服务端的门户,为客户端提供消息收发的出入口。发送的消息先由客户端通过网络给到接入服务,然后再由接入服务递交到业务层进行处理。主要有四块功能:连接保持、协议解析、Session 维护和消息推送。

  • 业务处理服务:是消息业务逻辑处理层,比如消息的存储、未读数变更、更新最近联系人等。

  • 存储服务:用于进行账号信息、关系链,以及消息的持久化存储。

  • 外部接口服务:通过手机操作系统自身的公共连接服务来进行操作系统级的“消息推送”,通过这种方式下发的消息一般会在手机的“通知栏”对用户进行提醒和展示。

2.3.IM系统的特性

  • 实时性

  • 可靠性

  • 一致性:是指同一条消息,在多人、多终端需要保证展现顺序的一致性。

  • 安全性

2.4.心跳机制:解决网络的不确定性

  • 支持客户端断线重连:通过“心跳”快速识别连接的可用性,除了可以降低服务端的资源开销,也被用于来支撑客户端的断开重连机制。

    对于客户端发出心跳包,如果在一定的超时时间内(考虑到网络传输具有一定的延迟性,这个超时时间至少要大于一个心跳的间隔),比如连续两次发送心跳包,都没有收到 IM 服务端的响应,那么客户端可以认为和服务端的长连接不可用,这时客户端可以断线重连。

    导致服务端没有响应的原因可能是和服务端的网络在中间环节被断开,也可能是服务器负载过高无法响应心跳包,不管什么情况,这种场景下断线重连是很有必要的,它能够让客户端快速自动维护连接的可用性。

  • 连接保活:维护一条“高可用”的长连接,还有一个重要的任务就是尽量让建立的长连接存活时间更长。

  • 心跳检测的几种实现方式

    • TCP Keepalive

      TCP 的 Keepalive 作为操作系统的 TCP/IP 协议栈实现的一部分,对于本机的 TCP 连接,会在连接空闲期按一定的频次,自动发送不携带数据的探测报文,来探测对方是否存活。操作系统默认是关闭这个特性的,需要由应用层来开启。

      默认的三个配置项:心跳周期是 2 小时,失败后再重试 9 次,超时时间 75s。三个配置项均可以调整。

      这样来看,TCP 的 Keepalive 作为系统层 TCP/IP 协议栈的已有实现,不需要其他开发工作量,用来作为连接存活与否的探测机制是非常方便的;上层应用只需要处理探测后的连接异常情况即可,而且心跳包不携带数据,带宽资源的浪费也是最少的。

      由于易用性好、网络消耗小等优势,TCP Keepalive 在很多 IM 系统中被开启使用,之前抓包就发现,WhatsApps 使用空闲期 10 秒间隔的 TCP Keepalive 来进行存活探测。

      虽然拥有众多优势,但 TCP Keepalive 本身还是存在一些缺陷的,比如心跳间隔灵活性较差,一台服务器某一时间只能调整为固定间隔的心跳;另外 TCP Keepalive 虽然能够用于连接层存活的探测,但并不代表真正的应用层处于可用状态。

      我举一个例子,比如 IM 系统出现代码死锁、阻塞的情况下,实际上已经无法处理业务请求了,但此时连接层 TCP Keepalive 的探针不需要应用层参与,仍然能够在内核层正常响应。这种情况就会导致探测的误判,让已失去业务处理能力的机器不能被及时发现。

    • 应用层心跳

      为了解决 TCP Keepalive 存在的一些不足的问题,很多 IM 服务使用应用层心跳来提升探测的灵活性和准确性。应用层心跳实际上就是客户端每隔一定时间间隔,向 IM 服务端发送一个业务层的数据包告知自身存活。

      如果 IM 服务端在一定时间内没有收到心跳包,就认定客户端由于某种原因连接不可达了,此时就会从 IM 服务端把这个连接断开,同时清除相应分配的其他资源。

      应用层心跳相比 TCP Keepalive,由于需要在应用层进行发送和接收的处理,因此更能反映应用的可用性,而不是仅仅代表网络可用。

      而且应用层心跳可以根据实际网络的情况,来灵活设置心跳间隔,对于国内运营商 NAT 超时混乱的实际情况下,灵活可设置的心跳间隔在节省网络流量和保活层面优势更明显。

      目前大部分 IM 都采用了应用层心跳方案来解决连接保活和可用性探测的问题。比如之前抓包中发现 WhatApps 的应用层心跳间隔有 30 秒和 1 分钟,微信的应用层心跳间隔大部分情况是 4 分半钟,目前微博长连接采用的是 2 分钟的心跳间隔。

      下面是一个典型的应用层心跳的客户端和服务端的处理流程图,从图中可以看出客户端和服务端,各自通过心跳机制来实现“断线重连”和“资源清理”。

    • 智能心跳

      所谓智能心跳,就是让心跳间隔能够根据网络环境来自动调整,通过不断自动调整心跳间隔的方式,逐步逼近 NAT 超时临界点,在保证 NAT 不超时的情况下尽量节约设备资源。据说微信就采用了智能心跳方案来优化心跳间隔。

2.5.消息的多终端漫游

​ 所谓的“多终端漫游”是指用户在任意一个设备登录后,都能获取到历史的聊天记录。

  • 设备维度的在线状态:对于在多个终端同时登录并在线的用户,可以让 IM 服务端在收到消息后推给接收方的多台设备,也推给发送方的其他登录设备。

  • 离线消息存储:如果消息发送时,接收方或者发送方只有一台设备在线,可能一段时间后,才通过其他设备登录来查看历史聊天记录,这种离线消息的多终端漫游就需要消息在服务端进行存储了。当用户的离线设备上线时,就能够从服务端的存储中获取到离线期间收发的消息。

ZuoFuhong/go-IM 使用纯go开发,基于websocket的聊天(IM)系统,支持多设备支持,离线消息同步。本项目是由gim项目fork而来,在此基础上进行了二次开发,目标是为了梳理gim的核心流程,并仿写实现单用户多设备支持,离线消息同步的逻辑层。在开发的过程中, 砍掉了gRPC、TCP服务端、Redis缓存等模块。同时,在go.mod中仅依赖了几个必须的第三方包,其余均由纯go实现。

  • 离线消息同步

  • 心跳

  • 消息单发

  1. 阮一峰的网络日志-WebSocket 教程

  2. 即时消息技术剖析与实战

  3. 基于Go的马蜂窝旅游网分布式IM系统技术实践

  4. Stomp-面向文本的消息传递协议

  5. 关于socket.io的使用

  6. Golang写的IM服务器gim