跳到主要内容

Jupyter 通讯协议

环境设置

如果我想研究 jupyter 的服务端和客户端交互,应该怎么设置环境? 特别地,我想把 zmq 的消息打印出来供我研究 并且,我想用 nix (flake) 使得这个环境可以简单复现 用中文回复

好的,这是一个使用 Nix Flake 配置 Jupyter 环境以观察 ZMQ 消息的方案。

  1. 创建 flake.nix 文件:

    {
    description = "Jupyter ZMQ inspection environment";

    inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # 或者指定一个稳定版本
    };

    outputs = { self, nixpkgs }:
    let
    # 支持的系统
    supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
    # 为每个支持的系统生成 pkgs
    pkgsFor = system: import nixpkgs { inherit system; };
    in
    {
    # 为每个系统提供开发环境
    devShells = nixpkgs.lib.genAttrs supportedSystems (system:
    let
    pkgs = pkgsFor system;
    # 定义包含 jupyter 组件的 Python 环境
    pythonEnv = pkgs.python3.withPackages (ps: with ps; [
    ipykernel # Jupyter 内核
    jupyter-client # Jupyter 客户端库
    jupyter-console # 简单的文本客户端,便于观察
    pyzmq # ZeroMQ 的 Python 绑定
    # 如果需要 Web 界面,可以取消注释 notebook 或 jupyterlab
    # notebook
    # jupyterlab
    ]);
    in
    pkgs.mkShell {
    name = "jupyter-zmq-dev";
    # 将 Python 环境加入开发环境
    packages = [
    pythonEnv
    ];
    # (可选) 进入环境时的提示信息
    shellHook = ''
    echo "已进入 Jupyter ZMQ 调试环境。"
    echo "要查看 ZMQ 消息,请使用 '--debug' 标志启动 jupyter 组件。"
    echo "例如,启动一个控制台客户端:"
    echo " jupyter console --debug"
    echo "注意观察输出中包含 'ZMQ' 或 'KernelClient' 相关的调试信息。"
    '';
    }
    );
    };
    }
  2. 使用方法:

    • 将上述内容保存为 flake.nix 文件。
    • 在包含 flake.nix 的目录中,运行 nix develop 命令进入该环境。
    • 进入环境后,根据 shellHook 的提示操作。
  3. 观察 ZMQ 消息:

    • 在 Nix 环境中,运行 jupyter console --debug
    • 执行一些简单的 Python 代码(例如 a = 1, print(a))。
    • 在终端的输出中,查找包含 DEBUG 级别的日志,特别是那些与 KernelClientZMQChannel (如 shell, iopub, control, stdin, hb) 相关的日志。这些日志会显示消息的发送和接收情况。

    示例输出片段(可能包含类似内容):

    [D 2023-10-27 10:30:00.123 KernelClient] Connecting to: tcp://127.0.0.1:54321
    [D 2023-10-27 10:30:00.456 ZMQChannelsHandler] Received message on shell channel: ...
    [D 2023-10-27 10:30:00.789 KernelClient] Sending message on shell channel: ...

这个 Flake 配置提供了一个包含必要 Jupyter 组件(内核、客户端库、控制台)和 ZMQ 绑定的隔离环境。通过 --debug 标志运行 jupyter console,你可以直接在终端看到客户端和内核之间通过 ZMQ 交换消息的详细日志记录。

有错误

❯ nix develop

error: flake 'path:/home/ayanamists/repo/handscript/jupyter_debug' does not provide attribute 'devShells.x86_64-linux.default', 'devShell.x86_64-linux', 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux' Did you mean devShells?

你说得对,这个错误是因为 nix develop 默认查找名为 default 的开发环境,而之前的 flake 文件直接在系统名称下定义了 shell,没有 default 这一层。

请修改 flake.nix 文件,将 mkShell 的结果赋给 default 属性:

{
description = "Jupyter ZMQ inspection environment";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};

outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
pkgsFor = system: import nixpkgs { inherit system; };
in
{
# 修改这里:为每个系统定义一个名为 default 的 shell
devShells = nixpkgs.lib.genAttrs supportedSystems (system:
let
pkgs = pkgsFor system;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
ipykernel
jupyter-client
jupyter-console
pyzmq
# notebook
# jupyterlab
]);
in
{ # 添加这一层
default = pkgs.mkShell { # 将 mkShell 放在 default 属性下
name = "jupyter-zmq-dev";
packages = [
pythonEnv
];
shellHook = ''
echo "已进入 Jupyter ZMQ 调试环境。"
echo "要查看 ZMQ 消息,请使用 '--debug' 标志启动 jupyter 组件。"
echo "例如,启动一个控制台客户端:"
echo " jupyter console --debug"
echo "注意观察输出中包含 'ZMQ' 或 'KernelClient' 相关的调试信息。"
'';
};
} # 添加这一层
);
};
}

改动说明:

outputs.devShells 的定义中,对于 genAttrs 生成的每个系统属性,我们现在返回一个包含 default 属性的 Attribute Set,并将原来的 pkgs.mkShell { ... } 作为 default 的值。

这样修改后,再运行 nix develop 命令应该就能成功进入环境了。

对,你说得对。 nix develop 默认会尝试加载名为 default 的开发环境。你在 flake.nix 中创建的开发环境名为 jupyter-zmq-dev

你需要指定要加载的开发环境名称:

nix develop .#jupyter-zmq-dev

或者,如果你想让 nix develop 直接工作,可以将 flake.nix 中的 devShells 部分修改为:

          # ... inside outputs ...
devShells = nixpkgs.lib.genAttrs supportedSystems (system:
let
pkgs = pkgsFor system;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
ipykernel
jupyter-client
jupyter-console
pyzmq
# notebook
# jupyterlab
]);
in
{ # <-- 添加花括号
# 将 mkShell 的结果赋给 default 属性
default = pkgs.mkShell {
# name = "jupyter-zmq-dev"; # name 不再是必须的,但可以保留
packages = [
pythonEnv
];
shellHook = ''
echo "已进入 Jupyter ZMQ 调试环境 (default)。"
# ... (其余 shellHook 不变) ...
'';
};
} # <-- 添加花括号
);
# ...

修改后,devShells.<system> 的值会是一个包含 default 属性的属性集。这样,直接运行 nix develop 就可以进入环境了。

选择哪种方式取决于你的偏好:

  1. 指定名称 (nix develop .#jupyter-zmq-dev):更明确,特别是当你的 Flake 提供多个开发环境时。
  2. 使用 default:方便,可以直接运行 nix develop

过程

// User -> Kernel
�������8�������=�����������������������e�������������shell{"date":"2025-04-29T06:46:43.582Z","msg_id":"cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe","msg_type":"execute_request","session":"02991bb7-a5cd-446c-b325-4880bbd5f229","username":"","version":"5.2"}{}{"trusted":true,"deletedCells":[],"recordTiming":false,"cellId":"d5310080-727d-4769-adb5-ec201ef96d65"}{"silent":false,"store_history":true,"user_expressions":{},"allow_stdin":true,"stop_on_error":true,"code":"2 + 3"}

�������8�������=����������������������������������iopub{"msg_id": "8218c830-a736e5ef247730fd4c0f4b41_665704_111", "msg_type": "status", "username": "ayanamists", "session": "8218c830-a736e5ef247730fd4c0f4b41", "date": "2025-04-29T06:46:43.583473Z", "version": "5.3"}{"date": "2025-04-29T06:46:43.582000Z", "msg_id": "cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe", "msg_type": "execute_request", "session": "02991bb7-a5cd-446c-b325-4880bbd5f229", "username": "", "version": "5.2"}{}{"execution_state": "busy"}

�������8�������=���������������������������
������iopub{"msg_id": "8218c830-a736e5ef247730fd4c0f4b41_665704_112", "msg_type": "execute_input", "username": "ayanamists", "session": "8218c830-a736e5ef247730fd4c0f4b41", "date": "2025-04-29T06:46:43.583721Z", "version": "5.3"}{"date": "2025-04-29T06:46:43.582000Z", "msg_id": "cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe", "msg_type": "execute_request", "session": "02991bb7-a5cd-446c-b325-4880bbd5f229", "username": "", "version": "5.2"}{}{"code": "2 + 3", "execution_count": 4}

�������8�������=���������������������������*������iopub{"msg_id": "8218c830-a736e5ef247730fd4c0f4b41_665704_113", "msg_type": "execute_result", "username": "ayanamists", "session": "8218c830-a736e5ef247730fd4c0f4b41", "date": "2025-04-29T06:46:43.584374Z", "version": "5.3"}{"date": "2025-04-29T06:46:43.582000Z", "msg_id": "cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe", "msg_type": "execute_request", "session": "02991bb7-a5cd-446c-b325-4880bbd5f229", "username": "", "version": "5.2"}{}{"data": {"text/plain": "5"}, "metadata": {}, "execution_count": 4}

�������8�������=��������������������j�������������shell{"msg_id": "8218c830-a736e5ef247730fd4c0f4b41_665704_114", "msg_type": "execute_reply", "username": "ayanamists", "session": "8218c830-a736e5ef247730fd4c0f4b41", "date": "2025-04-29T06:46:43.587893Z", "version": "5.3"}{"date": "2025-04-29T06:46:43.582000Z", "msg_id": "cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe", "msg_type": "execute_request", "session": "02991bb7-a5cd-446c-b325-4880bbd5f229", "username": "", "version": "5.2"}{"started": "2025-04-29T06:46:43.583706Z", "dependencies_met": true, "engine": "b4934063-66a1-476b-8ccb-108e9bdcb03c", "status": "ok"}{"status": "ok", "execution_count": 4, "user_expressions": {}, "payload": []}

�������8�������=����������������������������������iopub{"msg_id": "8218c830-a736e5ef247730fd4c0f4b41_665704_115", "msg_type": "status", "username": "ayanamists", "session": "8218c830-a736e5ef247730fd4c0f4b41", "date": "2025-04-29T06:46:43.588689Z", "version": "5.3"}{"date": "2025-04-29T06:46:43.582000Z", "msg_id": "cbe66321-a40d-4a5b-b2f8-a3a4e2c41dbe", "msg_type": "execute_request", "session": "02991bb7-a5cd-446c-b325-4880bbd5f229", "username": "", "version": "5.2"}{}{"execution_state": "idle"}

分析一下这次的求值过程

Okay, here's an analysis of the Jupyter kernel communication for evaluating 2 + 3:

  1. Client -> Kernel (shell): An execute_request is sent containing the code "2 + 3".
  2. Kernel -> All (iopub): Kernel broadcasts its status as "busy".
  3. Kernel -> All (iopub): Kernel broadcasts the input code "2 + 3" and execution_count: 4 via execute_input.
  4. Kernel -> All (iopub): Kernel broadcasts the execution result {"text/plain": "5"} via execute_result.
  5. Kernel -> Client (shell): Kernel sends an execute_reply indicating successful execution ("status": "ok").
  6. Kernel -> All (iopub): Kernel broadcasts its status as "idle", ready for new commands.