公司目前的后台是用Beego框架搭的,而且为了服务的不中断升级,咱们开启了Beego的Grace模块,用于热升级支持。一切都跑井井有理,直到有一天,领导甩出一些服务日志,告知程序一直报错:前端
2018/03/08 17:49:34 20848 Received SIGINT.
2018/03/08 17:49:34 20848 [::]:5490 Listener closed.
2018/03/08 17:49:34 20848 Waiting for connections to finish...
2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection 20848
1
2
3
4
问题出在第4行,每次服务关闭时,都会报出use of closed network connection。按理说这时候网络链接应该关闭了啊,进程都退出了,怎么还Accept 5490端口?到Beego的Issues列表里一搜,已经有人问过这个问题了(issue2809),下面尚未人回答,搜也搜索不到,只剩最后一个工具了:看源码。golang
1. Grace模式
2.Accept家族
3. 网络编程中的 Accept、Wait
4. Close 链
5. 后记
6. 参考文献
1. Grace模式
首先能够确定,不开Grace模式的话,是没有这些日志打出来的,而是直接结束。所以咱们先要对Beego的Grace模式有一些了解。Beego官网对此必定的介绍:Grace模块。大体是说他们参照:Grace_restart_in_golang这篇文章的思路实现的热升级功能,文章很长,讲述的思路很清晰,大致过程以下:编程
开源中国翻译-GracefulRestart 这篇中文翻译说明的更通俗易懂。明白了热升级的原理,咱们就能够进入代码中详细寻找了。一切都从beego.Run()开始。网络
beego.Run()建立好了BeeApp对象,而且调用BeeApp.Run()执行。Run方法有不一样的启动模式,在此,咱们只关注Grace部分。app
func (app *App) Run() {
addr := BConfig.Listen.HTTPAddr
...框架
// run graceful mode
if BConfig.Listen.Graceful {
...
if BConfig.Listen.EnableHTTP {
go func() {
// 建立了GraceServer 是对http.Server的一层封装
server := grace.NewServer(addr, app.Handlers)
...
if err := server.ListenAndServe(); err != nil {
logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
endRunning <- true
}
}()
}
<-endRunning
return
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代码里能够看到,logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))正是打出上述日志的源头:tcp
2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection 20848
1
那么为何会返回use of closed network connection这个错误呢?跟进ListenAndServe()方法中查看:函数
func (srv *Server) ListenAndServe() (err error) {
...
// 处理上图中的热升级信号(fork子进程),SIGINT、SIGTERM信号(进程结束信号)
go srv.handleSignals()工具
...
// 若是是子进程执行,Getppid()拿到父进程pid,而且Kill
if srv.isChild {
process, err := os.FindProcess(os.Getppid())
if err != nil {
log.Println(err)
return err
}
err = process.Kill()
if err != nil {
return err
}
}学习
log.Println(os.Getpid(), srv.Addr)
return srv.Serve()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
跟进Serve()方法:
func (srv *Server) Serve() (err error) {
srv.state = StateRunning
//这里咱们传入了一个GraceListener,对net.Listener作了封装,在后面会用到。
err = srv.Server.Serve(srv.GraceListener)
log.Println(syscall.Getpid(), "Waiting for connections to finish...")
//此处会等待全部链接处理完成,对应图中的父进程结束流程。
srv.wg.Wait()
srv.state = StateTerminate
return
}
1
2
3
4
5
6
7
8
9
10
仍是调回了http.Server.Serve()方法,看这个方法:
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
for {
//代码在这里阻塞,若是没有链接进来的话。
rw, e := l.Accept()
if e != nil {
select {
//正常的结束流程
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
...
//不正常的结束流程
return e
}
//开启一个go程处理新链接
c := srv.newConn(rw)
go c.serve(ctx)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
若是是正常结束,咱们应该会收到ErrServerClosed,这虽然是个Error,可是相似于io.EOF,是一个正常的结束流程,问题在于,咱们收到的并非ServerClosed,而是use of closed connection,由l.Accept()方法返回,那咱们进到Accept()一探究竟。
2.Accept家族
前面说了,l.Accept()中l是一个GraceListener,那咱们直接去看它的Accept()方法。
func (gl *graceListener) Accept() (c net.Conn, err error) {
//调AcceptTCP()
tc, err := gl.Listener.(*net.TCPListener).AcceptTCP()
if err != nil {
return
}
...
//每次新来一个链接+1,当链接处理完成时-1。 前面wg.Wait()等的就是这个值减为0。
gl.server.wg.Add(1)
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
仍是调回了net.TCPListener的AcceptTCP(),去TCPListener下看看它的AcceptTCP():
func (l *TCPListener) AcceptTCP() (*TCPConn, error) {
...
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}
1
2
3
4
5
6
7
8
这里咱们看到返回了一个OpError,它的格式正如accept tcp [::]:5490: use of closed network connection所示,以accept开头,有net、addr信息,还有一个Err的封装,看来没有找错,错误就是从这里发出来的,赶忙进到l.accept()看看:
func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
//建立了新的TCP链接
return newTCPConn(fd), nil
}
1
2
3
4
5
6
7
8
这里调了fd.accept()涉及到一点UNIX系统知识,fd即file descriptor(文件描述符),在UNIX中,一切皆文件,一个Socket链接,一个进程,均可以看做是一个个的文件,前面的图中咱们介绍的热升级技术,子进程之因此能拿到父进程的Socket链接,也是父进程在fork子进程的过程当中,把本身的Socket链接的文件做为启动参数传递给了子进程,从而让子进程能够经过这个文件接管新来的请求,咱们直接进入ln.fd.accept()看看这个fd当中有何玄机:
func (fd *netFD) accept() (netfd *netFD, err error) {
d, rsa, errcall, err := fd.pfd.Accept()
if err != nil {
if errcall != "" {
err = wrapSyscallError(errcall, err)
}
//有可能
return nil, err
}
if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
//有可能
return nil, err
}
if err = netfd.init(); err != nil {
fd.Close()
//有可能
return nil, err
}
...
return netfd, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上面三处代码,都有可能返回err,经过本地运行服务,发现启动服务后,没有链接进来时,(也就是说,代码如今在Accept()阻塞,并无走到下面的控制流程中去),这时向其发送SIGINT信号,依然会打出use of closed connection日志,这说明这个err就是由fd.pfd.Accept()方法抛出来的,进到这个方法里看看详情:
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
...
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
//这个accept()是对accept系统调用的封装方法
s, rsa, errcall, err := accept(fd.Sysfd)
...
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
...
}
return -1, nil, errcall, err
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
函数里的accept()是对accept系统调用的封装,再往下已然超出这篇文章的范畴,那又是UNIX系统的一大篇知识,Accept家族到这结束,咱们得出的结论是:这个错误是UNIX系统发出的,与咱们的服务和Go语言都无关。这个结论无疑是鸵鸟式作法,尽管错误是系统发出的,可是系统不会无端的汇报错误,必然是什么操做有问题,触发系统汇报了这个错误。让我门再仔细的扫视这个方法,它首先prepareRead()准备了一番,而后才进行accept系统调用,尽管不多是卡在这个prepareRead()方法上(由于咱们的服务启动后,是能够正常接受和处理链接的),可是咱们仍是能够去看看这里作了哪些prepare。
func (pd *pollDesc) prepareRead(isFile bool) error {
return pd.prepare('r', isFile)
}
func (pd *pollDesc) prepare(mode int, isFile bool) error {
if pd.runtimeCtx == 0 {
return nil
}
res := runtime_pollReset(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}
1
2
3
4
5
6
7
8
9
10
11
最终调用了Runtime的runtime_pollReset()对IO轮询器进行重置,而且convertRuntime抛出的Err,进入这个convertErr()看看:
func convertErr(res int, isFile bool) error {
switch res {
case 0:
return nil
case 1:
return errClosing(isFile)
case 2:
return ErrTimeout
}
...
}
// ErrNetClosing is returned when a network descriptor is used after it has been closed.
var ErrNetClosing = errors.New("use of closed network connection")
// Return the appropriate closing error based on isFile.
func errClosing(isFile bool) error {
if isFile {
return ErrFileClosing
}
return ErrNetClosing
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
终于,咱们在不可能抛出这个Err的地方发现了这个Err:use of closed network connection。这话说的实在有些绕,由于咱们知道prepareRead()是没有返回Err的,否则咱们的服务也不可能启动而且监听端口,因此虽然这个地方声明了这个ErrNetClosing对象,可是却不是这里抛出来的Err。前面的分析咱们已经得出结论,Err是系统返回给咱们的。分析到了这里又中断了,不过咱们能够拓展下思路,看看这个ErrNetClosing的注释写了什么:大意是说,当使用一个被关闭的网络描述符时,这个Error会被返回,那么咱们找找这个ErrNetClosing在那里被使用到了,在prepareRead()方法附近,咱们发现了下面的这个方法:
//修改了上述变量ErrNetClosing的值
var ErrNetClosing = errors.New("use of closed network connection --- 改")
func (pd *pollDesc) wait(mode int, isFile bool) error {
...
res := runtime_pollWait(pd.runtimeCtx, mode)
println(" 没错,错误就是我抛出来的 res:", res)
//若是res为1 ,抛出错误ErrNetClosing。若是res为0,err = nil。
return convertErr(res, isFile)
}
1
2
3
4
5
6
7
8
9
10
11
上面的代码是我对原有代码进行修改,加上一些日志,如今从新运行,发送SIGINT信号,看看日志有什么变化:
注意:若是修改了go标准库中的代码,你须要go build -a ,添加a参数,意思是全部的代码都从新编译,这也包括go标准库中的代码。
没错,错误就是我抛出来的 res:0
没错,错误就是我抛出来的 res:0
2018/03/09 11:42:57 31164 0.0.0.0:5490
2018/03/09 11:43:09 31164 Received SIGINT.
2018/03/09 11:43:09 31164 [::]:5490 Listener closed.
2018/03/09 11:43:09 31164 Waiting for connections to finish...
没错,错误就是我抛出来的 res:1
2018/03/09 11:43:09 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection --- 改 31164
1
2
3
4
5
6
7
8
当res=0时,一切都没什么问题,当接收到SIGINT,从日志能够看到,res这时候为1,这就致使了convertErr()返回错误ErrNetClosing。看起来,这个wait()就是抛出这个错误的真正源头了。然而,咱们虽然找到了它,但却没有解决咱们的疑问,为何res会被系统置为1,最终致使咱们收到ErrNetClosing?究竟是什么操做致使了这个错误?并且,看到这个wait()方法,不由又有新的疑问:为何Accept()家族最后阻塞在Accept系统调用上,错误倒是这个wait()返回的,wait()和Accept()家族有着怎样的关联呢?
3. 网络编程中的 Accept、Wait
看来Go帮咱们作了太多太多,咱们只知道,服务端在ListenAndServe()中阻塞,更进一步,是在fd.Accept()方法中阻塞,等待链接的到来。而前面的分析咱们却发现了一个wait()方法,无疑,wait更能比accept表达出阻塞等待的含义,咱们再次审视fd.Accept()方法:
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
...
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
//这个accept()是对accept系统调用的封装方法,其实它是返回了的,没有阻塞
s, rsa, errcall, err := accept(fd.Sysfd)
...
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
//这里调用了waitRead(),说明了上面的accept()方法其实没有阻塞,真正的阻塞在这里。
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
return -1, nil, errcall, err
}
}
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
终于,咱们发现了waitRead(),而它正是wait()方法的一层封装。形势已经清晰明了了,咱们陷入了网络会阻塞在Accept()方法这个思惟定势中,一直在Accept的调用链中追寻不得结果,如今来看,网络阻塞在Accept()这句话没错,可是是对应用层的代码有效,在系统底层,fd.Accept()完成会返回syscall.EAGAIN这个Err,正是捕捉到这个syscall.EAGAIN,让咱们的代码停在waitRead()中,直到有链接过来。那么,wait()方法实际上是对runtime的runtime_pollWait的一层封装,要知道wait()的具体内容,还要去runtime包中寻找。
//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
...
//代码将会阻塞在这里,若是netpollblock()不返回true,代码将一直循环。
for !netpollblock(pd, int32(mode), false) {
//循环会不断检查是否有错误,有错误则退出
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
}
return 0
}
func netpollcheckerr(pd *pollDesc, mode int32) int {
if pd.closing {
//pd关闭会致使这里返回 1
return 1 // errClosing
}
if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
return 2 // errTimeout
}
return 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
至此咱们已经找出了res=1是出自何处,正是netpollcheckerr()返回了1,致使res接收到了1,从而抛出ErrNetClosing错误,这个错误致使咱们接收到use of closed netword connection这句日志。链条到此终止,但是咱们如今是只知其然,而不知其因此然。到底经历了一个怎样的过程,致使了pd.closing变成了true?这个错误又会不会影响到咱们的业务逻辑,可不能够忽略?为何一样是服务关闭,只有开启Graceful模式的服务才抛出这个错误呢?很明显要解决这些疑问,咱们要调转方向,回到ListenAndServe(),找找服务是怎么被Closed的。
4. Close 链
func (srv *Server) ListenAndServe() (err error) {
...
// 处理上图中的热升级信号(fork子进程),SIGINT、SIGTERM信号(进程结束信号)
go srv.handleSignals()
//若是是子进程,getListener()拿到的仍是父进程的监听器,若是是父进程,建立新的监听器。
l, err := srv.getListener(addr)
if err != nil {
log.Println(err)
return err
}
//对监听器作包装
srv.GraceListener = newGraceListener(l, srv)
...
}
func (srv *Server) handleSignals() {
var sig os.Signal
signal.Notify(
srv.sigChan,
hookableSignals...,
)
pid := syscall.Getpid()
for {
sig = <-srv.sigChan
...
switch sig {
case syscall.SIGHUP:
log.Println(pid, "Received SIGHUP. forking.")
err := srv.fork()
...
case syscall.SIGINT:
log.Println(pid, "Received SIGINT.")
srv.shutdown()
case syscall.SIGTERM:
log.Println(pid, "Received SIGTERM.")
srv.shutdown()
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
这里能够看到ListenAndServe()被调用时,会启动一个goroutine,等待系统信号的到来,一旦收到SIGHUP信号,则立刻fork一个子进程;若是收到SIGINT或者SIGTERM信号,则会调用stv.shutdown()关闭链接,看看stv.shutdown()作了些什么:
func (srv *Server) shutdown() {
...
srv.state = StateShuttingDown
if DefaultTimeout >= 0 {
go srv.serverTimeout(DefaultTimeout)
}
err := srv.GraceListener.Close()
...
}
func (srv *Server) serverTimeout(d time.Duration) {
...
time.Sleep(d)
//当d时间过去后,进程结束休眠,强制将计数器置0
for {
if srv.state == StateTerminate {
break
}
//计数器减1
srv.wg.Done()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这里调用了GraceListener的Close(),前面咱们说过,这个GraceListener其实是对TCPListener的封装。主要是在Accept()中添加一个计数器,当有链接来了,计数器加1,链接处理完成,计数器减1。固然,若是网络有延迟,或者客户端有链接被挂起致使计数器不为0,通过DefaultTimeout(60秒)后,serverTimeout()会强制把计数器置为0。那么,咱们进入Close()方法看看如何关闭监听。
func (gl *graceListener) Close() error {
if gl.stopped {
return syscall.EINVAL
}
//简单的向stop channer 发送nil信号
gl.stop <- nil
//等待TCPListener.Close()执行完毕
return <-gl.stop
}
func newGraceListener(l net.Listener, srv *Server) (el *graceListener) {
el = &graceListener{
Listener: l,
stop: make(chan error),
server: srv,
}
//开启一个goroutine,不阻塞代码
go func() {
//等待Close()的nil信号
<-el.stop
el.stopped = true
el.stop <- el.Listener.Close()
}()
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这里涉及到goroutine间的通讯,在咱们新建这个GraceListener时,就已经在监听结束信号了,代码将在<-el.stop阻塞,直到gl.stop<-nil语句被执行,stoped被置为true,且调用TCPListener.Close()并等待其执行完毕。那么TCPListener.Close()又执行了什么操做?
func (l *TCPListener) Close() error {
...
if err := l.close(); err != nil {
return &OpError{Op: "close", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return nil
}
func (ln *TCPListener) close() error {
return ln.fd.Close()
}
1
2
3
4
5
6
7
8
9
10
11
Close()方法是对close()方法的封装,由于Go中没有访问修饰符,方法首字母的大小写就表明这个方法是公有的仍是私有的。因此Close()-close()也算是Go的特点了。close()方法调用了fd.close(),前面咱们分析过了,这个fd是指文件描述符,也就是当前的Socket链接,这个链接被关闭,咱们就不能接受新链接了。
func (fd *netFD) Close() error {
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}
func (fd *FD) Close() error {
...
fd.pd.evict()
return fd.decref()
}
func (pd *pollDesc) evict() {
if pd.runtimeCtx == 0 {
return
}
runtime_pollUnblock(pd.runtimeCtx)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最终仍是调用了runtime_pollUnblock(),直接看runtime包的源码:
func poll_runtime_pollUnblock(pd *pollDesc) {
lock(&pd.lock)
if pd.closing {
throw("runtime: unblock on closing polldesc")
}
pd.closing = true
...
}
1
2
3
4
5
6
7
8
还记得咱们分析Wait过程说的pd.closing,正是它为true致使了res=1:
//若是netpollblock()不返回true,代码将一直循环
for !netpollblock(pd, int32(mode), false) {
//循环会不断检查是否有错误,有错误则退出
err = netpollcheckerr(pd, int32(mode))
...
}
---------
if pd.closing {
//pd关闭会致使这里返回 1
return 1 // errClosing
}
1
2
3
4
5
6
7
8
9
10
11
如今能够清晰的了解到发生了什么了,上面的代码吧pd.closing置为true,而Accpet()所在的goroutine检查到这个值发生了改变,因而终止了Accept()过程并报错,这些goroutine的关系以下图所示:
上图中的三个goroutine,从最右边的shutdown()开始看起,一步一步的完成关闭流程。最左边的是Accept()所在的goroutine通常咱们正常结束程序的话,这个goroutine会正常返回。可是咱们为了实现Graceful模式,还新建了两个goroutine,正是这两个新建的goroutine合做把当前的网络链接关闭,Accept()所在的goroutine不了解当前情况,觉得是意外的关闭,致使咱们收到那行Err日志。可是,这种关闭并不会对现有程序有什么影响,由于是咱们知道是本身主动执行的这个过程。
5. 后记
Beego日志的分析过程算是结束了,文章很长,能看到最后的小伙伴都是棒棒的,虽然我中途考虑过度成两篇,权衡之下仍是做罢,由于这些分析先后相连,也是我思路的文字记录,若是分开,老是有被强行打断之感,想必读者也会以为麻烦,不如一鼓作气,刨根问底来的痛快。为了照顾读者,以及用手机阅读的人,此文引用的代码尽可能精简,而且在该注释的地方作了注释,望能在这信息爆炸的时代,读者(也包括将来的我)能尽快的从中提取出关键信息来。
在我预计要写这篇文章时,远没有如今这么长,不过是发现了Accept阻塞时,fd.close()之后系统会报’use of network connection’错误,但不影响业务逻辑。可是随着文章的推动,一个又一个问题不断冒出来,促使我更深刻的去寻找答案,也算是一个意外的学习过程。也使我最近一直在考虑,追溯一个问题的答案,到底要到何种地步?在编程技术被高度封装的今天,咱们要窥探到语言底层为止?仍是操做系统层面?亦或者深刻到汇编,对每一个寄存器,每条指令都有所涉猎?前人和咱们把计算机这座山越堆越高,咱们站在高处,是否有一天会望不到山下的景象了,也许穷尽一辈子也学不尽最底下的东西。最近还据说JS如今被各类封装,甚至不少语言都是先编译成JS再运行,有人说JS都快成前端的汇编了。又据说了Flutter,这个框架封装了Android和iOS的开发流程,提供更高层次的接口兼容两个平台的开发。不由有些迷茫,新潮的技术层出不穷,去年RN、Weex还火的不行,今年就出了跨平台开发的大杀器,年年翻新,还学的过来么?
此次的分析过程却是对这些问题有了一点心得,文中对Accept过程分析到Accept系统调用就没再往下了,在对wait函数的分析中,分析到了runtime也就中止了,至于那些系统内核的事儿,一律没有涉及(也不懂)。由于这些现有的分析,足以造成对文章开头那个问题的解释,而且得出不会影响现有业务流程的结论。毕竟你的领导,他想要的只有结果。若是你先有的知识能hold住你的工做,那就不必作无谓的涉猎。毕竟学以至用,最终仍是为了那份薪水服务。若是想要更好的薪水,找到更好的工做,致使能力上hold不住,那天然而然会去主动补充本身。毕竟人生苦短,我用Python少加班,多干干本身喜欢的事,多陪陪重要的人。
6. 参考文献
Golang网络:核心API实现剖析
epoll 或者 kqueue 的原理是什么?
直接修改go语言包中的源码,编译程序,修改是否生效?
Graceful Restart in Golang
Socket层实现系列 — accept()的实现