如何在Go编程语言中可靠地unlink()Unix域套接字

11

我有一个Go程序,在本地HTTP服务的localhost:8080上托管一个简单的HTTP服务,以便通过proxy_pass指令将公共nginx主机连接到它,作为反向代理来处理网站请求的一部分。这一切都很好,没有问题。

我想将Go程序转换为在Unix域套接字上托管HTTP服务,而不是在本地TCP套接字上托管,以提高安全性并减少TCP的不必要协议开销。

问题: 问题是Unix域套接字一旦被bind()后就无法再次使用,即使在程序终止后也是如此。第二次(以及之后每一次)运行Go程序时,它会退出,并显示致命错误"address already in use"

通常做法是在服务器关闭时unlink() Unix域套接字(即删除文件)。然而,在Go中这很棘手。我的第一次尝试是在我的主函数中使用defer语句(见下文),但如果我使用CTRL-C等信号中断进程,则不会运行它。我想这是可以预料的。令人失望,但不出所料。

问题:是否有最佳实践可以在服务器进程关闭时(无论是优雅地还是不优雅地)unlink()套接字?

以下是我启动服务器监听的func main()的部分参考代码:

// Create the HTTP server listening on the requested socket:
l, err := net.Listen("unix", "/tmp/mysocket")
if err != nil {
    log.Fatal(err)
} else {
    // Unix sockets must be unlink()ed before being reused again.
    // Unfortunately, this defer is not run when a signal is received, e.g. CTRL-C.
    defer func() {
        os.Remove("/tmp/mysocket")
    }()

    log.Fatal(http.Serve(l, http.HandlerFunc(indexHtml)))
}

https://dev59.com/smgu5IYBdhLWcg3wgnVh - thwd
@Tom 谢谢。那个问题/答案已经有一年了,Go语言仍在改进,所以我想看看是否出现了任何新的/更好的东西。 - James Dunne
2
在第一行之前尝试使用 os.Remove("/tmp/mysocket"),它是否有效? - Shane Hou
我用一个简单的信号处理程序使它很好地工作了。现在对于这么简单的用例来说还好,但我担心当有更复杂的清理操作涉及到大量不可访问的打开资源时会怎样。 - James Dunne
3个回答

9

以下是我使用的完整解决方案。我在问题中发布的代码是为了清晰演示而简化的版本。

// Create the socket to listen on:
l, err := net.Listen(socketType, socketAddr)
if err != nil {
    log.Fatal(err)
    return
}

// Unix sockets must be unlink()ed before being reused again.

// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill, syscall.SIGTERM)
go func(c chan os.Signal) {
    // Wait for a SIGINT or SIGKILL:
    sig := <-c
    log.Printf("Caught signal %s: shutting down.", sig)
    // Stop listening (and unlink the socket if unix type):
    l.Close()
    // And we're done:
    os.Exit(0)
}(sigc)

// Start the HTTP server:
log.Fatal(http.Serve(l, http.HandlerFunc(indexHtml)))

我希望这是一段优秀有效的 Go 代码,能够让 Go 的作者感到自豪。对我来说看起来确实是这样的。如果不是这样,那会让我很尴尬。:)

对于任何好奇的人,这是 https://github.com/JamesDunne/go-index-html 的一部分,它是一个简单的 HTTP 目录列表生成器,并提供了一些 Web 服务器默认不提供的额外功能。


2
如果listener是UnixListener类型,l.Close()将会取消链接套接字文件。os.Remove(socketAddr)是多余的。 - Gvozden
谢谢!在这里进行了泛型化:https://gitlab.com/stellarpower-grouped-projects/tidbits/go/-/blob/main/SignalTerminate.go只是提醒一下:你无法捕获SIGKILL ;) - stellarpower

2

在现代的Go语言中,您可以使用syscall.Unlink()函数 - 文档在此处

import ( 
    "net"
    "syscall"
    ...
)

...


socketpath := "/tmp/somesocket"
// carry on with your socket creation:
addr, err := net.ResolveUnixAddr("unixgram", socketpath)
if err != nil {
    return err;
}

// always remove the named socket from the fs if its there
err = syscall.Unlink(socketpath)
if err != nil {
    // not really important if it fails
    log.Error("Unlink()",err)
}

// carry on with socket bind()
conn, err := net.ListenUnixgram("unixgram", addr);
if err != nil {
    return err;
}

2

你可以在信号处理程序中结束主函数,并为其他任务生成单独的go例程。这样,你就可以利用defer机制并清理所有(基于信号或非基于信号的)关闭:

func main() {
    // Create the HTTP server listening on the requested socket:
    l, err := net.Listen("unix", "/tmp/mysocket")
    if err != nil {
        log.Fatal(err)
        return
    }
    // Just work with defer here; this works as long as the signal handling
    // happens in the main Go routine.
    defer l.Close()

    // Make sure the server does not block the main
    go func() {
        log.Fatal(http.Serve(l, http.HandlerFunc(indexHtml)))
    }()


    // Use a buffered channel so we don't miss any signals
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGTERM)

    // Block until a signal is received.
    s := <-c
    fmt.Println("Got signal:", s)

    // ...and exit, running all the defer statements
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接