理解 Golang 中的 Unix 域套接字
在 Go 语言中,套接字是一种通信端点,它允许程序通过网络发送和接收数据。Go 语言中主要有两种类型的套接字:Unix 域套接字 (AF_UNIX) 和网络套接字 (AF_INET|AF_INET6)。本文将探讨这两种套接字之间的一些区别。
Unix 域套接字(又称本地套接字)用于同一台机器上进程间的通信。它们使用基于文件的接口,可以像访问普通文件一样通过文件系统路径进行访问。在某些情况下,Unix 域套接字比网络套接字更快更高效,因为它们不需要网络协议和通信的开销。它们通常用于进程间通信 (IPC) 以及同一台机器上不同服务之间的通信。数据通过文件系统作为通信通道在进程间传输。
文件系统为进程间数据传输提供了一种可靠高效的机制。进程间的通信仅由内核参与。进程通过读写同一个套接字文件进行通信,该文件由内核管理。内核负责处理通信细节,例如同步、缓冲和错误处理,并确保数据可靠且按正确顺序传递。
这与网络套接字不同,网络套接字通信涉及内核、网络协议栈和网络硬件。进程使用网络套接字中的网络协议(例如 TCP/UDP)来建立连接并通过网络传输数据。内核和网络协议栈负责处理通信细节,例如路由、寻址和纠错。网络硬件则负责数据在网络上的物理传输。
net.Dial在 Go 语言中,可以使用`client` 或net.Listen`server` 函数并指定网络类型来创建 Unix 域套接字unix。例如,以下代码创建一个 Unix 域套接字并监听传入的连接:
// Create a Unix domain socket and listen for incoming connections.
socket, err := net.Listen("unix", "/tmp/mysocket.sock")
if err != nil {
panic(err)
}
我们来看一个如何在 Golang 中使用 Unix 域套接字的例子。以下代码创建了一个简单的回显服务器,它使用 Unix 域套接字:
package main
import (...)
func main() {
// Create a Unix domain socket and listen for incoming connections.
socket, err := net.Listen("unix", "/tmp/echo.sock")
if err != nil {
log.Fatal(err)
}
// Cleanup the sockfile.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
os.Remove("/tmp/echo.sock")
os.Exit(1)
}()
for {
// Accept an incoming connection.
conn, err := socket.Accept()
if err != nil {
log.Fatal(err)
}
// Handle the connection in a separate goroutine.
go func(conn net.Conn) {
defer conn.Close()
// Create a buffer for incoming data.
buf := make([]byte, 4096)
// Read data from the connection.
n, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
// Echo the data back to the connection.
_, err = conn.Write(buf[:n])
if err != nil {
log.Fatal(err)
}
}(conn)
}
}
让我们使用上述回显服务器进行测试netcat,您可以使用该-U选项指定套接字文件。这样您就可以连接到套接字并通过它发送和接收数据。
$ echo "I'm a Kungfu Dev" | nc -U /tmp/echo.sock
I'm a Kungfu Dev
另一方面,网络套接字用于不同机器上的进程之间进行通信。它们使用诸如 TCP 和 UDP 之类的网络协议。网络套接字比 Unix 域套接字更通用,因为它们可以与连接到网络的任何机器上的进程进行通信。它们通常用于客户端-服务器通信,例如 Web 服务器和客户端应用程序。
net.Dial在 Go 语言中,网络套接字使用 ` create` 或 ` set` 函数创建net.Listen,网络类型可以是 TCPTCP或 UDP。例如,以下代码创建一个 TCP 套接字并监听传入的连接:
// Create a TCP socket and listen for incoming connections.
socket, err := net.Listen("tcp", ":8000")
if err != nil {
panic(err)
}
从客户角度进行基本分析
网络套接字的客户端 pprof
Type: cpu
...
(pprof) list main.main
...
ROUTINE ======================== main.main in /Users/douglasmakey/go/src/github.com/douglasmakey/go-sockets-uds-network-pprof/server_echo_network_socket/client/main.go
0 530ms (flat, cum) 70.67% of Total
. . 16:
. . 17: pprof.StartCPUProfile(f)
. . 18: defer pprof.StopCPUProfile()
. . 19:
. . 20: for i := 0; i < 10000; i++ {
. 390ms 21: conn, err := net.Dial("tcp", "localhost:3000")
. . 22: if err != nil {
. . 23: log.Fatal(err)
. . 24: }
. . 25:
. . 26: msg := "Hello"
. 40ms 27: if _, err := conn.Write([]byte(msg)); err != nil {
. . 28: log.Fatal(err)
. . 29: }
. . 30:
. . 31: b := make([]byte, len(msg))
. 100ms 32: if _, err := conn.Read(b); err != nil {
. . 33: log.Fatal(err)
. . 34: }
. . 35: }
. . 36:}
客户端 pprof 用于 Unix 套接字
Type: cpu
...
(pprof) list main.main
...
ROUTINE ======================== main.main in /Users/douglasmakey/go/src/github.com/douglasmakey/go-sockets-uds-network-pprof/server_echo_unix_domain_socket/client/main.go
0 210ms (flat, cum) 80.77% of Total
. . 16:
. . 17: pprof.StartCPUProfile(f)
. . 18: defer pprof.StopCPUProfile()
. . 19:
. . 20: for i := 0; i < 10000; i++ {
. 130ms 21: conn, err := net.Dial("unix", "/tmp/echo.sock")
. . 22: if err != nil {
. . 23: log.Fatal(err)
. . 24: }
. . 25:
. . 26: msg := "Hello"
. 40ms 27: if _, err := conn.Write([]byte(msg)); err != nil {
. . 28: log.Fatal(err)
. . 29: }
. . 30:
. . 31: b := make([]byte, len(msg))
. 40ms 32: if _, err := conn.Read(b); err != nil {
. . 33: log.Fatal(err)
. . 34: }
. . 35: }
. . 36:}
这些基本概况中需要注意的事项包括:
- 打开 Unix 套接字比打开网络套接字快得多。
- 从 Unix 套接字读取数据比从网络套接字读取数据快得多。
使用 Unix 域套接字连接 HTTP 服务器
要在 Go 中使用 Unix 域套接字和 HTTP 服务器,可以使用该server.Serve函数并指定net.Listener要监听的套接字。
package main
import (...)
const socketPath = "/tmp/httpecho.sock"
func main() {
// Create a Unix domain socket and listen for incoming connections.
socket, err := net.Listen("unix", socketPath)
if err != nil {
panic(err)
}
// Cleanup the sockfile.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
os.Remove(socketPath)
os.Exit(1)
}()
m := http.NewServeMux()
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello kung fu developer! "))
})
server := http.Server{
Handler: m,
}
if err := server.Serve(socket); err != nil {
log.Fatal(err)
}
}
测试一下
$ curl -s -N --unix-socket /tmp/httpecho.sock http://localhost/
Hello kung fu developer!
在 Go 中使用带有 Unix 域套接字的 HTTP 服务器可以带来诸多优势,例如本文中提到的安全性、性能、易用性和互操作性提升。这些优势能够使 HTTP 服务器的实现更加高效、可靠和可扩展,尤其适用于需要在同一台机器上进行通信的进程。
安全方面如何保障?
Unix 域套接字和网络套接字具有不同的安全特性。一般来说,Unix 域套接字比网络套接字更安全,因为它们不暴露在网络上,并且只能由同一台机器上的进程访问。
Unix 域套接字的主要安全特性之一是利用文件系统权限来控制访问。套接字文件以特定用户和组的身份创建,并且只有拥有该用户和组相应权限的进程才能访问。这意味着只有授权的进程才能连接到套接字并交换数据。
相比之下,网络套接字暴露在网络中,任何连接到该网络的计算机都可以访问它们。这使得它们容易受到恶意攻击者(例如黑客和恶意软件)的攻击。网络套接字使用网络协议,例如 TCP/UDP。这些协议本身具有安全机制,例如加密和身份验证。然而,这些机制并非总能抵御所有类型的攻击,网络套接字仍然可能被攻破。
该选哪个?
在 Go 语言中,选择使用 Unix 域套接字还是网络套接字时,必须考虑应用程序的需求和限制。Unix 域套接字速度更快、效率更高,但仅限于同一台机器上的进程间通信。网络套接字用途更广泛,但开销更大,并且容易受到网络延迟和可靠性问题的影响。一般来说,Unix 域套接字适用于进程间通信 (IPC) 和同一台机器上的服务间通信,而网络套接字适用于通过网络进行的客户端-服务器通信。
当需要在同一主机上的进程之间进行通信时,应选择 Unix 域套接字而非网络套接字。Unix 域套接字为同一主机上的进程提供安全高效的通信通道,适用于进程需要频繁或实时交换数据的场景。
例如,假设你有一个 K8s pod,其中有两个容器,它们必须尽快相互通信!
总之,Unix 域套接字和网络套接字是 Go 语言中两种重要的套接字类型,它们用于不同的目的和场景。理解这两种套接字类型之间的差异和优缺点对于在 Go 语言中设计和实现高效可靠的网络应用程序至关重要。
请记住,Unix 套接字进程之间的通信完全由内核处理,而网络套接字通信则涉及内核、网络协议栈和网络硬件。
文章来源:https://dev.to/douglasmakey/understanding-unix-domain-sockets-in-golang-32n8
