Long time no see !

距离上篇博客已经有近半年了。这半年,先是课业的繁重,后是工作的紧张,让我一直没有时间腾出手来写博客。所幸开学(离职)在即,我也总算能怀着一种轻松的心态来水一篇文章啦!~

这篇文章主要来写一写 bing-bong 的程序结构。Linux 社的朋友应该都知道,我最近写了一个用于订阅 Rss 的 QQ 机器人(什么你问为什么知道?当然是因为 Linux 茶话会是 bing-bong 的官方唯一指定试点群啊!)。因为时间的原因,它并没能像我想象的一样完美,反而在我看来有着各种各样的缺陷。不管是为了让大家更好地参与修改,还是为我将来的重构理清思路,重新梳理一下程序的逻辑都是势在必行的。

来源

依据惯例,首先介绍一下项目的来源。因为在 README 中有类似介绍,此处就不赘述了。

设计

众所周知,RSS 场景是典型的订阅-发布模型,十分适合使用消息队列完成程序功能。作者在设计时也率先考虑到了这一点。

有了消息队列,我们需要关注的就只有两个大的方面了——消息的生成、接收。

  • 消息的生成

一个很简单的想法,即使用定时任务抓取消息队列中的所有 topic 地址,获取到 feed 内容后判断是否发送过,如果未发送过则发送该条内容并将其标记为已发送。

  • 消息的接收

消息队列的订阅者接受到广播来的消息后,由机器人负责发送消息即可。

结构

以下是和程序逻辑有关的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── client
│ └── qq.go # QQ 机器人实现
├── config.yml # 机器人配置文件
├── db.sql # 数据库初始化语句
├── internal
│ └── rss.go # 内部的 rss 更新检测
├── main.go # 程序入口(初始化客户端、消息队列并启动事件监听)
├── message
│ └── mq.go # 消息队列实现
├── model
│ ├── conf.go # 配置文件读取
│ └── db.go # 数据库连接池及一系列数据库操作
└── utils
├── error.go # 检查错误的辅助函数
├── hash.go # 生成单条 feed 内容的 hash
└── message.go # 生成机器人发送的消息

实现

首先实现机器人,按照我们的设计可以抽象出机器人的功能:

1
2
3
type robot interface {
SendMessage(int64, string, bool) // 向id为int64的用户发送string信息(其中bool值用于标记订阅者是个人还是群组)
}

任意一个能够实现这个接口的机器人都能够满足我们的要求。

接着是消息队列,对于使用CSP 并发模型的 Golang 来说,简单实现一个用于消息传递的消息队列再容易不过了,具体代码可参见message/mq.go

现在已经有了消息的消费者和传递途径,我们的最终目标(至少在现在看来)就是实现生产者了。生产者将获取消息队列的 topic 列表(即所有存在的 url)并进行请求,如果发现更新则构建消息,将消息写入到消息队列的对应 topic 中(其中 topic 的请求位于internal/rss.go,构建消息位于utils/message.go)。

呼~看起来是写完了,但事实真的如此吗?考察以上设计能否满足以下需求:

  1. 机器人响应人的指令(如新增订阅、修改订阅等)
  2. 每次启动程序,机器人自动读取上次的订阅关系
  3. feed 查重

可以预见,要让机器人响应指令,需要让机器人监听消息事件并对消息做出相应响应,而事实上一个机器人也往往需要在初始化步骤后才能开始正常工作,故我最终将机器人接口设计为了:

1
2
3
4
5
type robot interface {
Init()
SendMessage(int64, string, bool)
HandleEvent(*message.MessageQueue)
}

并给出了 QQ 版本的样例实现 (放在client/QQ.go)。

对于 2、3 需求,很明显我们需要将订阅关系和已经发送过的 feed 信息持久化,这时候数据库就派上了用场。我在db.sql中存储了建表语句,并在model/db.go中初始化了数据库连接,封装了一系列数据库操作方法。

至此,我们的程序基本是大功告成了。

缺陷

如果留意我上述的实现代码,很容易会发现许多问题:

  1. 为什么消息队列的接收者不是单纯的 chan string,而要封装成一个大大的 Receiver?
  2. 按照你的逻辑,为什么一个人订阅多个 url,要起多个协程?只跑一个不行吗?
  3. 将消息队列的订阅与接受消息的监听耦合在一起,真的大丈夫吗?
  4. RSS 判重难道没有比 hash 更好的方法吗?
  5. 每个指令都需要读取数据库,不会影响性能吗?
  6. 一个如此简单的机器人却要接入 mysql 这种复杂数据库,不觉得会加大部署难度吗?
  7. 用户还需要自己执行数据库初始化语句,不会很麻烦吗?
  8. 数据库初始化写在 init 里,单测压根没法搞啊?
  9. ……

没错,这些(甚至更多)都是这个程序的缺陷。在将来版本的迭代中,我将尽力修复这些痛点。

结语

感谢大家看我唠叨了这么多!最后的最后,欢迎大家为项目点个 star 鸭!~