浅谈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 在物理上(内核内存中)是完全隔离的,互不相通。

相关推荐
2345VOR2 小时前
【QT的pyside6开发使用】
开发语言·qt
Ronin3052 小时前
【Qt常用控件】控件概述和QWidget 核心属性
开发语言·qt·常用控件·qwidget核心属性
故事和你912 小时前
sdut-程序设计基础Ⅰ-实验二选择结构(1-8)
大数据·开发语言·数据结构·c++·算法·优化·编译原理
温柔一只鬼.2 小时前
GUI学习——day2
java·开发语言·学习
yongui478342 小时前
离散偶极子近似(DDA)求解颗粒散射的MATLAB实现
开发语言·matlab
花哥码天下3 小时前
安装/卸载claude code和codex
开发语言·javascript·ecmascript
AsDuang3 小时前
Python 3.12 MagicMethods - 28 - __rsub__
开发语言·python
饕餮争锋3 小时前
Java泛型介绍
java·开发语言
飞Link3 小时前
告别复杂调参:Prophet 加法模型深度解析与实战
开发语言·python·数据挖掘