关于TCP服务的实现过程
首先需要了解tcp服务器,socker机制,这些和语言无关。
一个简单的tcp server实现过程:
创建socket资源,socker1 ,是一个打开的文件描述符。
bind绑定到Hosu, Port。
开始监听listen。
经过以上过程,一个tcp服务就搭建好了。
客户端发起tcp连接请求
操作系统与其完成三次握手阶段,此连接就算建立了,操作系统根据tcp协议中的端口号标识将此连接放入指定的 backlog 队列中。
tcp server 从 backlog 队列中 accept 请求,也就是消费此队列,那么,它该从哪个 backlog 中消费呢,所以 accept 操作时需要带上自己的 socket1 。
被成功 accept 的请求才算和 tcp server 建立了连接,在此之前只是完成了三次握手。
于是 client 和 server 之间开始 read & write 。
于是,只要你得到了 socket1 ,你就可以冒充 tcp server ,也就可以 accept 客户端,并与之通讯。
关于fork子进程
我们还知道 fork 出的子进程会继承父进程的 socket 资源,于是便有了通常的操作:产生多个子进程来并行接受请求以达到提高并发能力的效果。
但是fork出的子进程是继承了父进程的上下文,和父进程用有相同的程序和代码,然而我们希望子进程加载新的程序从而实现热重启,所以fork操作显然无法满足需求。
关于exec系统调用
还有一个能产生子进程的方式就是exec族函数了,此函数会产生一个空的子进程,然后加载进指定的程序到进程空间并执行,这样一来虽然满足了使用新程序的需求,但是子进程却无法继承父进程的资源了,这就尴尬了,
但是,go封装的exec包提供了更加底层的能力,允许传递更多的参数,比如,你甚至可以将文件描述符复制到子进程中,这个能力还是很新颖的,在别的语言中貌似没看到。
也正是这一点,go才可以实现真正的零停机热重启,像java,swoole只能实现局部热重载,因此意义不大,所以一般是使用滚动发布的形式。
exec族函数系统调用,是通过启动一个新的进程来执行一段程序或命令,此进程将成为直属的子进程。
关于exec族可以看一下这个文章 https://blog.csdn.net/raoxiaoya/article/details/108073550
exec族函数:
exec(path, args..., env)
path: 要执行的命令
arges: 传递给命令的参数
env:可以携带一些环境变量到进程里面
关于进程通讯
以上概念可以实现使用新的程序来启动新的服务,还差一步就是关闭老的服务。
进程是个封闭的空间,进程间的通讯有很多方式,当然简单的通讯首选进程信号,这是操作最简单的。
完整的实现思路
1、go主协程提供服务。
2、开启子协程注册,监听并处理信号,比如 SIGINT --> 关机,SIGHUP --> 热重启。
3、进程收到 SIGINT 信号。
设置状态来停止 accept 操作,就是不再接受请求了,多余的请求就留在 backlog 里面了,然后在一定的时间延迟内完成对已有请求的处理,处理完毕或者时间到了就退出进程。
4、进程收到 SIGHUP 信号,创建新服务。
path 和 args 参数:和主进程的启动命令与参数一样,通过 os.Args 获取
env:复制父进程所有的环境变量,并且添加一个新的环境变量作为标志(比如 ENDLESS_CONTINUE=1),以此来区别当前进程是否是子进程,可想而知后面所有的热重启操作产生的进程都带有这个标志。
重点来了,复制 socket1 文件描述符到子进程中。
子进程启动之后又会从头执行新的代码,那么问题来了,这样会导致重新执行 bind 和 listen 操作,显然这样会报错的,因为端口已经被主进程监听着,所以在这里需要通过环境变量中的 ENDLESS_CONTINUE 的值来做个判断。
好了,现在有两个进程在同时提供服务,新程序继续向下执行,需要关闭父进程了,在子进程里面(通过ENDLESS_CONTINUE来判断),向父进程发送SIGINT信号(在子进程中可以获取父进程的PID)。
那么为什么不在父进程中执行完exec就主动终止自己呢,那是因为子进程不一定能启动成功并提供服务,所以终止信号得由子进程来发送,并且是在确保可以提供服务之后的地方尽早发送信号。
源码部分
执行子进程
https://github.com/fvbock/endless
endless.go
// 执行子进程部分
func (srv *endlessServer) fork() (err error) {
runningServerReg.Lock()
defer runningServerReg.Unlock()
// only one server instance should fork!
if runningServersForked {
return errors.New("Another process already forked. Ignoring this one.")
}
runningServersForked = true
var files = make([]*os.File, len(runningServers))
var orderArgs = make([]string, len(runningServers)