我对开发环境的基本要求之一是可以下断点单步调试。这很基础,却很重要。
代码自动补全和断点调试功能二选一?
我选断点。
点击函数或变量自动跳转到声明处和断点调试功能二选一?
我选断点。
静态类型推导,编译时类型检查和断点调试功能二选一?
嗯,是个难题。
这样讲应该足以解释断点调试能力在我心中的地位了吧。
为什么突然想起说这件事呢,是因为昨天晚上我为了给我的 php 开发环境安装 xdebug 折腾了一宿,有感而发了。
按说给 php 安 xdebug 能有多难呢?确实不难,我在公司和个人环境里也安过很多次了,基本没遇到过太大困难。可昨天的情况比较特殊,甚至堪称极端,真的是把一个简单的问题拔高到了面试题的水平。
事情的起因是,我想在元旦假期里学习一下 laravel。最新的 laravel 框架要求 php 版本在 7.2 以上,外加一些其他的依赖项。我这个博客是运行在一个 web hosting 服务器上的,想动服务器配置并不容易。我常用的 vps 是朋友买的,我本身是借宿在那里,也不想太破坏他的环境。最终我选择在 vps 上用 docker 实现一个满足 laravel 要求的开发环境,而这正是噩梦的开始。
在 docker 里跑 php,这个 php container 暴露给外界访问的端口就不能是 80 了。假设暴露给外界访问的端口是 8080,我访问这个 docker 内的 php 文件的方式就是 http://my.vps:8080/path/to/file ,看起来有点丑不是么。为了美化这个 url,我修改了 vps 本机的 apache 设置,给这个 8080 端口配置了一个转发用的子目录 docker,这样我可以通过 http://my.vps/docker/path/to/file 的 url 来访问 docker。怎么样,好看多了吧,哈哈!
到这一步做完,我才开始配置 xdebug,而这时,我的 IDE 和目标 php 服务之间已经隔了两层了。
冷静!仔细想想,就算不给 apache 配置 ProxyPass,它到 php 之间也是会隔一层的呀,所以这不算是增加难度。呃,但是算上 ProxyPassReverse 设置,我的 http header 在中间被改来改去的,还是增加了不稳定因素。反正先试试再说吧。
一试果然不行,连不通。
那就不着急了,先从头捋一遍通信流程吧。附上我最喜欢的 xdebug 通信流程图。
看样子浏览器到 php/Xdebug 这部分通信时没问题的,问题出在 xdebug 想回拨给我的时候。
查了一下,我用的 PhpStorm 的文档里有远程调试 php 时的指导方案:
ssh -R 9000:localhost:9000 username@hostname
思路是让 xdebug 回拨的时候访问服务器端的 9000 端口,同时把服务器端的 9000 端口反向代理到我的 PhpStorm 所在本地主机的 9000 端口,这样服务器端 xdebug 的回拨可以最终被在本地监听 9000 端口的 PhpStorm 接收到,很合理。
不过我的环境里还涉及到了 docker,怎么让 docker 容器内的 xdebug 的连接请求转发到外部宿主上呢?因为我是用 docker-compose 创建的 docker,所以我首先尝试用 docker-compose 自带的 container 到主机的端口映射方法。
ports: - 9000:9000
结果在 docker 启动时 ssh 的反向代理无法绑定到服务器端的 9000 端口,这招不行。为了确认原因,我在 docker 容器内和宿主服务器上分别用 tcpdump 测试 9000 端口上的通信内容。结果显示 docker 容器内的 9000 端口上确实有通信,但宿主服务器的 9000 端口上没有任何通信。看来 docker-compose 的 ports 是专门用来让宿主访问容器使用的,反过来行不通。
sudo tcpdump -i any port 9000
我知道 mac 版的 docker 容器访问宿主 ip 有一个 docker.for.mac.localhost 的 hostname ,但是在 linux 环境里对应的 host.docker.internal 不知道为什么没有被定义。
算了暴力点直接拿 ip 吧!我在 docker 里做了一个显示请求方 ip 的 php 页面,类似于下面这样:
<?php echo $_SERVER[REMOTE_ADDR];
取到的 ip 是 172.19.0.1 。之后反复重启 docker 试了几次,ip 没变,应该是问题不大了,就它吧。
用这个配置测了一下,结果 PhpStorem 还是连不上 xdebug。。什么情况!
用 tcpdump 看了一下远端宿主服务器的 9000 端口已经有流量了,但本地用 wireshark 却看不到本机 9000 端口上的流量。PhpStorm 行不行啊,有没有在监听 9000 哦?用 360 流量防火墙看了一下,确实启用 PhpStorm 的监听模式后程序会开始监听 9000 端口,关闭监听模式后程序对 9000 端口的监听也会停止。
会不会是 PhpStorm 没听懂呢?下一个 xdebug 的命令行客户端试了试,没反应。行不行啊?又下了个 linux 版的在服务器上执行 ./dbgpClient -p 9000 ,能收到 xdebug 的回拨,进入调试状态。看来问题出在服务器的通信没有转发给我的本地主机。
为什么呢?没头绪啊。。敲个 sudo netstat -natpl | grep 9000 研究了一下,看到 127.0.0.1:9000 这里突然想起来,ssh 的反向代理默认只接受来自本地的通信,docker-compose 的容器默认是创建一个独立的内网 ip 用来通信的,反正不是 127.0.0.1 。会不会因为这个,所以 ssh 的反向代理没有受理来自 docker 的通信?
google 了一下,修改 ssh 的设置为 GatewayPorts clientspecified 可以让客户端决定服务器端绑定的端口可以接收来自哪些 ip 的通信。于是打开 ssh 配置文件,发现没有 GatewayPorts 的设置,那更好,直接添加在文件末尾,然后重启 ssh 服务。
sudo vi /etc/ssh/sshd_config sudo systemctl restart sshd.service
之后在本地主机上重新执行 ssh 反向代理,这次要设置为允许接收来自外部 ip 的通信:
ssh -R 0.0.0.0:9000:localhost:9000 username@hostname
这次,终于调通了。
下面是技术总结(太累了就贴张图吧 orz):
补充:
1. 为什么我在本地的 window 主机上可以使用 ssh 命令?
因为用的 Cygwin
2. 对所有 ip 开放端口访问会不会有安全隐患?
确实。应该可以把最后的 0.0.0.0 改成 docker 容器的 ip,如果他有固定 ip 的话。不过我只会在自己需要调试 php 的时候才会创建这个反向代理,我没在用的时候这个安全隐患不存在,我在用的时候有其他人入侵这个端口应该相对比较容易察觉到。这里确实是防范意识不足,偷懒了。之后会采用更安全的连接方式的。
3. apache 的端口代理到子目录会影响 xdebug 调式吗?
不会。无论访问子目录还是直接访问端口,设置在 PhpStorm 里的断点都可以正确生效。