Long time no see !
距离上篇博客已经有近半年了。这半年,先是课业的繁重,后是工作的紧张,让我一直没有时间腾出手来写博客。所幸开学(离职)在即,我也总算能怀着一种轻松的心态来水一篇文章啦!~
这篇文章主要来写一写 bing-bong 的程序结构。Linux 社的朋友应该都知道,我最近写了一个用于订阅 Rss 的 QQ 机器人(什么你问为什么知道?当然是因为 Linux 茶话会是 bing-bong 的官方唯一指定试点群啊!)。因为时间的原因,它并没能像我想象的一样完美,反而在我看来有着各种各样的缺陷。不管是为了让大家更好地参与修改,还是为我将来的重构理清思路,重新梳理一下程序的逻辑都是势在必行的。
来源#
依据惯例,首先介绍一下项目的来源。因为在 README 中有类似介绍,此处就不赘述了。
设计#
众所周知,RSS 场景是典型的订阅 - 发布模型,十分适合使用消息队列完成程序功能。作者在设计时也率先考虑到了这一点。
有了消息队列,我们需要关注的就只有两个大的方面了 —— 消息的生成、接收。
- 消息的生成
一个很简单的想法,即使用定时任务抓取消息队列中的所有 topic 地址,获取到 feed 内容后判断是否发送过,如果未发送过则发送该条内容并将其标记为已发送。
- 消息的接收
消息队列的订阅者接受到广播来的消息后,由机器人负责发送消息即可。
结构#
以下是和程序逻辑有关的目录结构:
.
├── 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 # 生成机器人发送的消息
实现#
首先实现机器人,按照我们的设计可以抽象出机器人的功能:
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
)。
呼~看起来是写完了,但事实真的如此吗?考察以上设计能否满足以下需求:
- 机器人响应人的指令(如新增订阅、修改订阅等)
- 每次启动程序,机器人自动读取上次的订阅关系
- feed 查重
可以预见,要让机器人响应指令,需要让机器人监听消息事件并对消息做出相应响应,而事实上一个机器人也往往需要在初始化步骤后才能开始正常工作,故我最终将机器人接口设计为了:
type robot interface {
Init()
SendMessage(int64, string, bool)
HandleEvent(*message.MessageQueue)
}
并给出了 QQ 版本的样例实现 (放在client/QQ.go
)。
对于 2、3 需求,很明显我们需要将订阅关系和已经发送过的 feed 信息持久化,这时候数据库就派上了用场。我在db.sql
中存储了建表语句,并在model/db.go
中初始化了数据库连接,封装了一系列数据库操作方法。
至此,我们的程序基本是大功告成了。
缺陷#
如果留意我上述的实现代码,很容易会发现许多问题:
- 为什么消息队列的接收者不是单纯的 chan string,而要封装成一个大大的 Receiver?
- 按照你的逻辑,为什么一个人订阅多个 url,要起多个协程?只跑一个不行吗?
- 将消息队列的订阅与接受消息的监听耦合在一起,真的大丈夫吗?
- RSS 判重难道没有比 hash 更好的方法吗?
- 每个指令都需要读取数据库,不会影响性能吗?
- 一个如此简单的机器人却要接入 mysql 这种复杂数据库,不觉得会加大部署难度吗?
- 用户还需要自己执行数据库初始化语句,不会很麻烦吗?
- 数据库初始化写在 init 里,单测压根没法搞啊?
- ......
没错,这些(甚至更多)都是这个程序的缺陷。在将来版本的迭代中,我将尽力修复这些痛点。
结语#
感谢大家看我唠叨了这么多!最后的最后,欢迎大家为项目点个 star 鸭!~