<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>goeasy-io (GoEasy)</title>
    <link>https://ruby-china.org/goeasy-io</link>
    <description/>
    <language>en-us</language>
    <item>
      <title>sync-player：使用 websocket 实现异地同步播放视频</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文作者：星空无限&lt;/p&gt;

&lt;p&gt;原文链接：&lt;a href="https://liyangzone.com/2020/09/20/%E5%89%8D%E7%AB%AF/sync-player/" rel="nofollow" target="_blank"&gt;https://liyangzone.com/2020/09/20/%E5%89%8D%E7%AB%AF/sync-player/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GoEasy 已获作者授权转载，GoEasy 转载时有改动，感谢作者的分享。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;前段时间我有这样一个需求，想和一个异地的人一起看电影，先后在网上找了一些方案，不过那几个案都有一些缺点&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;coplay:&lt;/strong&gt; 一个浏览器插件，只能播放各大视频网站的视频，视频资源有限，我想要看的视频没有，比如一些经典电影和美剧之类&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;微光 APP:&lt;/strong&gt; 还是上面的问题，而且只有手机端&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;向日葵等远程桌面：&lt;/strong&gt; 受限于网络问题，卡顿很严重，体验不好&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;作为一个对用户体验有追求的切图仔，我是一个下载党，看电影必须下载到本地看，基本不看视频网站上的玩意&lt;/p&gt;

&lt;p&gt;那么有没有能实现同步播放本地文件的方案呢，答案是肯定的，经过我的一些摸索和研究，我实现了本地文件的同步播放，同时支持 PC 和手机端，而且还支持外挂字幕等高级功能，如何实现请往下看。&lt;/p&gt;
&lt;h2 id="功能介绍&amp;amp;特性："&gt;功能介绍&amp;amp;特性：&lt;/h2&gt;
&lt;p&gt;一个可以同步看视频的播放器，可用于异地同步观影、观剧，支持多人同时观看。
本项目有两个版本，web 版运行在浏览器上，可跨平台，不限操作系统、设备，功能简单适用于要求不高的用户。还有基于 SPlayer(射手影音)DIY 的客户端版本 (windows、MAC)，播放 4K 高清文件、外挂字幕，统统没问题。&lt;/p&gt;
&lt;h2 id="演示demo:"&gt;演示 demo:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;web 版同步效果&lt;/strong&gt;
&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/10/1.gif" title="" alt="BSQoHx.gif"&gt;
&lt;strong&gt;客户端与 web 版同步效果&lt;/strong&gt;
&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/10/2.gif" title="" alt="BSQID1.gif"&gt;&lt;/p&gt;
&lt;h2 id="原理："&gt;原理：&lt;/h2&gt;
&lt;p&gt;基于 websocket 实现，与一些用 websocket 实现的聊天室类似，只不过这个聊天室里的消息换成了播放暂停的动作和时间信息，客户端收到消息后执行相应的动作：播放、暂停、快进，以达到同时播放的效果。&lt;/p&gt;
&lt;h2 id="项目所用到的"&gt;项目所用到的&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;node.js&lt;/li&gt;
&lt;li&gt;socketio&lt;/li&gt;
&lt;li&gt;HTML5 video API&lt;/li&gt;
&lt;li&gt;vue.js&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="如何使用："&gt;如何使用：&lt;/h2&gt;
&lt;p&gt;本项目的核心是 websocket，所以至少需要一台服务器提供 websocket 服务，websocket 服务可以自己部署，可以使用第三方平台 GoEasy 提供的 websocket 服务。&lt;/p&gt;
&lt;h2 id="1、自己部署："&gt;1、自己部署：&lt;/h2&gt;
&lt;p&gt;websocket 服务器可以是一台具有公网 IP 的云服务器，也可以是一台具有公网 IP 的普通 PC，没有公网 IP 也可以。你也可以使用 zerotier 或其他 VPN 工具将两台设备组成一个大局域网，让它们能互相通信。websocket 服务器操作系统不限，只要有 node.js 环境。&lt;/p&gt;

&lt;p&gt;websocket 服务端部署方法：安装 node.js 环境，将 server 目录移动到服务器上，进入 server 目录，执行以下命令：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;安装项目依赖包&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 安装项目依赖包

npm install 

# 启动websocket服务

node index.js
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="2、使用GoEasy的websocket服务"&gt;2、使用 GoEasy 的 websocket 服务&lt;/h2&gt;
&lt;p&gt;注册 GoEasy 开发者账号并创建一个应用，获得 appkey，复制到本项目相应位置即可。&lt;/p&gt;

&lt;p&gt;GoEasy 官网：&lt;a href="https://www.goeasy.io" rel="nofollow" target="_blank" title=""&gt;https://www.goeasy.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;无论是使用哪种 websocket 服务都可以，本项目写了两套代码，只需将不用的那套注释掉即可 (默认 GoEasy)。&lt;/p&gt;

&lt;p&gt;除了 websocket 服务器之外，还需要两个 http 服务端，一个是 web 服务端 (提供 html、css、js 等文件的访问)，一个是视频服务端 (提供视频文件访问)。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你可以将 web 服务部端署到以下位置：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;具有公网 IP 的服务器&lt;/li&gt;
&lt;li&gt;github-pages 或国内的码云提供的静态 web 服务&lt;/li&gt;
&lt;li&gt;localhost(本地服务器)，同一个局域网内的设备访问该服务器内网 IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;视频文件只需一个视频地址就行，也有以下几种选择：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;具有公网 IP 的服务器&lt;/li&gt;
&lt;li&gt;localhost(本地服务器)，同一个局域网内的设备访问该服务器内网 IP&lt;/li&gt;
&lt;li&gt;第三方视频地址&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/10/3.png" title="" alt="image"&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使用场景 1：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;云服务器带宽足够大 (至少要大于播放视频的码率)，云服务器既可以作为 websocket 服务端，也可以作为 http 服务端。上图中所有设备都访问云服务器的 ip 或域名。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使用场景 2：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;云服务器的带宽很小，这时候它只能作为 websocket 服务端，这时可以用上图中的 PC1 和 PC2 作为 http 服务端，PC1 和 PHONE1 在一个内网访问 PC1 的内网 IP，PC2 和 PHONE2 在一个内网访问 PC2 的内网 IP，PC3 可作为自己的 http 服务端，PHONE3 若是有提供视频文件的服务端，也可以使用。
&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/10/4.png" title="" alt="image"&gt;
&lt;strong&gt;使用场景 3：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;需要使用 zerotier 或其他 VPN 工具将异地设备组成一个大局域网，其中任意一台 PC 均可作为 websocket 服务端和 http 服务端 (需要上传带宽足够大)。上图中各设备都访问那台 PC 的内网 ip 即可。&lt;/p&gt;

&lt;p&gt;最简单的使用方法，下载 nginx 开启一个本地服务器，下载本项目 client 文件夹放到到 nginx 根目录里，视频文件也放到里面。注册 goeasy 开发者账号并创建一个应用，获得 appkey，并填入到 appkey 到代码 (script/main.js) 相应位置。然后浏览器打开 192.168.3.58/client/，填入你的视频地址192.168.3.58/movie/xxx.mp4或网络视频地址，对方也这样操作一番，即可实现同步播放视频。&lt;/p&gt;

&lt;p&gt;web 版本的功能比较简单，而且受限于网络问题，快进之类的操作需要缓冲一段时间。如果你不满足 web 版功能，对用户体验有更高的要求，如支持更多文件格式、播放高清本地视频文件、外挂字幕等，我也找到了另一种方式来满足你的需求。&lt;/p&gt;

&lt;p&gt;那就是 DIY 一个开源的播放器的源码：SPlayer(射手影音)。&lt;/p&gt;

&lt;p&gt;射手影音官网：&lt;a href="https://splayer.org" rel="nofollow" target="_blank"&gt;https://splayer.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;源码地址：&lt;a href="https://github.com/chiflix/splayerx" rel="nofollow" target="_blank"&gt;https://github.com/chiflix/splayerx&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在以 electron + 播放器为关键字一番搜索之后，我找到了这个基于 electron 实现的开源播放器，并下载了源码来研究。&lt;/p&gt;

&lt;p&gt;经过一番研究之后，我找到了控制视频播放、暂停、快进的代码位置，并将控制同步的代码移植了进去，从而也实现了同步功能，并且与 web 版兼容。&lt;/p&gt;

&lt;p&gt;具体方法请看：&lt;a href="https://github.com/liyang5945/sync-player/blob/master/how-to-modify-splayer.md" rel="nofollow" target="_blank" title=""&gt;修改教程&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本项目部分图标样式来源于此项目：&lt;a href="https://github.com/Justineo/coplay" rel="nofollow" target="_blank" title=""&gt;coplay&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本项目 github 地址：&lt;a href="https://github.com/liyang5945/sync-player" rel="nofollow" target="_blank" title=""&gt;点击前往&lt;/a&gt; ，欢迎⭐⭐⭐STAR⭐⭐⭐&lt;/p&gt;
&lt;h2 id="关于GoEasy："&gt;关于 GoEasy：&lt;/h2&gt;
&lt;p&gt;GoEasy 是一个成熟稳定的企业级 websocket PAAS 服务平台，开发人员不需要考虑 websocket 服务端的搭建，只需要几行代码，就可以轻松实现客户端与客户端之间，服务器与客户端之间的的 websocket 通信。&lt;/p&gt;

&lt;p&gt;GoEasy 作为国内领先的第三方 websocket 消息推送平台，具备极佳的兼容性。除了兼容所有常见的浏览器以外，同时也兼容 uni-app，各种小程序，以及 vue、react-native、cocos、laya、egret 等常见的前端框架。&lt;/p&gt;

&lt;p&gt;同时 GoEasy 已经内置 websocket 中必备的心跳，断网重连，消息补发，历史消息和客户端上下线提醒等特性，开发人员也不需要自己搭建 websocket 服务处理集群高可用，安全和性能问题。GoEasy 已经稳定运行了 5 年，支持千万级并发，成功支撑过很多知名企业的重要活动，安全性和可靠性都是久经考验。&lt;/p&gt;

&lt;p&gt;有兴趣自己搭建 websocket 的话，可以参考这篇技术分享《&lt;a href="https://www.cnblogs.com/goeasy-websocket/p/12425696.html" rel="nofollow" target="_blank" title=""&gt;搭建 websocket 消息推送服务，必须要考虑的几个问题&lt;/a&gt;》&lt;/p&gt;</description>
      <author>goeasy-io</author>
      <pubDate>Tue, 20 Oct 2020 12:25:03 +0800</pubDate>
      <link>https://ruby-china.org/topics/40492</link>
      <guid>https://ruby-china.org/topics/40492</guid>
    </item>
    <item>
      <title>Websocket 直播间聊天室教程 - GoEasy 快速实现聊天室</title>
      <description>&lt;p&gt;最近两年直播那个火啊，真的是无法形容！经常有朋友问起，我想实现一个直播间聊天或者我想开发一个聊天室，要如何开始呢？&lt;/p&gt;

&lt;p&gt;&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/04/live-chatroom-1024x695.gif" title="" alt="直播间"&gt;&lt;/p&gt;

&lt;p&gt;今天小编就手把手的教你用 GoEasy 做一个聊天室，当然也可以用于直播间内的互动。全套源码已经开源，git 地址：&lt;a href="https://gitee.com/goeasy-io/GoEasyDemo-Live-Chatroom.git" rel="nofollow" target="_blank"&gt;https://gitee.com/goeasy-io/GoEasyDemo-Live-Chatroom.git&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;本教程主要目的是为大家介绍实现思路，为了确保本教程能帮助到使用不同前端技术的朋友，采用了 HTML + JQuery 的方式，后续还会推出 Uniapp(vue/nvue) 和小程序版本，大家可以持续关注。&lt;/p&gt;

&lt;p&gt;我们这次要实现的聊天室，有两个界面，分别是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;登录界面&lt;/li&gt;
&lt;li&gt;聊天室界面
# 登录
&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/04/login.png" title="" alt="登录界面"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;对于登录界面，我们期望：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户可以输入自己的昵称&lt;/li&gt;
&lt;li&gt;用户可以选择自己喜欢的头像&lt;/li&gt;
&lt;li&gt;用户可以选择进入不同的聊天室（直播间）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;实现步骤&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;登录界面的实现，不用多说，因为真的是 So Easy! 一个简单的界面，只包含三个简单的逻辑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;验证是否输入昵称&lt;/li&gt;
&lt;li&gt;验证是否选择一个头像&lt;/li&gt;
&lt;li&gt;根据选择进入相应的聊天室&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下边重点讲一下聊天室的实现。&lt;/p&gt;
&lt;h2 id="聊天室（直播间）"&gt;聊天室（直播间）&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://www.goeasy.io/articles/wp-content/uploads/2020/04/liveroom.png" title="" alt="聊天室界面"&gt;&lt;/p&gt;

&lt;p&gt;当我们进入一个聊天室后，我们期望：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户能看到当前有多少用户在线，这个数字能够实时的更新&lt;/li&gt;
&lt;li&gt;用户能看到当前在线用户们的头像，而且能够实时的更新&lt;/li&gt;
&lt;li&gt;如果有用户进入或离开聊天室&lt;br&gt;
a. 聊天室会有“XXX 进来了"或"XXX 离开了"的提示&lt;br&gt;
b. 在线用户的数字和用户的头像列表会随之自动更新&lt;/li&gt;
&lt;li&gt;用户可以在聊天里发言&lt;/li&gt;
&lt;li&gt;用户可以发送道具：火箭或者比心&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="实现步骤"&gt;实现步骤&lt;/h3&gt;&lt;h4 id="第一步：聊天室界面显示"&gt;第一步：聊天室界面显示&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 初始化：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当用户选择了一个聊天室，显示聊天室界面之前，我们首先要进行以下初始化工作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;初始化当前用户 currentUser，用户 id，昵称，头像&lt;/li&gt;
&lt;li&gt;初始化当前聊天室 ID: currentRoomId&lt;/li&gt;
&lt;li&gt;初始化 GoEasy 对象，注意一定要加上 userId 参数（可以是该用户的 uuid 或 id 等唯一标识，只有设置了 userId 的客户端在上下线时，才会触发上下线提醒）。同时需要将头像和昵称放入 userData，当我们收到一个用户上线提醒的时候，我们需要知道这个用户的头像和昵称。&lt;/li&gt;
&lt;li&gt;初始化 onlineUsers，onlineUsers 是用来存放当前聊天室在线用户数和在线用户列表。将当前聊天室 Id (currentRoomId) 作为 channel，执行 goEasy.hereNow 查询此刻聊天室在线用户数和用户列表，赋值给 onlineUsers。除了在进入聊天室的时候初始化 onlineUsers，当有用户进入或离开时，也会动态的更新 onlineUsers。&lt;/li&gt;
&lt;li&gt;以当前聊天室的 id(currentRoomId) 作为 channel，执行 subscriber 方法监听和接收聊天室新消息。&lt;/li&gt;
&lt;li&gt;以当前聊天室的 id(currentRoomId) 作为 channel，执行 subscriberPresence 监听用户进入和离开事件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;** 参考代码：service.js **&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//初始化聊天室
this.joinRoom = function(userId,nickName, avatar, roomID) {
        //初始化当前用户
        this.currentUser = new User(userId, nickName, avatar);
        //初始化当前聊天室id
        this.currentRoomId = roomID;
        //初始化goeasy，建立长连接
        this.goeasy = new GoEasy({
            host: "hangzhou.goeasy.io",
            appkey: "您的appkey",
            userId: this.currentUser.id,
            userData: '{"nickname":"' + this.currentUser.nickname + '","avatar":"' + this.currentUser.avatar + '"}',
            onConnected: function () {
                console.log( "GoEasy connect successfully.")
            },
            onDisconnected: function () {
                console.log("GoEasy disconnected.")
            }
        });
        //查询当前在线用户列表，初始化onlineUsers对象
        this.initialOnlineUsers();
        //监听用户上下线提醒，实时更新onlineUsers对象
        this.subscriberPresence();
        //监听和接收新消息
        this.subscriberNewMessage();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 页面展示：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;完成初始化之后，就跳转到直播间界面，在页面上显示以下数据：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当前聊天室的名称&lt;/li&gt;
&lt;li&gt;聊天记录，并且显示聊天室界面&lt;/li&gt;
&lt;li&gt;展示聊天室界面&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;参考代码：controller.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//页面切换到聊天室界面
function showChatRoom() {
    //更新房间名
    $("#chatRoom-header").find(".current-chatRoom-name").text(loginCommand.roomName);

    //加载聊天历史
    var chatHistory = service.loadChatHistory();
    chatHistory.forEach(function (item) {
        //展示发送的消息
        var otherPerson = createCurrentChatRoomPerson(item.senderNickname + ":", item.content)
        $(".chatRoom-content-box").append($(otherPerson));
    });

    //隐藏登录界面
    $(".chat-login-box").hide();
    // //显示聊天界面
    $(".chatRoom-box").show();
    // //滑动到最后一行
    scrollBottom();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们已经完成了 goeasy 长连接的初始化，和一个聊天室静态展示。接下来，我们一起来看看如何让这个聊天室能够动起来。&lt;/p&gt;
&lt;h4 id="第二步：聊天室互动"&gt;第二步：聊天室互动&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 实时更新在线用户数和头像列表&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;之前在 service.initialOnlineUsers 方法已经初始化 onlineUsers 对象，但聊天室随时都有用户进进出出，所以我们接下来还需要能够在有用户上线或下线的时候能够实时的更新 onlineUsers，并且实时显示在页面上。
当我们收到一个用户上线提醒，我们将新上线的用户的信息存入在线用户对象 onlineUsers 里，当有用户离开时，在本地在线用户列表里删除。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;参考代码：service.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//监听用户上下线时间，维护onlineUsers对象
this.subscriberPresence = function() {
    var self = this;
    this.goeasy.subscribePresence({
        channel: this.currentRoomId,
        onPresence: function(presenceEvents) {
            presenceEvents.events.forEach(function(event) {
                var userId = event.userId;
                var count = presenceEvents.clientAmount;
                //更新onlineUsers在线用户数
                self.onlineUsers.count = count;
                //如果有用户进入聊天室
                if (event.action == "join" || event.action == "online") {
                    var userData = JSON.parse(event.userData);
                    var nickName = userData.nickname;
                    var avatar = userData.avatar;
                    var user = new User(userId, nickName, avatar);
                    //将新用户加入onlineUsers列表
                    self.onlineUsers.users.push(user);
                    //触发界面的更新
                    self.onJoinRoom(user.nickname, user.avatar);
                } else {
                    for (var i = 0; i &amp;lt; self.onlineUsers.users.length; i++) {
                        var leavingUser = self.onlineUsers.users[i];
                        if (leavingUser.id == userId) {
                            var nickName = leavingUser.nickname;
                            var avatar = leavingUser.avatar;
                            //将离开的用户从onlineUsers中删掉
                            self.onlineUsers.users.splice(i, 1);
                            //触发界面的更新
                            self.onLeaveRoom(nickName, avatar);
                        }
                    }
                }
            });
        },
        onSuccess : function () {
            console.log("监听成功")
        },
        onFailed : function () {
            console.log("监听失败")
        }
    });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 发送消息&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;初始化一个 chatMessage 对象，包含发送方 id，昵称，消息内容，消息类型为 chat&lt;/li&gt;
&lt;li&gt;将 chatMessage 转换为一个 Json 格式的字符串&lt;/li&gt;
&lt;li&gt;调用 GoEasy 的 Publish 方法，完成消息的发送&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;参考代码（service.js)&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this.sendMessage = function(content) {
    var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.CHAT, content);
    var self = this;
    this.goeasy.publish({
        channel: self.currentRoomId,
        message: JSON.stringify(message),
        onSuccess: function() {
            console.log("消息发布成功。");
        },
        onFailed: function(error) {
            console.log("消息发送失败，错误编码：" + error.code + " 错误信息：" + error.content);
        }
    });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 接收和显示新消息/道具&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;之前我们已经在初始化页面的时候执行了 service.subscriberNewMessage()，当我们收到一条消息时：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;根据消息类型判断是一条聊天消息，还是一个道具&lt;/li&gt;
&lt;li&gt;如果收到的是一条聊天消息，直接显示到界面&lt;/li&gt;
&lt;li&gt;如果是道具，就播放动画&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;参考代码（service.js）&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//监听消息或道具
this.subscriberNewMessage = function() {
    var self = this;
    this.goeasy.subscribe({
        channel: this.currentRoomId, //替换为您自己的channel
        onMessage: function(message) {
            var chatMessage = JSON.parse(message.content);
            //todo:事实上不推荐在前端收到时保存, 一个用户开多个窗口，会导致重复保存, 建议所有消息都是都在发送时在服务器端保存，这里只是为了演示
            self.restapi.saveChatMessage(self.currentRoomId, chatMessage);
            //如果收到的是一个消息，就显示为消息
            if (chatMessage.type == MessageType.CHAT) {
                var selfSent = chatMessage.senderUserId == self.currentUser.id;
                var content = JSON.parse(message.content);
                self.onNewMessage(chatMessage.senderNickname, content, selfSent);
            }
            //如果收到的是一个道具，就播放道具动画
            if (chatMessage.type == MessageType.PROP) {
                if (chatMessage.content == Prop.ROCKET) {
                    self.onNewRocket(chatMessage.senderNickname);
                }
                if (chatMessage.content == Prop.HEART) {
                    self.onNewHeart(chatMessage.senderNickname);
                }
            }
        }
    });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. 发送和接收并展示道具&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;其实和发送消息的实现几乎是一样的，具体代码请参考 service.js 的 sendProp 方法，controller.js 的 onNewHeart() 方法。动画的播放，使用了 TweenMax 这个库，主要是为了展示一个实现思路，小编也不知道这个库是否有很好的兼容性，以及是否能够用在 Uniapp 和小程序下，知道的朋友可以留言分享给大家。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this.sendProp = function(prop) {
    var self = this;
    var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.PROP, prop);
    this.goeasy.publish({
        channel: self.currentRoomId,
        message: JSON.stringify(message),
        onSuccess: function() {
            console.log("道具发布成功。");
        },
        onFailed: function(error) {
            console.log("道具发送失败，错误编码：" + error.code + " 错误信息：" + error.content);
        }
    });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，一个聊天室就搞定了，是不是很简单？&lt;/p&gt;

&lt;p&gt;如果阅读本文或开发中有任何问题，也欢迎在 GoEasy 官网添加 (&lt;a href="https://www.goeasy.io" rel="nofollow" target="_blank" title=""&gt;https://www.goeasy.io&lt;/a&gt;）加 GoEasy 为好友，来获得更多技术支持。&lt;/p&gt;</description>
      <author>goeasy-io</author>
      <pubDate>Fri, 29 May 2020 12:31:07 +0800</pubDate>
      <link>https://ruby-china.org/topics/39917</link>
      <guid>https://ruby-china.org/topics/39917</guid>
    </item>
    <item>
      <title>搭建 websocket 消息推送服务，必须要考虑的几个问题</title>
      <description>&lt;p&gt;近年，不论是正在快速增长的直播，远程教育以及 IM 聊天场景，还是在常规企业级系统中用到的系统提醒，对 websocket 的需求越来越大，对 websocket 的要求也越来越高。从早期对 websocket 的应用仅限于少部分功能和 IM 等特殊场景，逐步发展为追求支持高并发，百万、千万级每秒通讯的高可用 websocket 服务。
&lt;img src="//static.cnodejs.org/FnLfrK5EIrYHwUWwzK9i0g-gX_GG" title="" alt="up-b73acdfd58268ef040d2239934eb7651954.png"&gt;&lt;/p&gt;

&lt;p&gt;面对各种新场景对 websocket 功能和性能越来越高的需求，不同的团队有不同的选择，有的直接使用由专业团队开发的成熟稳定的第三方 websocket 服务，有些则选择自建 websocket 服务。&lt;/p&gt;

&lt;p&gt;作为一个具有多年 websocket 开发经验的老程序猿，经历了 GoEasy 企业级 websocket 服务从无到有，从小到大的过程，此文是根据过去几年在 GoEasy 开发过程中踩过的坑，以及为众多开发团队提供 websocket 服务、与众多开发者交流中的总结的一些经验和体会。&lt;/p&gt;

&lt;p&gt;这次主要从搭建 websocket 服务的基本功能和特性方面做一些分享，下次有机会再从构建一个高可用 websocket 时要面对的高并发，海量消息，集群容灾，横向扩展，以及自动化运维等方面进更多的分享。&lt;/p&gt;

&lt;p&gt;以下几点是个人认为在构建 websocket 服务时必须要考虑的一些技术特性以及能显著提高用户体验的功能，供各位同学参考：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.建立心跳机制&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;心跳机制几乎是所有网络编程的第一步，经常容易被新手忽略。因为在 websocket 长连接中，客户端和服务端并不会一直通信，如果双方长期没有沟通则都不清楚彼此当前状态，所以需要发送一段很小的报文告诉对方“我还活着”。另外还有两个目的：
服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道，让它下线；
客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.建立具有良好兼容性的客户端 SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;虽说现在主流浏览器都支持 websocket，但在编码中还是会遇到浏览器兼容性问题，而且通过 websocket 通信的客户端早已不仅限于各种 web 浏览器，还包括越来越多的 APP，小程序。因此就要求构建的 websocket 服务必须能够很友好的支持各种客户端。最好的方式就是构建一个能够兼容所有主流浏览器、小程序和 APP，以及 uni-app、vue、react-native 等目前常见的各种前端框架的客户端 SDK，这样不论公司的各个项目使用什么样的前端技术，都能够快速的集成 websocket 服务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.断网自动重连和消息补发机制&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;移动互联网时代，终端用户所处的网络环境多样且复杂，如用户进出电梯，出入地下室或地铁等网络不稳定的场所，或其他原因导致的网络不稳定都是很常见的场景。因此，一个可靠的 websocket 服务必须具备完善的断网自动重连机制。确保断网后，网络一旦恢复，能第一时间自动重新建立长连接，并且能够立即补发在网络不稳定期间发送的消息。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.离线消息&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;基础的 Websocket 通讯从技术上来说，消息送达的前提条件就是建立起一个长连接，没有建立网络连接就来讨论通讯那是耍流氓。但是从使用者的角度上来说，随手关闭浏览器，或者将小程序、APP 进程直接杀掉而导致网络连接断开的情况是随时都在发生的。然后我们下意识的期待，就是我下次打开浏览器访问网页，或者打开 APP 时，能够收到用户离开系统期间的所有信息。从技术上这是一个跟 websocket 没有多大关系的需求，但实际上却是 websocket 服务不可或缺的基本特性，也是一个能够极大提升用户体验的功能。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.上下线提醒，客户端在线列表&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;掌握当前系统有哪些用户在线，捕捉用户上下线事件，是搭建一个企业级 websocket 服务，必不可少的特性，尤其是开发 IM 和游戏类产品。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.支持历史消息查询&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;websocket 服务，某种意义也是属于一个消息系统，对于历史消息的查询需求，是无法绕开的话题。比如 IM 系统中常见的历史消息，因此在 websocket 服务内部实现一个高速，可靠的消息队列机制来支持 websocket 服务实现历史消息的查询就是一个必须的工作。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.消息的压缩机制&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不论是为了保证消息通讯的速度和实时性，还是为了节约流量和带宽费用，或者是出于提高网卡的使用效率和增加系统的吞吐量，在通讯过程中对消息进行必要的压缩都是必不可少的。&lt;/p&gt;

&lt;p&gt;除了需要考虑以上七点以外，笔者认为，还有几个问题也是很值得初学者积极关注的：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.缓存和持久化&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;选择合适的消息缓存机制，是企业级 websocket 服务保证性能必须要考虑的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.异步调用&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;要支持大量消息通讯的高性能系统，必然推荐异步调用。若设计为同步调用，调用方就需要一直等待被调用方完成。如果一层一层的同步调用下去，所有的调用方需要相同的等待时间，调用方的资源会被大量的浪费。更糟糕的是一旦被调用方出问题，其他调用就会出现多米诺骨牌效应跟着出问题，导致故障蔓延。收到请求立即返回结果，然后再异步执行，不仅可以增加系统的吞吐量，最大的好处是让服务之间的解耦更为彻底。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.独立于业务和标准化&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;尽管在一个 web 项目中可以同时存在常规 http 服务和 websocket 服务，尤其对性能要求不高的单应用 web 系统，这种方式更简单，更便于维护。但对于性能和可用性高的企业级系统或者互联网平台，更好的方式，是将 websocket 服务作为一个单独的微服务来进行设计，避免和常规的 http 服务抢占资源，导致系统性能不可控，同时也更便于横向扩展。&lt;/p&gt;

&lt;p&gt;一个设计良好的企业级 websocket 服务应该是一个独立于业务系统、标准化的单独存在的技术性微服务，能够作为公司基础架构的一部分为公司的所有项目提供通讯服务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.幂等性和重复消息的过滤&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;所谓幂等性，就是一次和多次请求一个接口都应该具有同样的后果。为什么需要？对每个接口的调用都会有三种可能的结果：成功，失败和超时。对最后一种的原因很多可能是网络丢包，可能请求没有到达，也有可能返回没有收到。于是在对接口的调用时往往都会有重试机制，但重试机制很容易导致消息的重复发送，从用户层面这往往是不可接受的，因此在接口的设计时，我们就需要考虑接口的幂等性，确保同一条消息发送一次和十次都不回导致消息的重复到达。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.支持 QoS 服务质量分级&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;其实对于上一点消息重复的问题，行业已经有了解决方案和标准规范，对于消息到达率和重复，常用的手段就是通过消息确认的方式来确保消息到达，要求越高，意味着确认机制越复杂，成本越高。为了在成本和到达率之间有很好的平衡，通常对消息系统的服务质量（QoS）分为以下三个级别：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;QoS 0(At most once)：“最多发一次”，意味着发送就可以了，不需要确认机制，发送了即可，适用于要求不高的场景，可以接受一定的不到达率，成本最低。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;QoS 1(At least once)：“至少发一次”，意味着发送方必须明确收到接收方的确认信号，否则就会反复发，每条消息至少需要两次通信来确认到达，可以接受一些消息被重发，但成本不高。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;QoS 2(Exactly once)：“确保只发一次”，意味着每条消息只能到达一次，且不允许重复到达，为了达到这个目标就需要双方至少通讯三次，成本最高。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个完善的 websocket 服务面对不同的应用场景，应该能够支持选择不同等级的 QoS，在成本和服务质量之间取得平衡。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最后&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;虽然 websocket 已经广泛的应用于各种系统和平台，但如果要搭建一个满足企业级或者大型互联网平台的可靠、安全稳定的 websocket 服务，对于没有经验的同学，在具体的技术实践过程依然是有不少的坑要踩。&lt;/p&gt;

&lt;p&gt;对 websocket 服务有较高要求，选择成熟可靠的第三方 websocket 服务其实也是一个成本更低和高效的选择。GoEasy 作为国内领先的第三方 websocket 消息平台，已经稳定运行了 5 年时间，支持千万级消息并发，除了兼容所有常见的浏览器以外，同时也兼容 uni-app，各种小程序，以及 vue、react-native 等常见的前端框架。&lt;/p&gt;

&lt;p&gt;希望本文能为初次搭建 websocket 服务的同学在思路上有所帮助和参考，也欢迎各位前辈多多批评指正，同时也希望未来有机会就更多的技术与大家进行交流。&lt;/p&gt;

&lt;p&gt;GoEasy 官网：&lt;a href="https://www.goeasy.io/" rel="nofollow" target="_blank" title=""&gt;https://www.goeasy.io/&lt;/a&gt;&lt;/p&gt;</description>
      <author>goeasy-io</author>
      <pubDate>Thu, 28 May 2020 16:26:11 +0800</pubDate>
      <link>https://ruby-china.org/topics/39912</link>
      <guid>https://ruby-china.org/topics/39912</guid>
    </item>
  </channel>
</rss>
