了解Golang的Unix域插座
#linux #go #unix #socket

在Golang中,插座是一个通信端点,允许程序通过网络发送和接收数据。 Golang中有两种主要类型的插座类型:UNIX域插座(AF_UNIX)和网络插座(AF_INET | AF_INET6)。这篇博客文章将探讨这两种类型的插座之间的一些差异。

UNIX域插座(又称本地插座)用于同一机器上的进程之间的通信。他们使用基于文件的接口,并且可以使用文件系统路径访问,就像常规文件一样。在某些情况下,UNIX域插座比网络插座更快,更有效,因为它们不需要网络协议和通信的开销。它们通常用于交流通信(IPC)和同一机器上的服务之间的通信。使用文件系统作为通信渠道在过程之间传输数据。

文件系统提供了一种可靠,有效的机制,用于在过程之间传输数据。只有内核参与过程之间的通信。这些过程通过读取和写入与内核管理的同一套接字文件进行通信。内核负责处理通信详细信息,例如同步,缓冲和错误处理,并确保数据可靠,正确顺序传递。

这与网络插座不同,网络插座涉及内核,网络堆栈和网络硬件。该过程在网络插座中使用网络协议,例如TCP/UDP,以建立网络上的连接和传输数据。内核和网络堆栈处理通信详细信息,例如路由,寻址和错误校正。网络硬件处理网络上数据的物理传输。

在Golang中,使用unix网络类型的net.Dial“ Server”或net.Listen“客户端”功能创建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服务器和客户端应用程序。

在Golang中,使用net.Dialnet.Listen函数创建网络插座,该函数具有网络类型,例如TCP或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:}

unix套接字的客户端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_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插座读取的速度要比从网络插座读取的速度要快得多。

Github Repo

与HTTP服务器一起使用UNIX域插座

要在GO中使用HTTP服务器使用UNIX域套接字,您可以使用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。这些协议具有自己的安全机制,例如加密和身份验证。但是,这些机制并不总是足以防止所有类型的攻击,并且网络插座仍​​然可以受到损害。

选择哪个?

在Golang的Unix域插座和网络插座之间做出决定时,重要的是要考虑应用程序的要求和约束。 UNIX域插座更快,更有效,但仅限于同一台计算机上的过程之间的通信。网络插座更广泛,但需要更多的开销,并且会遇到网络延迟和可靠性问题。通常,UNIX域插座适用于同一机器上的IPC和服务之间的通信,而网络插座则适用于网络上的客户服务器通信。

当您需要在同一主机上的进程之间进行通信时,应该选择一个UNIX域套接字。 UNIX域插座提供了同一主机上流程之间的安全有效的通信通道,并且适用于过程需要经常或实时交换数据的方案。

例如,想象一下您在K8S POD中有两个容器,必须尽快相互通信!

总而言之,UNIX域插座和网络插座是Golang中的两种重要类型,用于不同的目的和场景。了解这些类型的插座之间的差异和权衡对于在Golang设计和实施高效且可靠的网络应用程序至关重要。

请记住,与UNIX套接字之间的过程之间的通信仅由内核处理,而在网络插座中,通信涉及内核,网络堆栈和网络硬件。