Bokeh 服务端核心组件详解:bokeh.server.server 中 BaseServer、Server 与 bind_sockets 的完整指南
【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh
本文围绕 Bokeh API 参考文档 server.rst 展开,系统讲解bokeh.server.server模块提供的两个公开类BaseServer、Server以及辅助函数bind_sockets。读完后,你将掌握如何在 Python 中以编程方式(而非bokeh serve命令行)启动、配置和关停一个 Bokeh 服务器:包括监听地址/端口/Unix socket、多进程 worker、SSL 终结、WebSocket 来源白名单、会话(Session)查询,以及如何通过Server.from_settings让程序化创建的服务端遵循与bokeh serve相同的环境变量约定。
模块定位:bokeh.server.server 是什么
模块源码 的文档字符串说明了该模块的职责:提供基于 TornadoHTTPServer与BokehTornado应用的服务端对象,用于承载 Bokeh Server 应用(Server Application)。模块内公开的对象在__all__中被明确限定为三个(server.py#L84-L88):
BaseServer:轻量级协调类。它不替你创建任何组件,而是要求你显式提供并协调好运行 Bokeh 服务器所需的三件套:一个 TornadoIOLoop、一个BokehTornado应用实例、一个已经绑定到该应用的 TornadoHTTPServer。适合你已经在自行管理事件循环、需要精细控制进程/循环结构的场景。Server:高层便捷类。只需要传入 Bokeh 的Application实例(或应用路径到应用的映射、甚至一个可调用对象),它会自动创建并协调上述底层 Tornado 组件。它还提供了工厂方法Server.from_settings,可从 Bokeh 全局配置系统(bokeh.settings.settings)自动读取认证、SSL、会话签名、Cookie 等配置,与bokeh serve命令的环境变量约定保持一致。bind_sockets:开发辅助函数,将 socket 绑定到指定端口,也兼容操作系统自动分配端口的情况(见下文)。
模块文档同时指向用户指南中的ug_server_introduction章节获取 Bokeh server 的整体背景信息。需要说明的边界是:Server这个高层类存在明确限制——不能同时设置显式的io_loop和num_procs > 1,若要在这两个维度上同时自由,必须改用BaseServer手动协调三个组件。
Server:一次讲全构造参数与全部可配置项
Server的构造函数签名为(server.py#L420-L421):
def __init__(self, applications, io_loop=None, http_server_kwargs=None, **kwargs)applications:三种传入形态
applications参数接受三种形态,这是编写多应用服务器的关键:
- URL 路径到
Application的映射(dict[str, Application]):每个应用由一个对应 URL 的路径标识,如"/"或"/myapp"; - 单个
Application:会被自动映射到根路径"/"; - 一个可调用对象:作为便捷方式,会用
FunctionHandler为它创建一个Application。
Application本质上是 Document 的工厂:每个 Session 会初始化一个全新的 Document。单元测试 test_server__server.py#L285-L297 中的test_server_applications_callable_arg直接验证了可调用对象形态:把modify_doc传给服务器后请求url,取回会话并断言session.document.title == "Hello, world!",并验证了{"/foo": modify_doc}映射形态下/foo路径的行为一致。
http_server_kwargs 与 io_loop
io_loop(可选):显式指定运行 Bokeh Server 代码的 TornadoIOLoop;为None时使用IOLoop.current()。源码中有一个值得注意的实现细节:IOLoop 只能在HTTPServer.start()调用之后再引用(代码注释指向 issue #5524,见 server.py#L530-L532)。http_server_kwargs(可选):原样传递给tornado.httpserver.HTTPServer的额外参数,例如max_buffer_size用于限制最大上传体积。
_ServerOpts:全部服务端选项一览
其余关键字参数由私有的_ServerOpts(Options)类解析(server.py#L668),API 文档中的选项即来源于此。以下按源码逐项继承其说明,并补充默认值与实现事实:
| 参数 | 类型/默认值 | 说明(继承自源码 help 文本) |
|---|---|---|
num_procs | int,默认1 | HTTP 服务器启动的 worker 进程数。若同时配置了显式io_loop,则只能取 1;取 0 表示自动检测 CPU 核数。注意受 Tornado 限制,Windows 不支持num_procs > 1——官方建议在这种情况下运行多个 Bokeh server 实例放在负载均衡器之后。 |
address | str \| None | 服务器监听 HTTP 请求的地址。 |
port | int,默认DEFAULT_SERVER_PORT | 监听端口。该默认值来自全局配置:resources.py#L75 中DEFAULT_SERVER_PORT = settings.default_server_port(),而 settings.py#L817 显示其底层默认值为5006,且支持环境变量BOKEH_DEFAULT_SERVER_PORT覆盖。 |
unix_socket | str \| None | 要绑定的 Unix socket。与port、address、SSL 选项等其他网络参数不兼容,且 Windows 上不可用。 |
prefix | str,默认"" | 用于所有 Bokeh server 路径的 URL 前缀。测试test_prefix(test_server__server.py#L152-L158)验证了传prefix="foo"后server.prefix == "/foo"(注意自动补了前导斜杠)。 |
index | str \| None | 用于索引页/的 Jinja2 模板路径。 |
allow_websocket_origin | list[str] \| None | 允许连接 WebSocket 的主机列表。当使用bokeh.embed.server_document等机制把 Bokeh 应用嵌入外部网站时通常需要设置;为None时默认使用"localhost"。 |
use_xheaders | bool,默认False | 是否让 Bokeh server 用X-Real-Ip、X-Forwarded-For、X-Scheme、X-Forwarded-Proto请求头(若提供)覆盖所有请求的远端 IP 与 URI scheme/协议。这是放在反向代理后面部署的关键开关。 |
ssl_certfile | str \| None | SSL 终结所用的证书文件路径。 |
ssl_keyfile | str \| None | SSL 终结所用的私钥文件路径。 |
ssl_password | str \| None | 必要时用于解密 SSL keyfile 的密码。 |
websocket_max_message_size | int,默认DEFAULT_WEBSOCKET_MAX_MESSAGE_SIZE_BYTES | 设置 Tornado 的websocket_max_message_size值,该常量定义于 tornado.py。 |
构造期的参数校验(server.py#L466-L477)值得单独强调,因为它们直接抛RuntimeError:
num_procs > 1且显式传了io_loop:报错提示改用BaseServer协调显式 IOLoop 与多进程 HTTPServer;num_procs > 1且平台为 Windows:直接拒绝;unix_socket非空且平台为 Windows:直接拒绝。
此外还有两条从源码结构看值得了解的隐含约束:使用unix_socket时_address/_port会被置为None(server.py#L490-L493);而num_procs != 1时,源码会断言所有已注册应用都满足application_context.application.safe_to_fork,即用户应用代码不能在启动多进程之前执行过,否则视为不安全操作(server.py#L515-L518)。
一个可运行的完整示例
综合上述参数,一个典型的程序化服务器如下:
from bokeh.application import Application from bokeh.server.server import Server def modify_doc(doc): doc.title = "Hello, Bokeh Server" # 单应用 -> 自动映射到 "/"; 也可用 {"path": Application(...}} 多应用 app = Application(modify_doc) server = Server( app, address="0.0.0.0", port=5006, # 默认即 DEFAULT_SERVER_PORT (5006) allow_websocket_origin=["localhost:8000", "my-website.com"], use_xheaders=True, # 放在 Nginx 等反向代理后时开启 # ssl_certfile="cert.pem", # ssl_keyfile="key.pem", # num_procs=2, ) server.show("/") # 仅本地测试时使用,生产部署不应调用 server.run_until_shutdown()Server.from_settings:与 bokeh serve 环境变量约定对齐的工厂方法
Server.from_settings(server.py#L536-L638)是类方法,作用为:未显式以关键字参数传入的配置,自动从全局settings模块补齐,从而使程序化创建的服务端与bokeh serve命令遵循相同的环境变量约定。其参数与默认来源如下:
| 参数 | 未传入时的默认来源 |
|---|---|
applications/io_loop/http_server_kwargs | 同Server.__init__ |
auth_provider | settings.auth_module()指向的路径加载为AuthModule;未配置路径时使用NullAuth |
secret_key | settings.secret_key_bytes() |
sign_sessions | settings.sign_sessions() |
ssl_certfile/ssl_keyfile/ssl_password | settings.ssl_certfile()/settings.ssl_keyfile()/settings.ssl_password() |
cookie_secret | settings.cookie_secret() |
xsrf_cookies | settings.xsrf_cookies() |
ico_path | settings.ico_path()(自定义 favicon.ico文件路径) |
其余关键字参数原样转发给Server。该方法会抛ValueError的情况是:sign_sessions=True但拿不到secret_key(server.py#L623-L624)。
单元测试对该方法做了两条直接验证(test_server__server.py#L361-L405):
test__from_settings_uses_envvars:mock 掉settings后断言Server.from_settings(Application())确实把auth_provider(AuthModule实例)、sign_sessions、secret_key、SSL 三件套、cookie_secret、xsrf_cookies全部透传进了Server.__init__;test__from_settings_kwarg_overrides_envvar:显式传入的auth_provider=null_auth会覆盖环境变量级别的设置。
即优先级明确:显式关键字参数 > settings/环境变量。
BaseServer:手动协调 IOLoop、BokehTornado 与 HTTPServer
当你需要把 Bokeh 服务器嵌入自己已有的事件循环(例如与其他 asyncio 服务共存、或在测试中精确控制循环生命周期)时,使用BaseServer。其构造函数(server.py#L123)要求三个参数:
io_loop:运行 Bokeh Tornado 应用的 TornadoIOLoop;tornado_app:BokehTornado实例,即生成 Bokeh Document 与 Session 的服务端机制本体;http_server:处理 HTTP 请求的 TornadoHTTPServer,必须已经在使用tornado_app创建时就配置好。
BaseServer的构造过程只做一件事:在这三个对象上调用self._tornado.initialize(io_loop)把BokehTornado初始化到给定循环上。最小用法(与 test_server__server.py#L267-L283 的test_base_server完全一致):
from bokeh.application import Application from bokeh.server.server import BaseServer from bokeh.server.tornado import BokehTornado from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop loop = IOLoop() loop.make_current() app = BokehTornado(Application()) httpserver = HTTPServer(app) httpserver.start() server = BaseServer(loop, app, httpserver) server.start() # ... 你的代码 ... httpserver.stop() server.stop() loop.close()生命周期方法逐个拆解
start():把 Bokeh Server 及其后台任务安装到IOLoop上。源码文档明确强调:此方法不阻塞、也不影响IOLoop的状态,循环的启停必须由你自己负责——它适用于你本就在显式管理IOLoop的情形。stop(wait=True):停止并移除所有 Bokeh Server 的IOLoop回调,并停止其配置的HTTPServer。wait控制是否等待有序清理。这里有一个从源码结构看很有意思的并发细节(server.py#L175-L214):同步调用无法阻塞服务器自身正在运行的事件循环,因此当检测到调用发生在服务器所运行循环的线程内时,清理会被调度为任务并立即返回,此时需要await wait_until_stopped()作为完成屏障;异步调用方则应直接使用stop_async()。stop_async():从异步代码中停止服务器并等待有序的应用清理。当你后续的工作依赖于所有会话与生命周期钩子都执行完毕时,应使用它而不是stop。与stop一样,只能调用一次。wait_until_stopped():等待由stop在"同一事件循环"内调度的清理任务完成,即上面提到的完成屏障。unlisten():停止监听端口,调用后服务器不再可用。文档注明该方法主要用于测试。run_until_shutdown():在Server层最常用的"启动并常驻"方式。它会:若尚未start()则先启动;通过atexit.register安装进程退出钩子;在非 Windows 平台上为事件循环注册SIGTERM信号处理器(_sigterm会打印Received signal SIGTERM, shutting down并让loop.start()返回);然后调用self._loop.start()进入阻塞。捕获KeyboardInterrupt(即 Ctrl-C)后打印Interrupted, shutting down并执行self.stop()。也就是说,它同时响应 Ctrl-C 与 SIGTERM 两种关停方式(server.py#L250-L275)。
会话与运行信息访问
BaseServer还提供了一组查询属性/方法,常用于运维脚本或测试:
get_session(app_path, session_id):按应用路径与会话 ID 取一个活动会话,返回ServerSession;get_sessions(app_path=None):取所有应用当前活动的会话;给定app_path时只取该应用的。测试test_get_sessions(test_server__server.py#L168-L212)验证了完整行为:每发起一次 HTTP 请求,对应路径的会话数加一;get_sessions()不带参数时汇总全部应用;对不存在的app_path则抛ValueError;show(app_path, browser=None, new="tab"):在浏览器窗口或标签页中打开应用。app_path必须以/开头(否则抛ValueError);new可选"tab"或"window";browser可指定如"firefox"等(语义同标准库webbrowser)。源码文档明确提醒:此方法适合本地测试,生产部署中不应调用;port/address属性:BaseServer的实现是从HTTPServer实际绑定的 socket 中读取真实监听值(server.py#L366-L384),而Server子类则直接返回构造期记录的_port/_address——在使用unix_socket时Server.port/address为None,并有专门的unix_socket属性。Server.port的返回值类型是int | None,这保证了bind_sockets在端口为 0(由 OS 自动分配)时仍能给出真实端口;prefix/index属性:透传BokehTornado上配置的 URL 前缀与索引模板路径。
bind_sockets:端口绑定的开发辅助函数
bind_sockets(address, port)(server.py#L94-L103)实现很短,但行为上有两个要点:
def bind_sockets(address: str | None, port: int) -> tuple[list[socket.socket], int]: '''Bind sockets to one port, including when the OS selects the port.''' sockets = netutil.bind_sockets(port=port or 0, address=address) ... return sockets, actual_port- 支持
port=0让操作系统自动选择端口:绑定后会从所有 socket 的getsockname()中反解出真实端口(actual_port),保证所有 socket 落在同一端口上(否则断言失败); - 显式端口时做一致性断言:如果调用方指定了端口,会断言实际端口必须与之一致。
Server.__init__在走 TCP/IP 路径时正是调用bind_sockets(opts.address, opts.port)完成绑定的(server.py#L499),这也是测试中常用port=0启动临时服务器而不产生端口冲突的原因(如test_server_applications_callable_arg)。
SSL 终结与 WebSocket 来源白名单的实现细节
SSL 终结:当ssl_certfile有值时,Server会在日志中记录Configuring for SSL termination,然后创建ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)并load_cert_chain(certfile=..., keyfile=..., password=...),把上下文放入http_server_kwargs['ssl_options'](server.py#L483-L488)。单元测试test_ssl_args_plumbing(test_server__server.py#L251-L265)用 mock 验证了 cert/key/password 三个参数确实被原样传给load_cert_chain。
WebSocket 来源白名单:allow_websocket_origin会经create_hosts_allowlist(定义于 util.py)转换为允许的主机列表并作为extra_websocket_origins传给BokehTornado;非 Unix socket 场景下白名单中会带上实际端口号(server.py#L494-L502)。这解释了为什么嵌入外部站点时必须显式列出页面所在域名。
X-Forwarded 处理:use_xheaders通过http_server_kwargs.setdefault('xheaders', ...)注入HTTPServer,测试test_use_xheaders断言server._http.xheaders is True(test_server__server.py#L246-L249)。
相关模块与延伸阅读
bokeh.server.server只是 Bokeh 服务端的入口层,从源码的导入与引用关系看,可以沿以下路径继续深入(均为仓库内相对路径):
- src/bokeh/server/tornado.py:
BokehTornado应用本体,负责路由、Document/Session 生成与websocket_max_message_size等机制; - src/bokeh/server/session.py:
ServerSession,get_session(s)的返回类型; - src/bokeh/server/auth_provider.py:
NullAuth、AuthModule、AuthProvider,from_settings认证参数的类型来源; - src/bokeh/command/subcommands/serve.py:
bokeh serve命令行子命令,其选项与_ServerOpts的对应关系; - 对应 API 参考页:server.rst 由 Sphinx
automodule指令渲染本模块全部成员文档,本文对其内容逐项继承并结合源码扩充;同目录还有 tornado.rst、session.rst 等参考页。
小结
bokeh.server.server用两个层次清晰的类覆盖了 Bokeh 服务端的两种使用姿势:Server面向绝大多数场景——传一个Application(或路径映射、可调用对象)加上一组_ServerOpts选项(端口默认 5006、num_procs多进程、SSL、WebSocket 白名单、URL 前缀等),必要时用from_settings对齐bokeh serve的环境变量约定,最后run_until_shutdown()常驻并以 Ctrl-C/SIGTERM 优雅退出;BaseServer则面向需要自管IOLoop的嵌入场景,配合start/stop/stop_async/wait_until_stopped/unlisten提供精确的生命周期控制。约束(Windows 不支持多进程、unix_socket与网络参数互斥、sign_sessions必须有secret_key)在源码中均以显式异常表达,测试套件 tests/unit/bokeh/server/test_server__server.py 对其中大部分行为提供了可直接复现的验证用例。
【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考