使用场景

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

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

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

使用流程

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

分布式方案

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

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

HASH_SLOT = CRC16(key) mod 16384

以上两种方案都可以实现需求,但一致性 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 环境。

你也可以选择自己编译:

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-websocket666
./go-websocket 666

连接测试:

ws://127.0.0.1:666/wsclientId

单机部署

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

分布式部署

安装 Redis: 参考网上教程

配置文件:

configs/config.inicluster
[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; # 代理转发地址
    }
}

至此,项目部署完成。

源码

交流

QQ 群:1028314856