我有一个老旧的 Windows10 笔记本,它的性能不足以支持2025年的开发需要,但我偶尔需要在离开家里那台强力主机的情况下(比如在老家或办公室时)运行开发环境。于是我买了一台强力的无头主机(mechrevo imini pro, R78845H, 32G RAM)并安装了Debian(注意,安装包括Gnome图形界面,这可能无意间帮我节省了一些安装 X11 转发所必需的依赖库的步骤),用一根外置网卡(绿联USB3.0外置网卡2.5G网速 )把笔记本和无头主机连了起来,并建立了以笔记本为网关和DHCP服务器的局域网。关于如何折腾建立这网络,内容多到值得另写一篇博客,本文按下不表。这样一来,我便可以使用JetBrains IntelliJ中自带的JetBrains Gateway(通过 ssh)连接到无头主机(下称服务器)的 IntelliJ 后端,进行 Java 开发。
一切都很好,直到我编写 minecraft-access 这个 Minecraft mod。Minecraft 是个游戏,而且这个 mod 是客户端 mod,因此调试运行 mod 需要运行游戏,而这游戏需要……打开一个窗口。服务器没有外接显示器,如果直接通过 JB Gateway 在服务器中调试运行程序,程序会抛出以下异常并退出:
java.lang.IllegalStateException: Failed to initialize GLFW, errors: GLFW error during init: [0x1000E]Failed to detect any supported platform
at knot//com.mojang.blaze3d.platform.GLX._initGlfw(GLX.java:78)
at knot//com.mojang.blaze3d.systems.RenderSystem.initBackendSystem(RenderSystem.java:460)
at knot//net.minecraft.client.MinecraftClient.<init>(MinecraftClient.java:501)
at knot//net.minecraft.client.main.Main.main(Main.java:250)
GLFW 是一个 OpenGL 工具库,Minecarft 使用 LWJGL (Lightweight Java Game Library) 处理其界面、音效、输入,而这个库的底层应用正是 GLFW。搜索后得知,该报错是指 GLFW 找不到一个可匹配的窗口系统。
X11/X Window 正是一个窗口系统,它能解决我的问题。经过搜索(这篇文章写得不错)我了解到 X11 协议的术语含义与我们所熟知的概念是相反的:X client
是渲染界面且支持与 X server
通信的程序。在这个场景中,X client
需要运行在服务器里。X server
是运行在连接了输入输出设备的电脑上,并向 X client
转发输入数据、接收界面数据的程序。在这个场景中,它需要运行在我的老笔记本上。虽然界面的渲染由 X client
承担,但渲染好的界面数据仍需要 X server
将其展示在显示器上。
另外值得一提的是,X11 方案在设计之初没有充分考虑安全性,因此现有的安全方案便是通过万能的 ssh 转发 X11 流量。《UNIX and Linux System Administration Handbook, 4th Edition》书中提到:“通过 ssh 转发的 X11 界面并不流畅,即使在局域网中也是,如果存在网络延迟,情况会变得更糟”。它是对的,虽然我最终找到了成功方案,但游戏界面比较卡顿,我不得不缩小窗口来减少延迟,称得上“勉强能用”,组合按键和双击等复杂操作比较难触发。
#TLDR
考虑到最终效果并不好,我不建议你使用这套 X11 Forwarding 方案,按照这个讨论试试 VNC 或者老版本(未收费)的 Nomachine’s NX 吧。不过如果你坚持,这是所有步骤:
- 服务器
/etc/ssh/sshd_config
中配置X11Forwarding
的值为yes
,sudo systemctl restart ssh
重启sshd
进程。 - 笔记本用户
C:\Users\<user>\.ssh\config
(Windows)或~/.ssh/ssh_config
(Linux)中做以下配置并重启电脑或 ssh 服务。 - 笔记本安装 MobaXterm,打开 MobaXterm 并连接到服务器,保持这个连接,检查该连接的
DISPLAY
环境变量的值,不出意外的话为localhost:10.0
。 - 笔记本使用 JB Gateway 连接服务器上的 IntelliJ 后端,在项目的
Run/Debug Configuration
中增加环境变量DISPLAY=<MobaXterm 连接的 DISPLAY 值>
。 - 使用 IntelliJ 调试运行程序,界面应显示在笔记本侧。
|
|
#服务器端
在 /etc/ssh/sshd_config
中配置 X11Forwarding
的值为 yes
,同时保持 X11UseLocalhost
、X11DisplayOffset
为默认值,根据手册了解到,以下配置的作用是:
|
|
这些配置使得 sshd
进程会将通过 ssh 连接的远程 X server
的流量转发到 localhost:10.0
。在服务器上运行的程序,若环境变量配置为 DISPLAY=localhost:10.0
,则可以接收到 sshd
进程转发的 X server
的数据。关于什么是 DISPLAY
环境变量和 display number
,见 https://askubuntu.com/a/432257、https://linux.die.net/man/7/x。配置完记得 sudo systemctl restart ssh
重启一下 sshd
进程。
#笔记本端
Windows系统没有自带的 X server
,现有方案有 Xming
、VcXsrv
、MobaXterm
,我选择了 MobaXterm
。安装启动后,该程序将自动架设 X server
,地址为 <每个网络适配器的IP>:0.0
。
#cmd
先用 Windows cmd 试试,-v
打印 debug 日志,-X
允许经过鉴权的 X11 转发,-b
指定要连接的网卡:
|
|
失败了,发现这个远程 shell 没有设置 DISPLAY
环境变量,那我们手动设置一下:
|
|
还是不行,搜索后发现,是 X server
,即笔记本这边的 cmd 没有配置 DISPLAY
环境变量:
|
|
仍然不行,但是出现了 “debug1: No xauth program” 这行日志,说明我们至少向前一步了。搜索得知, xauth
程序需要存在于 X server
这一端, cmd 里没有也没办法自行安装该程序。还好 ssh
还提供了 -Y
参数,允许未经鉴权的 X11 转发(注意,这对笔记本侧来说有安全风险):
|
|
成功了!
#MobaXterm
在 MobaXterm 的本地终端上检查 DISPLAY
环境变量,发现其值已经设置到正确的值:
|
|
通过 MobaXterm ssh 连接到服务器,发现 DISPLAY
也已设置,xeyes
运行成功:
|
|
注意这里 shell 中的 DISPLAY
变量的 display number 被设置为11
,这是因为我的 cmd 终端还没退出 ssh,退出后再重新通过 MobaXterm 连接服务器,DISPLAY
会变回 localhost:10.0
,这就是 sshd_config
中 X11DisplayOffset
配置项的作用,偏移从10开始。不过 MobaXterm 的本地终端上并没有安装 xauth
(xauth: command not found),不清楚其内部是如何实现 X11 认证的。
#IntelliJ (over JB Gateway)
我找不到修改 JB Gateway 的 ssh 以传递 -X
-Y
参数的方法,还好可以通过修改 ssh 配置的方式达到相同目的,修改 C:\Users\<user>\.ssh\config
文件如下,并重启电脑(重启 ssh 服务也可以):
|
|
使用 JB Gateway 连接服务器上的 IntelliJ 后端,在项目的 Run/Debug Configuration
中增加环境变量 DISPLAY=localhost:10.0
(它将在服务器上生效),点击运行并忐忑等待——失败了,出现了和文章开头一样的 GLFW 错误。在 IntelliJ 内的终端(连接到服务器)配置 DISPLAY=localhost:10.0
并运行 xeyes
也失败了。
根据上面的尝试,我们可以判断大概是 IntelliJ 这边没有配置 DISPLAY
环境变量,来试试:
|
|
不行,相同的错误。经过尝试,我发现了一个不知道是否算作 workaround 的方法——借助已证明成功转发 X11 的 MobaXterm ssh 连接展示界面。用 MobaXterm 打开并保持到服务器的连接,将其 DISPLAY
环境变量的值(如localhost:10.0
)配置到 Run/Debug Configuration
上,调试运行……成功了!
就是这样,没什么要说的了,感谢阅读,希望这篇文章能帮到你。如果你知道更好的方案,请给我留言,如果我忍耐不了这方案的延迟,会尝试换方案。