部署超简单的 Golong 分布式 WebSocket 微服务 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
dylangl

部署超简单的 Golong 分布式 WebSocket 微服务

  •  
  •   dylangl Mar 14, 2020 4609 views
    This topic created in 2238 days ago, the information mentioned may be changed or developed.

    使用场景

    在实现业务的时候,我们常常有些需求需要系统主动发送消息给客户端,方案有轮询和长连接,但轮询需要不断的创建销毁 http 连接,对客户端、对服务器来说都挺消耗资源的,消息推送也不够实时。这里我们选择了 WebSocket 长连接的方案。

    有大量的项目需要服务端主动向客户端推送消息,为了减少重复开发,我们做成了微服务。

    使用于服务器需要主动向客户端推送消息、客户端需要实时获取消息的请求。例如聊天、广播消息、多人游戏消息推送、任务执行结果推送等方面。

    使用流程

    用 Websocket 客户端连接本服务,服务端会返回客户端一个唯一的 client id,通过这个 client id 可以知道是哪个连接,客户端拿到这个 id 之后上报到服务端,服务端根据业务需求可以给这个长连接发送指定信息,或者绑定到分组。

    分布式方案

    维持大量的长连接对单台服务器的压力也挺大的,这里也就要求该服务需要可以扩容,也就是分布式地扩展。分布式对于可存储的公共资源有一套完整的解决方案,但对于 WebSocket 来说,操作对象就是每一个连接,它是维持在每一个程序中的。每一个连接不能存储起来共享、不能在不同的程序之间共享。所以我能想到的方案是不同程序之间进行通讯。

    那么,怎样知道某个连接在哪个应用呢?答案是通过 client id 去判断。那么通过 client id 又是如何知道的呢?有以下几种方案:

    1. 一致性 hash 算法

      一致性 hash 算法是将整个哈希值空间组织成一个虚拟的圆环,在 redis 集群中哈希函数的值空间为 0-2^32-1 ( 32 位无符号整型)。把服务器的 IP 或主机名作为关键字,通过哈希函数计算出相应的值,对应到这个虚拟的圆环空间。我们再通过哈希函数计算 key 的值,得到一个在圆环空间的位置,按顺时针方向找到的第一个节点就是存放该 key 数据的服务器节点。

      在没有节点的增减的时候,可以满足我们的需求,但如果此时一个节点挂掉了或者新增一个机器怎么办?节点挂点之后,会在圆环上删除节点,增加节点则反之。这时候按顺时针方向找的数据就不准确,在某些业务上来说可以接受,但在 WebSocket 微服务上来说,影响范围内的连接会断掉,如果要求没那么高,客户端再进行重连也可以。

    2. hash slot (哈希槽)

      服务器的 IP 或者主机名作为 key,对每个 key 进行计算 CRC16 值,然后对 16384 进行取模,得出一个对应 key 的 hash slot。

      HASH_SLOT = CRC16(key) mod 16384 

      我们根据节点的数量,给每个节点划分范围,这个范围是 0-16384。hash slot 的重点就在这个虚拟表,key 对应的 hash slot 是永不变的,增减节点就是维护这张虚拟表。

    以上两种方案都可以实现需求,但一致性 hash 算法的方案会使部分 key 找到的节点不准确; hash slot 的方案需要维护一张虚拟表,在实现起来需要有一个功能去判断服务器是否挂了。修改这张虚拟表,新增节点也一样,在实现起来会遇到很多问题。

    然后我采取的方案是,每个连接都保存在本应用,然后用对称加密加密服务器 IP 和端口,得到的值作为 client id。对指定 client id 进行操作时,只需要解密这个 key,就能得到相应的 IP 和端口。判断是否为本机,不是本机的话进行 RPC 通讯告诉相应的程序。长连接的连接数据不可迁移,程序挂掉了相应的连接也就挂了,在该程序上的连接也就断开了,这时重连的话会找到另一个可用的程序。

    Golang 实现的分布式 WebSocket 微服务

    简介

    本系统基于 Golang、Redis、RPC 实现分布式 WebSocket 微服务,也可以单机部署,单机部署不需要 Redis、RPC。分布式部署可以支持 nginx 负责均衡、水平扩容部署,程序之间使用 RPC 通信。

    目前实现的功能有,给指定客户端发送消息、绑定客户端到分组、给分组里的客户端批量发送消息、获取在线的客户端、上下线自动通知。适用于长连接的大部分场景,分组可以理解为聊天室,绑定客户端到分组相当于把客户端添加到聊天室,给分组发送信息相当于给聊天室的每个人发送消息。

    架构图

    单机服务 WebSocket 单机服务架构图

    分布式

    WebSocket 分布式服务架构图

    时序图

    单发消息

    1. 客户端发送连接请求,连接请求通过 nginx 负载均衡找到一台 ws 服务器;
    2. ws 服务器响应连接请求,通过对称加密服务器 IP 和端口号,得到的值作为 client id,并返回。
    3. 客户端拿到 client id 之后,交给业务系统;
    4. 业务系统拿到 client id 之后,通过 http 发送相关消息,经过 nginx 负载分配到一台 ws 服务器;
    5. 这台 ws 服务器拿到 clinet id 和消息,解密出对应的服务器 IP 和端口;
    6. 拿到 IP 地址和端口,通过 PRC 协议给指定 ws 程序发送信息;
    7. 该 ws 程序接收到 client id 和信息,给指定的连接发送信息;
    8. 客户端收到信息。

    WebSocket 微服务单发时序图

    群发消息

    1. 前 3 个步骤跟单发的一样;
    2. 业务系统拿到 client id 之后,通过 http 给指定分组发送消息,经过 nginx 负载分配到一台 ws 服务器;
    3. 这台 ws 服务器拿到分组 ID 和消息,去 Redis 查询服务器列表,然后发送 RPC 广播;
    4. 所有收到广播的服务,找到本机所有该分组的连接;
    5. 给所有这些连接发送消息;
    6. 客户端收到信息。

    WebSocket 微服务群发消息时序图

    使用

    下载本项目:

    这里已经打包好了,下载相应的环境,支持 Linux、Windows、MacOS 环境。

    https://github.com/woodylan/go-websocket/releases

    你也可以选择自己编译:

    git clone https://github.com/woodylan/go-websocket.git 

    编译:

    // 编译适用于本机的版本 go build // 编译 Linux 版本 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build // 编译 Windows 64 位版本 CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build // 编译 MacOS 版本 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build 

    执行:

    编译成功之后会得到一个二进制文件go-websocket,执行该二进制文件,文件名后面跟着的是端口号,下面的命令666则表示端口号,你可以可以改成其他的。

    ./go-websocket 666 

    连接测试:

    打开支持 Websocket 的客户端,输入 ws://127.0.0.1:666/ws 进行连接,连接成功会返回clientId

    单机部署

    单机部署很简单,不需要配置 Redis、RabbitMQ,只需要编译然后运行该二进制文件就可以了,步骤如上。

    分布式部署

    安装 Redis: 参考网上教程

    配置文件:

    配置文件位于项目根目录的configs/config.inicluster为 true 表示分布式部署。

    [common] # 是否分布式部署 cluster = true # 对称加密 key 16 位 crypto_key = xxxxxxxxxxxxxxxx [redis] host = 127.0.0.1 port = 6379 password = 

    运行项目:

    在不同的机器运行本项目,注意配置号端口号,项目如果在同一机器,则必须用不同的端口。你可以用supervisor做进程管理。

    配置 Nginx 负载均衡:

    upstream ws_cluster { server 127.0.0.1:666; server 127.0.0.1:667; } server { listen 660; server_name ws.example.com; access_log /logs/access.log; error_log /logs/error.log; location /ws { proxy_pass http://ws_cluster; # 代理转发地址 proxy_http_version 1.1; proxy_read_timeout 60s; # 超时设置 # 启用支持 websocket 连接 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /api { proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $http_host; proxy_pass http://ws_cluster; # 代理转发地址 } } 

    至此,项目部署完成。

    源码

    github:https://github.com/woodylan/go-websocket

    交流

    QQ 群:1028314856

    18 replies    2020-08-08 16:08:34 +08:00
    Yoock
        1
    Yoock  
       Mar 14, 2020
    已 star
    LittleYangYang
        2
    LittleYangYang  
       Mar 14, 2020
    star 已交
    dylangl
        3
    dylangl  
    OP
       Mar 14, 2020
    whitehack
        4
    whitehack  
       Mar 14, 2020
    架构看起来不错。

    但是有一点比较奇怪,为什么客户端已经有 ws 连接了,还要通过 http 来与业务系统通信?
    然后业务系统再通过 ws 来推送消息。而不是直接通过 ws 解决问题? http 只是用来做辅助。
    我感觉 ws 为主,http 为辅,是比较合理的方案。
    而你这里是 http 为主,ws 为辅。

    只是比较好奇。望解惑。
    onesec
        5
    onesec  
       Mar 14, 2020
    开源不易,已经 star
    circleee
        6
    circleee  
       Mar 14, 2020
    star
    lazypu
        7
    lazypu  
       Mar 14, 2020
    居然 star 过
    dylangl
        8
    dylangl  
    OP
       Mar 14, 2020   1
    @whitehack 我做的定位是微服务,可以给不同的业务系统使用。所以 ws 的定位是用来接收消息,http 用来发送消息。为什么要这么做的? ws 与业务系统是解耦的,ws 收到的消息不会转发到业务系统。要实现解耦,那么就只能先把消息发送到业务系统,业务系统处理好业务逻辑之后再通过 http 发送给 ws server。
    dylangl
        9
    dylangl  
    OP
       Mar 14, 2020
    @linxl 感谢支持
    Leigg
        10
    Leigg  
       Mar 14, 2020
    群聊的逻辑描述的是否不大清楚?群组是没有连接的吧?有的是每个用户的 ws 连接。
    说说我的想法:按理说 ws-server 应该是无状态的,所以根据群组 id 找到群成员连接的任务应该要落到 [业务系统] 上来执行,然后 [业务系统] 应该维护好所有用户连接态,能够根据每个群成员 id 找到对应 ws-server,通过 rpc 让对应 ws-server 给成员发消息。
    JRyan
        11
    JRyan  
       Mar 14, 2020 via Android
    这种适合做直播弹幕吧,B 站的 goim 是这个思路
    dylangl
        12
    dylangl  
    OP
       Mar 14, 2020
    @Leigg 可能我描述得不是很清楚,但我实际的做法跟你想的差不多。用户的连接 id ( client id )是由业务系统去维护的,当需要发送到某个用户,由业务系统去找到相应的连接 id,然后发送消息。

    业务系统可以通过 ws server 绑定 client id 到指定的分组(比如房间),有提供相应的接口。业务系统也可以发送消息到指定的分组。
    hantsy
        13
    hantsy  
       Mar 14, 2020
    过去我的程序一般都是 HTTP 为主,WS,SSE 为辅助(解决实时要求)。
    dylangl
        14
    dylangl  
    OP
       Mar 14, 2020
    @JRyan 弹幕、聊天、消息通知等都可以用这个。
    dylangl
        15
    dylangl  
    OP
       Mar 14, 2020
    @hantsy 这个程序主要解决实时消息推送的问题,可能跟你过去的程序类型不太一样。
    cabing
        16
    cabing  
       Mar 14, 2020
    开源不容易。

    已 star,增加更多的单元测试就更好了。
    dylangl
        17
    dylangl  
    OP
       Mar 15, 2020
    @cabing 谢谢,后续会增加
    1044523901
        18
    1044523901  
       Aug 8, 2020
    不错
    About     Help     Advertise     Blog     API     FAQ     Solana     1574 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 488ms UTC 16:25 PVG 00:25 LAX 09:25 JFK 12:25
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86