浅谈MCP原理

MCP概述

MCP 本质上是一个基于 JSON-RPC 2.0 标准的通信协议,通常运行在 SSE (Server-Sent Events)Stdio (标准输入输出) 传输层之上。

下面我将分三个部分为你深度解析:

  1. @Tool 注解是如何被"翻译"成 MCP 协议的?(反射与元数据提取)
  2. MCP 协议长什么样?(JSON-RPC 消息结构拆解)
  3. 连接与断连的底层握手流程(SSE/Stdio 的生命周期)

一、从 @Tool 到 MCP 协议:底层解析流程

当你在 Spring Boot 中写下 @Tool 注解时,Spring AI 内部发生了一系列"魔法"操作,将 Java 方法变成了 LLM 能理解的 JSON Schema。

1. 扫描与注册 (Startup Phase)
  • 扫描器 :Spring 容器启动时,ToolCallbackProvider (具体实现是 BeanToolCallbackProvider) 会扫描所有 Bean。
  • 识别 :发现方法上有 @Tool 注解。
  • 提取元数据
    • Name : 取 @Tool(name="...") 或默认方法名。
    • Description : 取 @Tool(description="..."),这是告诉 LLM"什么时候用这个工具"的关键。
    • Parameters : 分析方法参数,读取 @ToolParam 的描述。
2. 生成 JSON Schema (Runtime Phase)

Spring AI 使用内部的 ObjectMapper 和类型转换器,将 Java 参数类型转换为 JSON Schema 格式。这是 MCP 协议的核心负载。

Java 代码:

复制代码
@Tool(name = "get_weather", description = "获取指定城市的天气")
public String getWeather(@ToolParam(description = "城市名称,如 Beijing") String city, 
                         @ToolParam(description = "温度单位,Celsius 或 Fahrenheit") String unit) {
    // ...
}

转换后的 JSON Schema (LLM 看到的):

复制代码
{
  "type": "object",
  "properties": {
    "city": {
      "type": "string",
      "description": "城市名称,如 Beijing"
    },
    "unit": {
      "type": "string",
      "description": "温度单位,Celsius 或 Fahrenheit",
      "enum": ["Celsius", "Fahrenheit"] 
    }
  },
  "required": ["city", "unit"]
}
3. 封装为 MCP Tool Definition

当 LLM 客户端(Consumer)通过 MCP 协议请求"列出所有可用工具"时,Server 端会将上述信息封装成标准的 MCP 响应:

复制代码
{
  "name": "get_weather",
  "description": "获取指定城市的天气",
  "inputSchema": { 
    "type": "object",
    "properties": { ... } // 上面生成的 JSON Schema
  }
}

结论@Tool 注解本身不发送任何东西。是 Spring AI 的反射机制 读取注解,生成 JSON Schema,然后在收到 MCP 协议的 tools/list 请求时,动态构建 JSON 响应发回去。


二、MCP 协议是什么?由什么组成?

MCP 协议没有发明新的轮子,它严格遵循 JSON-RPC 2.0 规范,并定义了一套特定的 Method Names (方法名)Notification (通知)

1. 核心数据结构 (JSON-RPC 2.0)

所有 MCP 消息都是 JSON 对象,包含以下字段:

  • jsonrpc: 固定为 "2.0"
  • id: 请求的唯一 ID(用于匹配响应),通知(Notification)没有 ID。
  • method: 调用的方法名(MCP 定义的标准动词)。
  • params: 参数对象。
  • result: 响应结果(仅在 Response 中)。
  • error: 错误信息(仅在 Error Response 中)。
2. MCP 的核心方法集 (Protocol Methods)

MCP 定义了三类核心交互:

表格

类别 方法名 (Method) 作用 方向
初始化 initialize 协商协议版本、能力交换 Client -> Server
notifications/initialized 告知初始化完成 Client -> Server
工具发现 tools/list 获取所有可用工具列表 (含 JSON Schema) Client -> Server
工具调用 tools/call 执行具体工具,传入参数 Client -> Server
资源/提示 resources/list 获取可用数据资源列表 Client -> Server
prompts/list 获取可用提示词模板列表 Client -> Server
3. 真实报文示例

场景:Client 请求调用工具

Request (Client -> Server):

复制代码
{
  "jsonrpc": "2.0",
  "id": 1001,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Beijing",
      "unit": "Celsius"
    }
  }
}

Response (Server -> Client):

复制代码
{
  "jsonrpc": "2.0",
  "id": 1001,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "北京今天晴朗,气温 25°C。"
      }
    ],
    "isError": false
  }
}

三、怎么建连和断连?(Transport Layer)

MCP 协议本身只是 JSON 数据,它需要"交通工具"来传输。Spring AI MCP 主要支持两种传输层:SSE (HTTP)Stdio (进程间通信)

模式 A:基于 SSE (Server-Sent Events) - 最常用 (Web 场景)

这是 Spring AI MCP WebMvc/WebFlux Starter 默认的模式。

1. 建连流程 (Handshake)

SSE 是非对称 的:客户端发起 HTTP 请求建立长连接,服务端通过这个连接单向推送消息;但客户端要发送消息(如调用工具),通常需要另一个 HTTP POST 通道(或者在较新的 MCP 实现中复用 SSE 连接的双向能力,但在 Spring AI 当前实现中通常是 SSE 接收 + HTTP POST 发送双向 SSE)。

Spring AI 目前的标准 SSE 实现流程:

  1. Client 发起 SSE 连接 :
    • Client 发送 GET /mcp/message (带 Accept: text/event-stream)。
    • Server 挂起连接,保持 HTTP 200 OK,开始流式传输。
  2. Server 推送 Endpoint (可选但常见) :
    • 有些实现中,Server 会在 SSE 流中发送一个事件,告诉 Client:"如果你想发给我消息,请 POST 到这个 URL"。
    • 注:在 Spring AI 的简化实现中,通常约定同一个 URL 既用于 SSE 接收,也用于 POST 发送,或者通过 Header 区分。
  3. 协议握手 (JSON-RPC) :
    • Client 通过 POST (或 SSE 反向通道) 发送 {"method": "initialize", ...}
    • Server 通过 SSE 流推送 {"method": "initialize", ...} 的响应。
    • Client 发送 {"method": "notifications/initialized"} (通知,无响应)。
    • 连接建立成功 ,开始正常业务交互 (tools/list, tools/call)。
2. 断连流程
  • 主动断开 :
    • Client 关闭 HTTP 连接(停止监听 SSE 流)。
    • Server 检测到 ClientAbortException 或连接关闭信号,清理会话资源,移除该 Client 注册的临时上下文。
  • 心跳检测 :
    • MCP 协议本身没有强制的心跳,但底层 HTTP/SSE 通常依赖 TCP Keep-Alive 或应用层定期发送 ping (如果实现了扩展) 来检测死链。
模式 B:基于 Stdio (Standard Input/Output) - 本地 CLI 场景

这是 MCP 原始设计最常用的模式(类似 Python 脚本调用本地工具)。

1. 建连流程
  1. 启动进程 : Client (如 Claude Desktop 或 Java 应用) 作为一个父进程,通过操作系统命令 fork/exec 启动 Server 进程(例如 java -jar mcp-server.jar)。
  2. 绑定管道 :
    • Client 将自己的 stdout 连接到 Server 的 stdin
    • Client 将自己的 stdin 连接到 Server 的 stdout
  3. 握手 :
    • 双方直接通过 stdin/stdout 交换 JSON-RPC 字符串。
    • 流程同 SSE (initialize -> initialized)。
2. 断连流程
  • 退出信号 : Client 向 Server 进程发送 SIGTERMSIGKILL
  • EOF: Client 关闭写入端 (stdin),Server 读到 EOF (End Of File),自动退出。
  • 优雅退出 : Client 发送 JSON-RPC notifications/cancelled 或自定义退出指令,Server 执行 System.exit(0)

四、总结:底层全链路图解

假设你调用了 chatClient.prompt(...).call() 触发了工具调用:

  1. LLM 决策 : OpenAI 返回 function_call: { name: "get_weather", args: {...} }

  2. Spring AI 拦截 : ChatClient 发现需要调用工具,找到对应的 ToolCallback (即 MCP Client)。

  3. 构建 RPC : MCP Client 构建 JSON-RPC 请求:json

    复制代码
    { "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "get_weather", "arguments": {...} } }
  4. 传输层发送 :

    • 如果是 SSE: 通过 WebClient 发起 HTTP POST 请求,Body 是上面的 JSON。
    • 如果是 Stdio : 向子进程的 stdin 写入 JSON 字符串 + \n
  5. Server 端接收 :

    • Spring MVC/Flux 控制器接收到 JSON。
    • McpSyncServer / McpAsyncServer 解析 JSON,根据 method 路由到 tools/call 处理器。
    • 查找本地注册的 ToolExecutionService
  6. 反射执行 :

    • 根据 name ("get_weather") 找到对应的 Java Bean 和方法。
    • 利用反射 (Method.invoke) 调用你的 @Tool 方法,传入参数。
  7. 返回结果 :

    • Java 方法返回结果 "25°C"。
    • Server 将其封装为 JSON-RPC Response。
    • 通过 SSE 流推回给 Client。
  8. 最终回答 :

    • Client 收到结果,再次请求 LLM:"工具返回了 25°C,请回答用户"。
    • LLM 生成最终自然语言回复。

MCP进阶--studio

执行过程:

Client (Claude)\] \[操作系统管道 (Pipe)\] \[Spring AI Server (你的代码)

| | |

| 1. 发送 JSON 字符串 | |

| -------------------------> | (写入 stdin) |

| (例如: {"method":...}) | |

| | |

| | <--- Spring 内部线程监听 stdin ---|

| | |

| | 2. 读取一行 JSON |

| | -----------------------------> |

| | |

| | 3. 【反序列化】JSON -> Java 对象 |

| | (识别出这是调用 "add" 工具) |

| | |

| | 4. 【反射调用】你的 @Tool 方法 |

| | -----------------------------> |

| | result = myService.add(1, 2)|

| | <----------------------------- |

| | (拿到返回值 3) |

| | |

| | 5. 【序列化】Java 对象 -> JSON |

| | (把 3 包装成 {"result": 3}...) |

| | |

| | 6. 【关键一步】打印到 stdout |

| | System.out.println(jsonStr) |

| | -----------------------------> | (写入 stdout)

| | |

| 7. 读取返回结果 | <---------------------------- |

| <------------------------- | |

| (收到 {"result": 3}...) | |

注意:

其实studio就是system.out|system.in

system也是跟os文件句柄交互

小区大门和管道(System.out/Socket/File) -> OS 强映射

管道模式:

Java 进程 (Server) \] \[ Client 进程 (Node.js/Python)


(内部) System.out -----(管道 A)-----> (内部) 读取流 (Readable)

(我要发结果给你) (数据流向 -->) (我要收你的结果)

(内部) System.in <-----(管道 B)----- (内部) 写入流 (Writable)

(我要收你指令) (<-- 数据流向) (我要发指令给你)

studio建立管道发送信息

但是他是特殊管道

复制代码
import java.io.*;
import java.util.Arrays;

public class MyMcpClient {
    public static void main(String[] args) {
        System.out.println("[Java Client] 准备启动 MCP Server...");

        // 【步骤 1】配置启动命令
        // 假设你的 jar 包在当前目录,名字叫 my-server.jar
        // 如果 java 不在环境变量里,这里要写绝对路径,比如 "C:\\Program Files\\Java\\jdk-17\\bin\\java.exe"
        ProcessBuilder pb = new ProcessBuilder("java", "-jar", "my-server.jar");

        // 【步骤 2】关键设置!
        // 默认情况下 ProcessBuilder 就会创建管道,但为了保险,我们显式地不重定向到继承(除非你想看 Server 的日志直接刷屏)
        // 如果你想看 Server 的 System.err 日志直接在当前控制台显示,可以解开下面这行:
        // pb.redirectError(ProcessBuilder.Redirect.INHERIT); 
        
        try {
            // 【步骤 3】启动进程 (魔法发生在这里)
            // 此时,操作系统创建了子进程,并建立了管道连接
            Process serverProcess = pb.start();

            // 【步骤 4】获取通道 (Streams)
            // 注意名字的反直觉性:
            // serverProcess.getOutputStream() -> 这是写入 Server 的 stdin 的流 (Client 的笔)
            // serverProcess.getInputStream()  -> 这是读取 Server 的 stdout 的流 (Client 的耳朵)
            
            PrintWriter toServer = new PrintWriter(new OutputStreamWriter(serverProcess.getOutputStream()), true);
            BufferedReader fromServer = new BufferedReader(new InputStreamReader(serverProcess.getInputStream()));
            
            // 单独开个线程读取 Server 的错误日志 (System.err),防止阻塞或丢失
            new Thread(() -> {
                try (BufferedReader errReader = new BufferedReader(new InputStreamReader(serverProcess.getErrorStream()))) {
                    String line;
                    while ((line = errReader.readLine()) != null) {
                        System.err.println("[Server Err] " + line);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();

            System.out.println("[Java Client] Server 已启动,正在发送测试指令...");

            // 稍微等一下,给 JVM 启动留点时间
            Thread.sleep(500);

            // 【步骤 5】发送数据 (写入 Server 的 System.in)
            String request = "{\"method\": \"initialize\", \"params\": {\"version\": \"1.0\"}}";
            toServer.println(request);
            // flush 很重要,确保数据立刻推送到管道
            toServer.flush();

            // 【步骤 6】接收数据 (读取 Server 的 System.out)
            String response = fromServer.readLine();
            if (response != null) {
                System.out.println("[Java Client] 收到 Server 回复: " + response);
            } else {
                System.out.println("[Java Client] Server 没有返回数据就关闭了。");
            }

            // 【步骤 7】清理
            System.out.println("[Java Client] 测试结束,关闭连接。");
            serverProcess.destroy(); // 杀死子进程

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

pb.start();建立连接秘密

慢动作拆解:pb.start() 的微观世界

假设你的 Java Client 正在运行,调用了 pb.start()。此时,CPU 会从 用户态 (User Mode) 切换到 内核态 (Kernel Mode),执行以下步骤:

第 1 步:创建管道 (The Pipes)

操作系统动作pipe()

在启动新进程之前 ,Java 告诉操作系统:"我要两根管子。"

操作系统在内核内存中开辟两块缓冲区,并返回 4 个文件描述符 (File Descriptors, FD)

  • 管道 A (用于 stdout) :
    • FD_A_Read (读端)
    • FD_A_Write (写端)
  • 管道 B (用于 stdin) :
    • FD_B_Read (读端)
    • FD_B_Write (写端)

此刻状态

  • Java Client 拿着 FD_A_Read (准备读 Server 的输出)
  • Java Client 拿着 FD_B_Write (准备写 Server 的输入)
  • 另外两个端 (FD_A_Write, FD_B_Read) 暂时悬空,等待分配给未来的子进程。
第 2 步:复制进程 (The Fork)

操作系统动作fork() (Linux) / 类似机制 (Windows)

操作系统把当前的 Java Client 进程 完整复制了一份。

  • 父进程:原来的 Java Client。
  • 子进程 :一个新的、几乎一模一样的进程(此时它还没运行 java -jar,它只是 Client 的克隆体)。

关键点 :子进程继承 了父进程所有的打开文件描述符。

所以,子进程现在手里也有那 4 个 FD:FD_A_Read, FD_A_Write, FD_B_Read, FD_B_Write

第 3 步:重定向 (The Redirect) ------ 这是"建连"的核心!

操作系统动作dup2() (Duplicate File Descriptor)

在子进程运行真正的程序之前,操作系统对子进程的文件描述符表进行偷梁换柱

  1. 处理标准输入 (stdin, FD 0)

    • 操作系统关闭子进程原本的 FD 0 (通常是键盘)。
    • 操作系统执行 dup2(FD_B_Read, 0)
    • 结果 :子进程的 FD 0 (stdin) 现在指向了 管道 B 的读端
    • 含义:以后子进程想读 stdin,其实是从管道 B 读。
  2. 处理标准输出 (stdout, FD 1)

    • 操作系统关闭子进程原本的 FD 1 (通常是屏幕)。
    • 操作系统执行 dup2(FD_A_Write, 1)
    • 结果 :子进程的 FD 1 (stdout) 现在指向了 管道 A 的写端
    • 含义:以后子进程想写 stdout,其实是往管道 A 写。
  3. 清理多余句柄

    • 关闭子进程中不需要的 FD_A_ReadFD_B_Write (因为子进程不需要读自己的输出,也不需要写自己的输入)。

此刻状态

  • 子进程
    • System.in (FD 0) -> 连着 FD_B_Read
    • System.out (FD 1) -> 连着 FD_A_Write
  • 父进程 (Client)
    • 拿着 FD_A_Read (等着读)
    • 拿着 FD_B_Write (准备写)
  • 管道通了! 就像两根水管接上了。
第 4 步:执行程序 (The Exec)

操作系统动作execve()

子进程现在的身份还是"Java Client 的克隆",这没用。

操作系统加载硬盘上的 java.exe (或 bin/java) 二进制文件,覆盖掉子进程当前的内存代码。

  • 子进程变成了全新的 Java Server 进程
  • 但是! 刚才重定向的 文件描述符 (FD 0, FD 1) 保持不变 !这是 exec 的特性。

奇迹时刻

新的 Java Server 启动了。

当 JVM 初始化 System.in 时,它去查 FD 0,发现是一个管道读端

当 JVM 初始化 System.out 时,它去查 FD 1,发现是一个管道写端
连接完成。

第 5 步:返回 (Return)

pb.start() 方法执行完毕,返回一个 Process 对象给父进程 (Client)。

这个 Process 对象里封装了父进程保留的那两个句柄 (FD_A_Read, FD_B_Write)。

Java Client (父进程) \] \[ 操作系统内核 (Kernel) \] \[ Java Server (子进程)


  1. 写入数据

clientOut.write("Hi") -----> FD_B_Write --------(内核缓冲区)--------> FD 0 (stdin) -----> System.in.read()

(Client 的流) (管道 B 写端) (被重定向过) (Server 读到 "Hi")

  1. 读取数据

serverIn.read() <----- FD 1 (stdout) <-------(内核缓冲区)------- FD_A_Read <----- clientIn.read()

(Server 写到) (被重定向过) (管道 A 读端) (Client 读到结果)

System.out.println()

是不是有点懵,别急

绑定

在调用 pb.start() 之前,Client 进程创建了两个管道。

此时,只有 Client 知道这两个管道的存在

  • Pipe_A: Client 持有 Read_AWrite_A
  • Pipe_B: Client 持有 Read_BWrite_B
  • Server 还不存在。世界上没有任何其他进程持有这些句柄。
  • 绑定状态:未建立。
绑定的关键时刻:fork() (遗传)

当 Client 调用 fork() 时,操作系统做了一个动作:复制当前进程的"文件描述符表"(File Descriptor Table)

  • 动作 :OS 创建了一个新进程(我们叫它 Baby)。
  • 结果Baby 的文件描述符表是 Client 的完美克隆
    • BabyFD 3 指向 Read_A (和 Client 一样)。
    • BabyFD 4 指向 Write_A (和 Client 一样)。
    • BabyFD 5 指向 Read_B ...
  • 绑定含义 :就在这一瞬间,Client 和 Baby 建立了唯一的共享关系。因为它们共同持有了同一组内核对象(管道)。
  • 为什么不会连错? 因为 Baby 是 Client 复制出来的,它只继承 了 Client 手里的东西。Client 2 手里的管道,Client 1 根本没有,所以 Baby 也绝对不可能继承到 Client 2 的东西。
    • 这就是绑定的物理基础:血缘关系(父子进程)。
绑定的固化时刻:dup2() (重定向)

现在 Baby 手里拿着所有的管子头,但它还不知道哪个是输入,哪个是输出。OS 立即执行 dup2 进行强制绑定

  • 指令dup2(Write_B, 0) (把管道 B 的写端,强制映射到标准输入 FD 0)

  • 指令dup2(Read_A, 1) (把管道 A 的读端,强制映射到标准输出 FD 1)

  • 动作 :OS 修改了 Baby 进程内部的映射表。

    • 从此,Baby 进程里任何试图读取 FD 0 (stdin) 的操作,物理上 都被内核 redirect 到了 Pipe_B 的写端(逻辑上是读端,这里指数据流向的反向,修正:应该是 dup2(Read_B, 0) 让 Baby 读,dup2(Write_A, 1) 让 Baby 写。抱歉,刚才为了简化逻辑可能有点绕,我们修正一下标准流程):

    修正后的标准绑定流程(绝对准确版):

    1. Client 创建 Pipe_In (Client写->Server读) 和 Pipe_Out (Server写->Client读)。
    2. fork() -> Baby 继承了 Pipe_In 的两端 和 Pipe_Out 的两端。
    3. 在 Baby 进程中执行:
      • dup2(Pipe_In_Read_End, 0) --> 绑定成功! Baby 的 stdin 现在死死连着 Pipe_In 的读端。
      • dup2(Pipe_Out_Write_End, 1) --> 绑定成功! Baby 的 stdout 现在死死连着 Pipe_Out 的写端。
    4. 关闭多余端 :Baby 关闭它不需要的 Pipe_In_Write_EndPipe_Out_Read_End
  • 此时的状态

    • Client 手里留着:Pipe_In_Write_EndPipe_Out_Read_End
    • Baby (未来的 Server) 手里留着:FD 0 (连着 Pipe_In_Read) 和 FD 1 (连着 Pipe_Out_Write)。
    • 绑定完成 :这是一条闭环。Client 的写端 -> 管道 -> Baby 的读端。
身份的确认:exec() (换皮不换芯)

最后,Baby 执行 exec("java", "Server.jar")

  • 关键点exec 会替换代码段、堆栈、变量,但绝不会修改已经打开的文件描述符表 (除非设置了 FD_CLOEXEC 标志,但 Java 默认不设置)。
  • 结果 :新的 Server 进程醒来,它的 FD 0 依然指向刚才绑定的那个管道。
  • 最终绑定:Client (PID 100) <--> 管道 <--> Server (PID 101)。
真正的绑定者:pipe() 系统调用

fork 之前,Client 进程调用了 pipe(int fd[2])
就是这个函数,完成了 Client 和 Server 未来的连接绑定。

pipe() 到底做了什么?

当 Client 调用 pipe(fd) 时,操作系统内核做了三件事:

  1. 创建一个内核对象

    内核在内存里分配了一个结构体(我们叫它 Pipe_Struct)。这个结构体里有一个缓冲区(Buffer),用来暂存数据。

    • 这就是未来的"通道"。
  2. 生成两个"钥匙"(文件描述符)

    内核创建了两个特殊的文件描述符,它们都指向同一个 Pipe_Struct,但权限不同:

    • fd[0] (读端):标记为 READ-ONLY 。指向 Pipe_Struct 的读接口。
    • fd[1] (写端):标记为 WRITE-ONLY 。指向 Pipe_Struct 的写接口。
  3. 交付给调用者(Client)

    内核把这两个句柄(fd[0]fd[1])直接放进了 当前进程(Client) 的文件描述符表中。

🔥 关键点来了:

就在 pipe() 返回的那一微秒,连接已经建立了!

  • fd[0]fd[1] 虽然是两个不同的数字,但它们在内核底层指向同一个缓冲区对象
  • 任何往 fd[1] 写的数据,都会立刻进入那个缓冲区。
  • 任何从 fd[0] 读的数据,都是从那个缓冲区里拿的。

这就是"连接"的本质:它们共享同一个内核缓冲区对象。


分发给 Server 的过程:继承即连接

现在,Client 手里拿着这对"连体婴儿"(fd[0]fd[1])。接下来怎么给 Server?

步骤 A:fork() (复制钥匙)

Client 调用 fork()

  • 内核创建子进程(未来的 Server)。
  • 内核复制了 Client 的文件描述符表给子进程。
  • 结果
    • Client 有:fd[0] (读), fd[1] (写)。
    • Server (子进程) 也有:fd[0] (读), fd[1] (写)。
    • 注意 :它们指向的是同一个 内核 Pipe_Struct
    • 此时连接状态 :Client 和 Server 都同时拥有读写两端。这还没完成"单向通道"的构建,但物理连接已经存在
步骤 B:dup2() + close() (剪断多余的线,确立方向)

为了形成 Client 写 -> Server 读 的单向通道,必须扔掉不需要的端。

  • 在 Server 进程中

    • 执行 dup2(fd[1], 0):把写端映射到 stdin。(修正:通常是把读端映射到 stdin,即 dup2(fd[0], 0),让 Server 能读 )。
      • 假设我们要建立 Client 写 -> Server 读 的通道(Pipe_B):
      • Server 需要 。所以 Server 执行 dup2(fd[0], 0)
    • 执行 close(fd[1])关键! Server 关闭了写端。
    • 现状 :Server 手里只剩fd[0] (读端)。
  • 在 Client 进程中

    • Client 保留 fd[1] (写端)。
    • Client 执行 close(fd[0])关键! Client 关闭了读端。
    • 现状 :Client 手里只剩fd[1] (写端)。
🎯 最终的连接图景

经过这一番操作:

  1. 内核中 :只有一个 Pipe_Struct (缓冲区)。
  2. Client 端 :持有唯一的 写钥匙 (fd[1])。
  3. Server 端 :持有唯一的 读钥匙 (fd[0])。

这就是连接!

  • 当 Client 用它的钥匙写数据 -> 数据进入 Pipe_Struct
  • 当 Server 用它的钥匙读数据 -> 数据从 Pipe_Struct 取出。
  • 因为世界上只有这两把钥匙能操作这个缓冲区,所以它们天然就是连在一起的。

Q: 怎么知道 Server 的 write_B (其实是读端) 和 Client 的 read_B (其实是写端) 是连着的?

A: 因为它们是在 pipe() 创建时,由内核强行"焊"在同一个对象上的。

  • 不是 Server 去连接 Client。
  • 不是 Client 去连接 Server。
  • 而是 Client 先召唤出一个"管道对象"(通过 pipe()),这个对象天生就有两头。
  • Client 把其中一头(读端)通过 fork 遗传给了 Server。
  • Client 自己留下了另一头(写端)。
  • 然后双方各自关掉对方那一头,只留自己需要的。

比喻:

想象一根水管。

  1. pipe() :工厂生产了一根水管,两头分别是 A 口和 B 口。A 和 B 天生就是通的。
  2. fork():工厂老板(Client)克隆了一个分身(Server)。老板手里拿着 A 和 B,分身手里也拿着 A 和 B(复制品,但通向同一根管子)。
  3. close()
    • 老板把 B 口封死,只留 A 口用来灌水。
    • 分身把 A 口封死,只留 B 口用来接水。
  4. 结果:老板灌的水,只能流向分身的 B 口。

为什么不会连到 Client 2?

因为 Client 2 调用 pipe() 时,工厂生产的是另一根完全不同的水管 (内存地址不同)。

Client 1 的分身(Server 1)继承的是 水管 1 的口。

Client 2 的分身(Server 2)继承的是 水管 2 的口。
水管 1 和水管 2 在物理上(内核内存中)是完全隔离的,互不相通。

相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio