在上一篇文章《Go配置文件热加载 - 发送系统信号》中给你们介绍了在Go语言中 利用发送系统信号更新配置文件 其核心思想就是:新起一个协程,监听linux
的用户自定义信号 USR1
, 当收到该信号类型时,主动更新当前配置文件。html
那么接下来,咱们将继续完成上一篇文章提到的第二种实现配置文件热更新方式:利用linux
提供的inotify
接口实现配置文件自动更新。node
首先在咱们实操以前,让咱们先来了解下什么是 inotify
。linux
在 Linux
内核 2.6.13
(June 18, 2005)版本以后,Linux
内核新增了一批文件系统的扩展接口(API),其中之一就是inotify
,inotify
提供了一种基于 inode
的监控文件系统事件的机制,能够监控文件系统的变化如文件修改、新增、删除等,并能够将相应的事件通知给应用程序。git
inotify
既能够监控文件,也能够监控目录。当监控目录时,它能够同时监控目录自己以及目录中的各文件的变化。此外,inotify
使用文件描述符做为接口,于是可使用一般的文件I/O操做 select
、poll
和 epoll
来监视文件系统的变化。github
总之,简单来讲就是:inotify
为咱们从系统层面提供了一种能够监控文件变化的接口,咱们能够利用它来监控文件或目录的变化。golang
inotify
提供经常使用的监控事件以下:编程
IN_ACCESSjson
文件被访问时触发事件,例如一个文件正在被read时。windows
IN_ATTRIBapi
文件属性(Metadata)发送变化触发的事件,例如文件权限发生变化(使用 chmod
修改),文件所属用户发生变化(使用chown
修改),文件时间戳发生变化等。
IN_CLOSE_WRITE
当一个文件写入操做结束文件被关闭时触发。
IN_CLOSE_NOWRITE
当一个文件或目录被打开没有任何写操做,当被关闭时触发。
IN_CREATE
当一个文件或目录被建立时触发。
IN_DELETE
文件或目录被删除时触发。
IN_DELETE_SELF
监控文件或目录自己被删除时触发,并且,若是一个文件或目录被移到其它地方,好比使用mv
命令,也会触发该事件,由于 mv
命令本质上是拷贝一份当前文件,而后删除当前文件的操做。
IN_MODIFY
文件被修改时触发,例如:有写操做( write
) 或者文件内容被清空(truncate
)操做。不过须要注意的是,IN_MODIFY
可能会连续触发屡次。
IN_MOVE_SELF
所监控的文件或目录自己发生移动时触发。
IN_MOVED_FROM
文件或目录移除所监控目录。
IN_MOVED_TO
文件或目录移入所监控目录。
IN_ALL_EVENTS
监控全部事件。
IN_OPEN
文件被打开事件。
IN_CLOSE
文件被关闭事件,包括 IN_CLOSE_WRITE
和 IN_CLOSE_NOWRITE
的事件总和。
IN_MOVE
涉及全部的移动事件,包括 IN_MOVED_FROM
和 IN_MOVED_TO
。
如上即是inotify
提供给咱们的经常使用监听事件,咱们能够在本身的项目中监听如上的一个或多个事件来实现特定的需求,若是想查阅更多事件细节,请参考此处:inotify doc
但须要说明的是,inotify
并不是是跨平台的,因此在macOS
或 windows
下则没法使用,但在macOS
也提供相似的实现:FSEvents ,以及 windows
下的 FindFirstChangeNotificationA,这里咱们再也不展开跨平台实现讨论,读者要是有兴趣能够查阅相关资料,或者使用文末推荐的开源库。
接下来,咱们开始实现项目的配置文件更新监控功能(实操)。在GO
语言中,咱们使用 golang.org/x/sys/unix 这个包来调用底层操做系统的一些封装功能,inotify
相关接口也包含在此包中,使用时只须要导入此包便可:
import "golang.org/x/sys/unix"
最简单的使用inotify
大体分为三个步骤:
inotify
初始化。咱们将按照这三个步骤来实现一个简单的 GO版
配置文件监控脚本 demo
,此处咱们仍是继续沿用 上一篇文章 的配置文件,当该文件发生变化时,咱们须要通知Go
代码从新读取该文件内容,从而实现热更新的目的。
/tmp/env.json
按照以前所说的步骤,第一步须要初始化inotify,初始化须要使用:InotifyInit()
函数,该函数会返回一个文件句柄和错误信息,以后的操做都是基于该文件句柄:
fd, err := unix.InotifyInit() if err != nil { log.Fatal(err) }
完成inotify
初始化后,接着咱们须要添加咱们须要监控的文件和以及想要监听的一个或多个事件,因为是项目的配置,此处的使用场景是:配置文件通常不会有删除的需求,而一般的操做是部署更新,所以此处咱们选择的监听事件是:
IN_CLOSE_WRITE
如第一小节中提到的,当所监听的配置文件以写的方式被打开后,当此文件关闭时触发的事件,在这种状况下就可能发生文件的更新,正好此种场景正是咱们想要的。
不过须要注意的是,若是文件以写的方式打开后,并未更新任何内容关闭时也会触发该事件。
path := "/tmp/env.json" watched, err := unix.InotifyAddWatch(fd, path, syscall.IN_CLOSE_WRITE) if err != nil { _ = unix.Close(fd) log.Fatal(err) }
如上代码中,文件监听使用了 InotifyAddWatch()
函数,第一个参数 fd
为第一步中的初始化文件句柄,第二个参数:path
为须要监听文件的路径,第三个参数为须要监听的事件。若是监听失败,较友好的方式是咱们须要关闭当前的文件句柄。
有了前两个步骤的准备,那么接下来咱们只须要读取获取到监听事件便可:
events := make(chan uint32) go func() { var buf [unix.SizeofInotifyEvent * 4096]byte for { n, err := unix.Read(fd, buf[:]) if err != nil { n = 0 continue } var offset uint32 for offset <= uint32(n - unix.SizeofInotifyEvent) { raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) mask := uint32(raw.Mask) nameLen := uint32(raw.Len) events <- mask offset += unix.SizeofInotifyEvent + nameLen } } }()
在这里,咱们新起了一个goroutine
, 由于接收事件通知是一个循环往复的过程。而后咱们把文件句柄中的事件使用 unix.Read()
函数读取到一个 buffer
中,若是unix.Read()
读取不到任何事件,那么它就会处于阻塞状态。而后,咱们循环遍历的方式,获取到 buffer
中的所接受到的全部事件通知,而后上报到 events
通道中。
那么如今一旦有新的监控事件通知,那么就会当即达到 events
通道中,接着咱们须要作的即是从 events
通道中获取通知事件便可:
for { select { case event := <-events: if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE { // 调用加载配置文件函数 loadConfig(path) } } }
最终,整个代码大体以下:
func main() { path := "/tmp/env.json" // 初始化inotify文件监控 fd, err := unix.InotifyInit() if err != nil { log.Fatal(err) } watched, err := unix.InotifyAddWatch(fd, path, syscall.IN_CLOSE_WRITE) if err != nil { _ = unix.Close(fd) log.Fatal(err) } defer func() { _ = unix.Close(fd) _ = unix.Close(watched) }() events := make(chan uint32) go func() { var ( buf [unix.SizeofInotifyEvent * 4096]byte n int ) for { n, err = unix.Read(fd, buf[:]) if err != nil { n = 0 fmt.Println(err) continue } var offset uint32 for offset <= uint32(n - unix.SizeofInotifyEvent) { raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) mask := uint32(raw.Mask) nameLen := uint32(raw.Len) // 塞到事件队列 events <- mask offset += unix.SizeofInotifyEvent + nameLen } } }() // 获取监听事件 for { select { case event := <-events: if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE { // 接收到事件,加载配置文件 loadConfig(path) } } } }
如上的代码,咱们其实就完成了一个简单的配置文件监控的代码思路,但总体代码的质量是纯面条式的,所以有必要封装一下,在这里我打算把它们封装成一个 Watcher 类(其实Go语言没有类的概念,实质就是一个struct),代码内容请参考连接地址,这里再也不此处展开,由于编程思想又是另外一个话题,有了这个struct
以后,咱们只须要直接使用便可:
func main() { path := "/tmp/env.json" notify, err := watcher.NewWatcher() if err != nil { log.Fatal(err) } err = notify.AddWatcher(path, syscall.IN_CLOSE_WRITE) if err != nil { log.Fatal(err) } done := make(chan bool, 1) go func() { for { select { case event := <-notify.Events: if event & syscall.IN_CLOSE_WRITE == syscall.IN_CLOSE_WRITE { fmt.Printf(" file changed \n") // 加载配置文件函数, 配置文件代码参考上一篇文章内容 // loadConfig(path) } } } }() <- done }
至此,咱们完成了一个基于inofity
的配置文件热更新所有代码,在Go
中来实现还算比较简单,接下来咱们须要总结一下:
inotify
是 Linux
是内核系统提供的监控系统,使用它作热更新,其实和语言无关,因此你能够熟悉的语言来开发。inotify
须要内核版本为2.6.13
以上,不支持macOS
和 Windows
系统,若是但愿实现跨平台文件监控那么可使用以下第三点的 fsnotify
库。若是不想重复早轮子,那么咱们能够站在巨人的肩上,推荐两个文件监听库:
(360技术原创内容,转载请务必保留文末二维码,谢谢~)
关于360技术360技术是360技术团队打造的技术分享公众号,天天推送技术干货内容
更多技术信息欢迎关注“360技术”微信公众号