CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • Go系统接口编程 前言
  • 第一部分:引言
  • 第1章 为什么选择Go语言?
  • 第2章 重温并发与并行
  • 第3章 理解系统调用
  • 第二部分:与操作系统交互
  • 第4章 文件和目录操作
  • 第5章 处理系统事件
  • 第6章 理解进程间通信中的管道
  • 第7章 Unix套接字
    • Unix套接字简介
    • 创建Unix套接字
      • 深入了解套接字创建
      • 从操作系统的角度来看
      • 从文件系统的角度来看
      • 创建客户端
      • 使用lsof检查套接字
    • 构建聊天服务器
      • 完整的聊天客户端
    • 在Unix域套接字下提供HTTP服务
      • 客户端
      • HTTP请求行
      • HTTP请求头部
      • 表示头部结束的空行
      • textproto包
      • 性能
      • 其他常见用例
    • 总结
  • 第8章 内存管理
  • 第三部分:性能
  • 第9章 性能分析
  • 第10章 网络编程
  • 第四部分:连接的应用程序
  • 第11章 遥测技术
  • 第12章 分布式部署你的应用程序
  • 第五部分:深入探索
  • 第13章 顶点项目——分布式缓存
  • 第14章 高效编码实践
  • 第15章 精通系统编程
目录

第7章 Unix套接字

# 第7章 Unix套接字

在本章中,你将学习套接字编程,不过这次重点关注Unix套接字。本章将帮助你理解Unix套接字的工作原理、类型,以及它们在Unix和类Unix操作系统(如Linux)的进程间通信(IPC)中的作用。通过示例,你将获得实践知识,尤其是如何使用Go编程语言创建Unix套接字服务器和客户端。

这些信息对于对开发高级软件系统感兴趣的程序员来说至关重要,特别是那些需要高效进程间通信机制的系统。对于系统和网络程序员而言,理解Unix套接字非常重要,因为这有助于创建更高效、更安全的应用程序。

在本章中,我们将涵盖以下主要主题:

  • Unix套接字
  • 构建聊天服务器
  • 使用Unix套接字提供HTTP服务

在本章结束时,你应该能够创建和管理Unix套接字,并理解它们的效率、安全性,以及它们是如何集成到文件系统命名空间中的。

# Unix套接字简介

Unix套接字,也称为Unix域套接字,它为同一台机器上的进程提供了一种快速、高效的相互通信方式,为进程间通信提供了一种本地的替代TCP/IP套接字的方案。这是Unix和类Unix操作系统(如Linux)特有的功能。

Unix套接字可以是面向流的(如TCP),也可以是面向数据报的(如UDP)。它们被表示为文件系统节点,如文件和目录。然而,它们不是普通文件,而是特殊的进程间通信机制。

Unix套接字有三个关键特性:

  • 效率:数据可以在进程之间直接传输,无需网络协议开销。
  • 文件系统命名空间:Unix套接字通过文件系统路径进行引用。这使得它们易于定位和使用,但也意味着它们会在文件系统中一直存在,直到被显式删除。
  • 安全性:可以使用文件系统权限来控制对Unix套接字的访问,基于用户和组ID提供一定级别的安全性。

接下来,让我们看看如何实际创建一个Unix套接字。

# 创建Unix套接字

让我们通过一个用Go语言编写的逐步示例,来创建一个Unix套接字服务器和客户端。之后,我们将了解如何使用lsof命令检查套接字:

  1. 套接字路径和清理操作:
socketPath := "/tmp/example.sock"
if err := os.Remove(socketPath); err != nil &&!os.IsNotExist(err) {
    log.Printf("Error removing socket file: %v", err)
    return
}
1
2
3
4
5
  • 路径定义:socketPath := "/tmp/example.sock"设置了Unix套接字的位置。
  • 清理逻辑:os.Remove(socketPath)尝试删除该位置上任何已存在的套接字文件,以避免在启动服务器时发生冲突。
  1. 创建并监听Unix套接字:
listener, err := net.Listen("unix", socketPath)
if err != nil {
    log.Printf("Error listening: %v", err)
    return
}
defer listener.Close()
fmt.Println("Listening on", socketPath)
1
2
3
4
5
6
7
  • 监听函数:net.Listen("unix", socketPath)在指定路径创建Unix套接字,并开始监听传入的连接。
  • 延迟语句:defer listener.Close()确保在主函数退出时关闭套接字,释放系统资源。
  1. 优雅关闭设置:
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
    <-signals
    fmt.Println("Received termination signal. Shutting down gracefully...")
    listener.Close()
    os.Remove(socketPath)
    os.Exit(0)
}()
1
2
3
4
5
6
7
8
9
  • 信号通道:signals := make(chan os.Signal, 1)设置了一个用于接收操作系统信号的通道。
  • 信号注册:signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)配置程序拦截SIGINT和SIGTERM信号,以便进行优雅关闭。
  • 信号处理协程:匿名的go func() { ... }()协程等待信号。接收到信号后,它会关闭监听器,删除套接字文件,然后退出程序。
  1. 连接接受循环:
for {
    conn, err := listener.Accept()
    if err != nil {
        log.Printf("Error accepting connection: %v", err)
        continue
    }

    go handleConnection(conn)
}
1
2
3
4
5
6
7
8
9
  • 无限循环:for { ... }循环持续等待并接受新连接。
  • 接受连接时的错误处理:如果listener.Accept()遇到错误(例如在服务器关闭期间),它会记录错误并继续下一次迭代,避免程序崩溃。
  1. 连接管理:
func handleConnection(conn net.Conn) {
    defer conn.Close()
    buffer := make([]byte, 1024)
    n, err := conn.Read(buffer)
    if err != nil {
        log.Printf("Error reading from connection: %v", err)
        return
    }
    
    fmt.Println("Received:", string(buffer[:n]))
    
    // Simulate a response back to the client
    response := []byte("Message received successfully\n")
    _, err = conn.Write(response)
    if err != nil {
        log.Printf("Error writing response to connection: %v", err)
        return
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 使用defer conn.Close()确保函数执行完毕后关闭连接,释放资源。
  • 使用buffer := make([]byte, 1024)分配一个字节缓冲区,用于接收传入的数据。
  • 使用n, err := conn.Read(buffer)读取传入数据,处理错误并在发生错误时退出。
  • 使用fmt.Println("Received:", string(buffer[:n]))显示接收到的消息,只显示缓冲区中读取的部分。
  • 使用response := []byte("Message received successfully\n")构造一个响应,用于确认收到消息。
  • 通过conn.Write(response)将响应发送回客户端,如果写入操作失败,则记录错误。

现在,我们可以通过执行以下命令来运行这段代码:

go run main.go
1

输出应该如下:

Listening on /tmp/example.sock
1

# 深入了解套接字创建

当我们使用Unix套接字类型和文件路径调用net.Listen时,Go运行时会在底层执行两个操作:在操作系统中创建套接字文件描述符,并将套接字绑定到指定的文件路径。

# 从操作系统的角度来看

当我说“在操作系统中创建一个套接字”时,指的是在操作系统内核中创建一个作为内部资源的套接字。这个操作就好比操作系统设置了一个通信端点。此时的套接字是由操作系统管理的一个抽象概念,允许进程发送和接收数据。请注意,这个套接字此时还没有与文件系统中的文件关联。它是存在于系统内存中的一个实体,由内核的网络或进程间通信子系统管理。

# 从文件系统的角度来看

这里的绑定是指将套接字与文件系统中的特定路径关联起来。这种绑定会创建一个套接字文件,这是一种特殊类型的文件,用作进程间通信的入口点或端点。

在文件系统中创建的“套接字文件”不是用于存储文本或二进制内容等数据的普通文件。相反,它是一种特殊类型的文件(在目录列表中通常显示为一个文件),代表套接字,并为进程提供了引用和使用它的方式。它是操作系统创建的抽象套接字在文件系统中获得命名表示的地方。

# 创建客户端

我们客户端的主要功能是连接到Unix套接字服务器,发送一条消息,然后关闭连接。

为了实现这个目标,让我们使用以下代码:

package main

import (
    "fmt"
    "net"
)

func main() {
    // Connect to the server at the UNIX socket
    conn, err := net.Dial("unix", "/tmp/example.sock")
    if err != nil {
        fmt.Println("Error dialing:", err)
        return
    }
    
    defer conn.Close()
    // Send a message
    _, err = conn.Write([]byte("Hello UNIX socket!\n"))
    if err != nil {
        fmt.Println("Error writing to socket:", err)
        return
    }
    
    buffer := make([]byte, 1024)
    n, err := conn.Read(buffer)
    if err != nil {
        fmt.Println("Error reading from socket:", err)
        return
    }
    
    fmt.Println("Server response:", string(buffer[:n]))
}
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

让我们详细分析这段客户端代码:

  • net.Dial("unix", "/tmp/example.sock")尝试建立与Unix套接字服务器的连接。
  • "unix"指定了连接类型,表示是Unix套接字。
  • "/tmp/example.sock"是套接字文件的路径,服务器预计在该路径监听。
  • 如果连接时出现错误(例如服务器未运行或套接字文件不存在),将打印错误并退出程序。
  • defer conn.Close()确保无论主函数如何结束,在其退出时都会关闭与套接字的连接。这是一个延迟调用,意味着它将在主函数结束时执行。
  • conn.Write([]byte("Hello UNIX socket!\n"))向服务器发送一条消息。
  • "Hello UNIX socket!\n"字符串被转换为字节切片,因为Write方法需要字节切片作为输入。
  • _字符用于忽略第一个返回值,该返回值是写入的字节数。
  • 如果向套接字写入时出现错误,将打印错误并退出程序。
  • 缓冲区创建:buffer := make([]byte, 1024)初始化一个长度为1024字节的字节切片,用于存储服务器的响应。
  • 读取操作:n, err := conn.Read(buffer)将服务器的响应读取到缓冲区中,其中n是读取的字节数,err捕获读取操作期间的任何错误。
  • 如果从套接字读取时出现错误,将打印错误并退出程序。
  • fmt.Println("Server response:", string(buffer[:n]))打印从服务器收到的响应。buffer[:n]将读取的字节转换回字符串以便显示。

# 使用lsof检查套接字

在类Unix系统中,“列出打开文件”(List Open Files,lsof)命令可以让我们了解进程访问的文件。Unix套接字被视为一种文件,可以使用lsof命令进行检查以收集相关信息。

要使用lsof检查套接字,我们应该先启动服务器程序,使其创建并监听Unix套接字。在终端中,你可以使用带有-U标志(表示Unix套接字)和-a标志(用于组合条件)的lsof命令,还可以指定套接字文件的路径:

lsof -Ua /tmp/example.sock
1

这个命令将显示有关Unix套接字的详细信息,包括正在监听该套接字的服务器的进程ID(PID)。如果在运行lsof时客户端已连接,你将看到服务器和客户端的相关条目。

客户端和服务器的完整代码可以在我们Git仓库的ch7/example1目录中找到。

# 构建聊天服务器

在编写任何代码之前,我们应该明确创建这个聊天系统的目标。

聊天服务器设计为监听位于/tmp/chat.sock的UNIX套接字(UNIX socket)。代码应负责创建和管理这个套接字,确保在启动前删除任何已存在的套接字文件,从而避免冲突。

服务器启动后,应保持一个持续的循环,不断等待新的客户端连接。每个成功的连接都在一个单独的协程(goroutine)中处理,使服务器能够同时管理多个客户端。

这个服务器的关键特性之一是它能够同时管理多个客户端连接。为了实现这一点,结合使用切片(slice)来存储客户端连接,以及使用互斥锁(mutex)进行并发访问控制是个不错的主意,这样可以确保对共享数据的线程安全操作。

每当有新客户端连接时,服务器应该向他们发送完整的消息历史记录,提供丰富的上下文体验。在聊天应用程序中,这种历史上下文至关重要,能让新加入的用户跟上对话进度。

是不是感觉要处理的事情太多了?别担心!我们会逐步扩展功能,直至完成服务器的最终版本。

为了帮助你理解如何使用Go语言基于UNIX套接字开发聊天服务器,将最终版本分解为更简单的初步阶段是很有效的方法。每个阶段都会引入一个关键特性或概念,逐步构建出最终版本。以下是分步指南:

  1. 基本的UNIX套接字服务器: 创建一个简单的服务器,监听UNIX套接字并能接受连接:
package main

import (
    "fmt"
    "net"
    "os"
)

const socketPath = "/tmp/example.sock"

func main() {
    os.Remove(socketPath)
    listener, err := net.Listen("unix", socketPath)
    if err != nil {
        fmt.Println("Error creating listener:", err)
        return
    }
    
    defer listener.Close()
    fmt.Println("Server is listening...")
    conn, err := listener.Accept()
    if err != nil {
        fmt.Println("Error accepting connection:", err)
        return
    }
    conn.Close()
}
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
  1. 处理单个客户端: 扩展服务器,使其能够从一个客户端读取消息并打印到控制台:
// ... (previous imports)
func main() {
    // ... (existing setup and listener code)
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting connection:", err)
            continue
        }

        handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buffer := make([]byte, 1024)
    n, err := conn.Read(buffer)
    if err != nil {
        fmt.Println("Error reading from connection:", err)
        return
    }
    fmt.Println("Received:", string(buffer[:n]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 处理多个客户端: 修改服务器,使其能够并发处理多个客户端连接:
// ... (previous imports)
var (
    clients []net.Conn
    mutex   sync.Mutex
)

func main() {
    // ... (existing setup and listener code)

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting connection:", err)
            continue
        }
        mutex.Lock()
        clients = append(clients, conn)
        mutex.Unlock()

        go handleConnection(conn)
    }
}

// ... (existing handleConnection function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 向所有客户端广播消息: 实现将接收到的消息广播给所有已连接客户端的功能:
// ... (previous imports and global variables)
func main() {
    // ... (existing setup and listener code)
    for {
        // ... (existing connection acceptance code)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buffer := make([]byte, 1024)
    for {
        n, err := conn.Read(buffer)
        if err != nil {
            removeClient(conn)
            break
        }
        message := string(buffer[:n])

        broadcastMessage(message)
    }
}

func broadcastMessage(message string) {
    mutex.Lock()
    defer mutex.Unlock()
    for _, client := range clients {
        client.Write([]byte(message + "\n"))
    }
}

func removeClient(conn net.Conn) {
    // ... (client removal logic)
}
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
  1. 添加消息历史记录: 存储消息历史记录,并在新客户端连接时发送给他们:
// ... (previous imports, global variables, and main function)

func handleConnection(conn net.Conn) {
    // Send message history to the new client
    for _, msg := range messageHistory {
        conn.Write([]byte(msg + "\n"))
    }
    // ... (existing reading and broadcasting code)
}

// ... (existing broadcastMessage and removeClient functions)
1
2
3
4
5
6
7
8
9
10
11

很好!我们已经完成了具备所有功能的聊天服务器。现在,是时候创建我们的客户端了。

客户端应该与监听特定UNIX套接字(/tmp/chat.sock)的服务器建立连接。建立连接后,客户端将向服务器发送消息。此外,客户端应该处理来自服务器的响应,读取并在控制台显示。在整个操作过程(连接、发送和接收)中,客户端应该处理任何潜在的错误,如果发生错误则打印出来。最后,无论客户端是正常退出还是因错误退出,都必须确保在退出前正确关闭套接字连接。

现在,让我们将这个客户端的开发分解为更简单的阶段:

  1. 与服务器建立连接: 创建一个连接到UNIX套接字服务器的客户端:
package main

import (
    "fmt"
    "net"
)

const socketPath = "/tmp/chat.sock"

func main() {
    conn, err := net.Dial("unix", socketPath)
    if err != nil {
        fmt.Println("Failed to connect to server:", err)
        return
    }
    defer conn.Close()
    fmt.Println("Connected to server.")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 监听来自服务器的消息: 添加监听并打印来自服务器消息的功能:
// ... (previous imports)
func main() {
    // ... (existing connection code)
    go func() {
        scanner := bufio.NewScanner(conn)
        for scanner.Scan() {
            fmt.Println("Message from server:", scanner.Text())
        }
    }()
    // Prevent the main goroutine from exiting immediately
    fmt.Println("Connected. Press Ctrl+C to exit.")

    select {} // Blocks forever
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 向服务器发送消息: 使客户端能够向服务器发送消息:
// ... (previous imports)
func main() {
    // ... (existing connection and server listening code)
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Println("Enter message:")
    for scanner.Scan() {
        message := scanner.Text()
        conn.Write([]byte(message))
    }
}
1
2
3
4
5
6
7
8
9
10
  1. 使用WaitGroup进行正确同步: 使用sync.WaitGroup管理协程同步,防止程序过早终止:
// ... (previous imports)
func main() {
    // ... (existing connection code)
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ... (existing server message handling code)
    }()
    
    // ... (existing message sending code)
    wg.Wait() // Wait for the goroutine to finish
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 完整的聊天客户端

既然我们已经熟悉了细节,让我们来看一下完整的客户端代码:

package main

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "sync"
)

const socketPath = "/tmp/chat.sock"

func main() {
    conn, err := net.Dial("unix", socketPath)
    if err != nil {
        fmt.Println("Failed to connect to server:", err)
        return
    }
    defer conn.Close()

    var wg sync.WaitGroup
    wg.Add(1)

    // 监听服务器消息
    go func() {
        defer wg.Done()
        scanner := bufio.NewScanner(conn)
        for scanner.Scan() {
            fmt.Println("Message from server:", scanner.Text())
        }
    }()

    // 向服务器发送消息
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Println("message:")
    for scanner.Scan() {
        message := scanner.Text()
        conn.Write([]byte(message))
    }

    wg.Wait()
}
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

随着复杂度的增加,逐步开发有助于理解每个组件在最终程序中的作用。

现在轮到你了!启动服务器并连接几个客户端,体验一下我们的聊天系统。

客户端和服务器的完整版本可以在我们GitHub仓库的ch7/chat目录中找到。

# 在Unix域套接字下提供HTTP服务

在Unix域套接字下暴露HTTP API?这在网络领域可是一种有趣的做法。让我们来探索一下这种非传统方式的优势。

对于那些应局限于特定机器的服务而言,Unix域套接字是一种安全的选择。它们通过文件系统权限提供细粒度的访问控制,使管理哪些用户或进程能够与你的HTTP API进行交互变得更加容易。

当你可以享受更低的延迟和更少的上下文切换时,为什么还要满足于传统的网络方式呢?这在高吞吐量的环境中尤为有用。

通过使用Unix域套接字,你可以避免占用TCP端口,在某些系统中,TCP端口可能是一种有限的资源。

Unix域套接字无需管理IP地址和端口号,简化了设置和配置过程,对于本地通信来说更是如此。此外,它们与Unix/Linux生态系统无缝集成,对于深度嵌入该环境的应用程序而言,自然是不二之选。

对于遗留系统或有特定协议要求的应用程序,Unix域套接字可能是实现高效通信的最佳甚至是唯一选择。

要在Go语言中创建一个监听Unix域套接字的HTTP服务器,可以使用net和net/http包。

下面我们逐步来探索这个服务器的实现:

  1. HTTP处理函数:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    _, err := w.Write([]byte("Hello, world!"))
    if err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        log.Println("Error writing response:", err)
    }
})
1
2
3
4
5
6
7

我们首先使用http.HandleFunc定义一个HTTP处理函数。这个函数处理所有发往根路径(/)的HTTP请求,并使用响应写入器返回“Hello, world!”。 2. Unix套接字和监听器设置:

socketPath := "/tmp/go-server.sock"
listener, err := net.Listen("unix", socketPath)
if err != nil {
    log.Fatal("Listen (UNIX socket):", err)
}

log.Println("Server is listening on", socketPath)
1
2
3
4
5
6
7

我们将Unix套接字路径指定为socketPath,这里设置为/tmp/go-server.sock。 net.Listen("unix", socketPath)用于设置一个Unix套接字服务器,以在指定路径上接受传入的连接。 我们使用标准的Go语言log包进行基本的日志记录。当服务器在指定的套接字路径上监听时,记录一条消息。 3. 优雅关闭:

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
1
2

我们创建了一个信号通道sigCh,用于捕获SIGINT(Ctrl + C)和SIGTERM(终止信号),以便实现服务器的优雅关闭。 使用signal.Notify在接收到这些信号时通知通道。 4. 用于关闭的goroutine:

go func() {
    <-sigCh
    log.Println("Shutting down gracefully...")
    listener.Close()
    os.Remove(socketPath)
    os.Exit(0)
}()
1
2
3
4
5
6
7

我们启动一个goroutine来处理优雅关闭。该goroutine在sigCh上等待信号。 当接收到信号时,它会记录一条消息,使用listener.Close()关闭Unix套接字监听器,使用os.Remove(socketPath)删除Unix套接字文件,并使用os.Exit(0)退出程序。 5. 启动HTTP服务器:

err = http.Serve(listener, nil)
if err != nil && err != http.ErrServerClosed {
    log.Fatal("HTTP server error:", err)
}
1
2
3
4

我们使用http.Serve(listener, nil)启动HTTP服务器。它在我们之前创建的Unix套接字监听器上监听传入的HTTP请求。 我们处理http.Serve返回的任何错误,并在必要时记录它们。我们还检查特殊情况http.ErrServerClosed,以确定服务器是否被优雅关闭。

了解了服务器的详细实现后,现在让我们来处理客户端的设置。

# 客户端

在这种情况下创建客户端时,我们假设HTTP响应体是基于文本的(纯文本)。

注意:
如果处理的是二进制数据,则必须以不同的方式处理。

下面我们逐步创建客户端:

package main

import (
    "bufio"
    "fmt"
    "net"
    "net/http"
    "net/textproto"
    "strings"
)

const socketPath = "/tmp/go-server.sock"

func main() {
    // 连接Unix套接字
    conn, err := net.Dial("unix", socketPath)
    if err != nil {
        fmt.Println("Error connecting to the Unix socket:", err)
        return
    }
    
    defer conn.Close()
    // 发送HTTP请求
    request := "GET / HTTP/1.1\r\n" +
        "Host: localhost\r\n" +
        "\r\n"
    _, err = conn.Write([]byte(request))
    if err != nil {
        fmt.Println("Error sending the request:", err)
        return
    }
    
    // 读取响应
    reader := bufio.NewReader(conn)
    tp := textproto.NewReader(reader)
    // 读取并打印状态行
    statusLine, err := tp.ReadLine()
    if err != nil {
        fmt.Println("Error reading the status line:", err)
        return
    }
    
    fmt.Println("Status Line:", statusLine)
    // 读取并打印头部信息
    headers, err := tp.ReadMIMEHeader()
    if err != nil {
        fmt.Println("Error reading headers:", err)
        return
    }
    
    for key, values := range headers {
        for _, value := range values {
            fmt.Printf("%s: %s\n", key, value)
        }
    }
    
    // 读取并打印响应体(假设是基于文本的)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err.Error() != "EOF" {
                fmt.Println("Error reading the response body:", err)
            }
            break
        }
        fmt.Print(line)
    }
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

代码主要包含三个部分:

  • 连接Unix套接字:使用net.Dial连接到位于/tmp/go-server.sock的Unix套接字。
  • 发送HTTP请求:向根路径(/)发送一个简单的HTTP GET请求。包含Host: localhost头部以符合HTTP/1.1标准。
  • 读取响应:使用bufio.Reader从服务器读取响应。解析并打印状态行和头部信息,然后打印响应体。

现在我们来探讨一下其中的一些选择及其细节。

这个请求旨在通过网络连接(如Unix域套接字)发送到HTTP服务器。下面我们将这个请求分解为各个组成部分。

# HTTP请求行

GET / HTTP/1.1\r\n
1
  • 方法:GET - 这是正在使用的HTTP方法。GET用于从指定资源请求数据。在这种情况下,它请求服务器发送位于根URL(/)的数据。
  • 路径:/ - 这是所请求资源的路径。/表示根路径,通常对应网站或API的主页。
  • 协议版本:HTTP/1.1 - 这指定了正在使用的HTTP协议版本。HTTP/1.1是一个常见版本,相比HTTP/1.0有多项改进,比如持久连接。
  • 行尾标识:\r\n - 这是一个回车符(\r)后跟一个换行符(\n),在HTTP协议中共同表示一行的结束。HTTP头部必须以\r\n结尾。

# HTTP请求头部

Host: localhost\r\n
1
  • 主机头部:Host: localhost - 主机头部指定请求发送到的服务器域名(主机)。在HTTP/1.1中这是必需的,用于区分同一服务器上托管的不同域名(虚拟主机)。这里使用localhost作为主机。
  • 头部行尾标识:\r\n - 同样,回车符和换行符表示头部行的结束。

# 表示头部结束的空行

\r\n
1

这个空行(仅包含\r\n)表示HTTP请求头部部分的结束和主体部分的开始。由于GET请求通常不包含主体,这一行表示请求的结束。

# textproto包

在我们的程序中,使用textproto包来读取和解析来自HTTP服务器的响应头部,这是为什么呢? 首先是为了方便:textproto简化了读取和解析基于文本的协议的过程。如果不使用它,就必须手动解析响应,这可能容易出错且效率低下。

此外,textproto确保符合基于文本的协议规范。它能正确处理诸如行尾标识(\r\n)和头部格式等细节。

它与Go语言的缓冲I/O(bufio)配合良好,对于数据可能突发到达的网络通信来说效率很高。

虽然textproto是为处理HTTP而设计的,但它足够通用,可用于其他基于文本的协议,是Go标准库中用于一般网络编程的有用工具。

现在我们已经探索了通过HTTP使用Unix套接字的应用程序,接下来看看一些重要的性能考量,以优化基于套接字的应用程序,以及最常见的用例。

通过HTTP进行通信的客户端和服务器的完整版本,可以在我们Git仓库的ch7/http2unix目录中找到。

# 性能

Unix域套接字不需要网络堆栈的开销,因为无需通过网络层路由数据。这减少了处理网络协议所消耗的CPU周期。Unix域套接字通常在内核中允许更高效的数据传输机制,比如发送文件时,可以减少内核和用户空间之间的数据复制量。它们在同一主机内进行通信,因此延迟通常比TCP套接字更低,即使是在同一机器上的进程之间进行通信,TCP套接字也可能涉及更复杂的路由。

你可能会问:它比调用回环接口(localhost)更快吗?

是的!回环接口仍然要经过TCP/IP堆栈,即使数据并未离开机器。这涉及更多处理,比如将数据封装成TCP段和IP数据包。

在涉及内核和用户空间之间的数据复制时,Unix域套接字可能更高效。一些Unix域套接字实现允许零复制操作,即数据可以在客户端和服务器之间直接传递,而无需冗余复制。使用TCP/IP则无法做到这一点,因为其通信通常涉及内核和用户空间之间更多的数据复制。

# 其他常见用例

有多个系统依赖Unix域套接字的优势,例如:

  • 系统进程间通信(System VIPC):这是类Unix操作系统中的一类机制,包括Unix域套接字、消息队列、信号量集和共享内存。Unix域套接字常用于同一系统内进程之间的高效快速通信。
  • X窗口系统(X11):X11是类Unix操作系统中使用的图形窗口系统,它可以使用Unix域套接字在X服务器和客户端应用程序之间进行通信,实现显示和输入管理。
  • D-Bus:D-Bus是一个消息总线系统,用于应用程序之间的通信。它在Linux系统中广泛使用,并且在进程间的本地通信中大量依赖Unix域套接字。
  • Systemd:Systemd是Linux的初始化系统和服务管理器,它使用Unix域套接字在其各个组件和服务之间进行通信,对系统启动过程和系统管理至关重要。
  • MySQL和PostgreSQL:这些流行的关系型数据库管理系统可以使用Unix域套接字进行本地客户端 - 服务器通信,为应用程序连接到数据库服务器提供了一种快速且安全的方式。
  • Redis:Redis是一个内存键值存储,它可以使用Unix域套接字进行本地客户端 - 服务器通信,提供低延迟和高吞吐量的数据访问。
  • Nginx和Apache:这些Web服务器可以使用Unix域套接字与后端应用服务器或FastCGI进程进行通信。当两个进程在同一台机器上时,这是一种比TCP/IP套接字更高效的代理请求方式。

了解了Unix套接字的用例后,让我们回顾并总结一下所学内容。

# 总结

在本章中,我们探索了Unix套接字的基本概念和实际应用。我们了解了Unix套接字及其在Unix和类Unix系统的进程间通信中的作用。本章深入介绍了Unix套接字与TCP/IP套接字的区别,强调了它们在本地高效进程间通信中的应用。

通过示例,你获得了创建和管理Unix套接字服务器及客户端的实践经验。此外,本章还突出了Unix套接字在无网络协议开销的数据传输方面的效率,以及通过文件系统权限控制的安全特性。

这些知识对于开发高效安全的软件系统至关重要,提升了读者在进程间通信场景中设计和实现强大的网络应用程序的能力。

展望未来,下一章(第8章 “内存管理”)将把重点从进程间通信转移到Go运行时及其垃圾回收器的内部机制。我们将探索内存是如何分配、管理和优化的。

上次更新: 2025/05/15, 21:40:15
第6章 理解进程间通信中的管道
第8章 内存管理

← 第6章 理解进程间通信中的管道 第8章 内存管理→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式