从浏览器输入地址到看到页面的完整链路, 深入理解进程、端口、地址、转发和跨环境访问。面向新手, 由浅入深。
零、从一个具体场景开始
假设你正在开发一个 Laravel 项目, 终端里运行着:
# 终端 1
php artisan serve --port=8120
# 终端 2
npm run dev
然后打开浏览器, 输入 http://localhost:8120, 按下回车——页面出现了。
这一瞬间, 实际上发生了很多事情。 这篇文档的目标就是把这"一瞬间"拆开, 让你理解每一步的底层原理, 从而在以后遇到端口不通、样式丢失、WSL2 连不上等问题时, 能够自己判断问题出在哪一层。
让我们从头开始。
一、浏览器如何找到你的服务——URL 到连接的完整过程
1.1 你输入的 URL 是什么
浏览器地址栏里的 http://localhost:8120 可以拆成三部分:
http :// localhost :8120
协议 主机名 端口
- 协议 (http): 告诉浏览器用什么"语言"和服务器交流。http 是明文传输, https 是加密传输。
- 主机名 (localhost): 告诉浏览器"去哪台机器找服务"。
- 端口 (8120): 告诉浏览器"到了机器之后, 进哪个门找服务"。
补充知识: 默认端口。 如果 URL 里没写端口, 浏览器会用协议的默认端口:
http://默认 80,https://默认 443。也就是说http://localhost等价于http://localhost:80。
1.2 浏览器如何把 "localhost" 变成 IP 地址
浏览器不能直接用主机名通信——它需要 IP 地址。把主机名转成 IP 地址的过程叫 DNS 解析。
一般网站的解析过程(比如访问 baidu.com):
- 浏览器先查自己的 DNS 缓存
- 没找到就查操作系统缓存
- 还没找到就问路由器
- 路由器问运营商的 DNS 服务器
- 最终拿到 IP 地址(如
110.242.68.66)
localhost 不需要联网——它被操作系统特殊处理了。你的电脑上有一个叫 hosts 的文件:
- Windows:
C:\Windows\System32\drivers\etc\hosts - Linux/Mac:
/etc/hosts
这个文件里通常有这样一行:
127.0.0.1 localhost
所以浏览器查 localhost 时, 在这个文件里直接找到了答案: 127.0.0.1。不需要联网, 不需要 DNS 服务器。
你可能遇到的问题: IPv6 干扰。 有时
hosts文件中::1 localhost(IPv6 版本) 排在前面, 浏览器会优先解析到::1。如果服务只监听 IPv4 的127.0.0.1, 就会出现"浏览器能解析但连不上"的情况。解决办法是注释掉 hosts 中的::1行, 或者在浏览器中使用127.0.0.1代替localhost。
1.3 127.0.0.1 到底是什么——回环接口
127.0.0.1 是一个特殊的 IP 地址。它的特殊之处在于: 它不经过任何物理网卡。
操作系统内核中有一个虚拟的网络接口, 叫回环接口 (Loopback Interface):
Linux: lo
Windows: Loopback Pseudo-Interface
当一个程序连接 127.0.0.1 时, 数据包在内核中的路径是:
程序 → TCP/IP 协议栈 → lo 接口 → 立即返回 → TCP/IP 协议栈 → 目标程序
整个过程完全在内存中完成, 从未离开本机的网络栈, 从未经过网线、Wi-Fi 或任何物理硬件。所以回环通信的延迟极低, 速度极快。
为什么叫"回环"? 你可以想象自己对着墙壁说话, 声音立刻反弹回来——数据包也是一样, 进入 lo 接口后立即原路返回。英文叫 Loopback, 直译就是"环回/回路"。
127.0.0.1 不是唯一的回环地址。 整个 127.0.0.0/8 网段都是回环地址, 也就是说 127.0.0.2、127.255.255.254 也都是回环地址。但在开发实践中, 我们基本只用 127.0.0.1。
Windows 上的小实验: 打开 PowerShell, 执行
ping 127.0.0.99——你会发现也能通, 因为整个 127.x.x.x 网段都指向本机回环。
1.4 浏览器与服务器建立连接——TCP 三次握手(简述)
拿到 IP 地址后, 浏览器开始与目标建立 TCP 连接。这里需要端口——你访问的是 8120 端口。
TCP 连接建立的过程叫做"三次握手", 可以类比打电话:
浏览器: "喂, 127.0.0.1:8120 在吗?" (SYN)
服务器: "在的, 你说。" (SYN-ACK)
浏览器: "好的, 那开始传数据。" (ACK)
三次握手完成后, 连接建立。浏览器发送 HTTP 请求:
GET / HTTP/1.1
Host: localhost:8120
Accept: text/html,application/xhtml+xml
...
服务器(你的 Laravel 开发服务器)收到请求后, 返回 HTTP 响应:
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html>
...
浏览器解析 HTML, 渲染页面——你看到了内容。
二、端口——一台机器上的服务门牌号
2.1 为什么需要端口
一台电脑(一个 IP 地址)上可以同时运行很多网络服务:
Laravel 开发服务 -> 8120
Vite 前端开发服务 -> 5173
MySQL 数据库 -> 3306
Redis 缓存 -> 6379
另一个项目的前端 -> 5174
IP 地址让请求"找到这台电脑", 端口让请求"找到电脑上具体的服务"。
类比:
IP 地址 = 写字楼地址(XX路XX号)
端口 = 房间号(3楼 301室)
进程 = 房间里正在办公的人
你寄快递写"XX路XX号 301室", 快递员先找到写字楼, 再找到房间。同样, 127.0.0.1:8120 先定位到本机, 再定位到 8120 端口的服务。
2.2 端口是一个数字:范围与分类
端口号的范围是 0 到 65535, 分为三类:
| 范围 | 名称 | 说明 | 举例 |
|---|---|---|---|
| 0 - 1023 | 知名端口 (Well-known) | 系统保留, 绑定需要管理员权限 | 80 (HTTP), 443 (HTTPS), 22 (SSH) |
| 1024 - 49151 | 注册端口 (Registered) | 应用常用, 不需要管理员权限 | 3306 (MySQL), 6379 (Redis), 5432 (PostgreSQL) |
| 49152 - 65535 | 动态/私有端口 | 操作系统随机分配, 临时使用 | 浏览器发起连接时随机选用的源端口 |
开发服务器通常使用 1024 以上的端口, 最常见的有:
3000 -> Node.js / Next.js / React 开发服务器
4200 -> Angular 默认端口
5173 -> Vite 默认端口
8000 -> Python http.server / Django / Laravel 默认端口
8080 -> Java / Tomcat / 各类开发工具
以上是社区约定俗成的默认端口。你可以用任意 1024-65535 范围内的端口,比如本文示例中用了 --port=8120,这完全没问题——只要不和已有服务冲突就行。
为什么开发端口经常选 3000? 这是社区习惯, 没有技术原因。Rails 和早期 Node 框架广泛使用 3000, 后来被大量教程和脚手架沿用。
2.3 一个连接需要两个端口——源端口和目标端口
一个网络连接由四元组唯一标识:
源 IP : 源端口 → 目标 IP : 目标端口
例如:
192.168.1.5 : 54321 → 127.0.0.1 : 8120
- 目标端口 (8120): 你访问的服务监听的端口。这是你写在 URL 里的或者默认的那个。
- 源端口 (54321): 操作系统随机分配的一个临时端口, 用于区分不同的连接。
这意味着: 你可以同时打开多个浏览器标签访问同一个服务, 因为每个标签的源端口不同, 服务器可以把它们的响应正确送回各个标签。你不需要在 URL 里写源端口——操作系统自动处理了。
三、服务是什么——进程、监听与生命周期
3.1 程序和进程的区别
这是一个非常重要的基础概念:
程序 (Program): 磁盘上的文件, 比如 /usr/bin/node, D:\php\php.exe
进程 (Process): 程序被加载到内存中运行起来的实例
类比: 程序是食谱(纸上的文字), 进程是厨师正在照着食谱炒菜。食谱只有一份, 但可以同时有多个厨师在做同一道菜——这就是你可以同时启动多个 php artisan serve(当然端口不能冲突)。
3.2 "启动服务"到底发生了什么
当你执行 php artisan serve --port=8120:
1. PHP 解释器被加载到内存, 成为一个进程
2. 进程调用操作系统的 socket() 和 bind() 系统调用
3. bind() 告诉操作系统: "我要在 127.0.0.1:8120 上等连接"
4. 进程调用 listen(), 进入监听状态
5. 进程阻塞, 等待客户端连接
"监听" (Listen) 的意思是: 进程告诉操作系统内核, "如果有人连接 127.0.0.1 的 8120 端口, 请通知我"。在此之后, 操作系统会代为处理 TCP 握手的细节, 把建立好的连接交给进程。
形象类比: 你在办公室坐着, 门上贴了"8120 号房间, 有事敲门"。现在有人在走廊上找"8120 号房间", 找到后敲门, 你开门接待。
3.3 前台进程与后台进程
开发时, 我们通常在终端里前台运行服务:
php artisan serve --port=8120
# 终端被这个进程占用, 显示日志
# Ctrl+C → 进程结束, 服务停止
生产环境中, 服务以后台进程或系统服务的方式运行:
systemd (Linux) -> 系统启动时自动运行, 崩溃后自动重启
Supervisor -> 进程守护工具
PM2 -> Node.js 进程管理器
Docker -> 容器化运行
在开发中, 记住这一点就够了: 终端关了, 服务就没了。 一个常见的困惑场景: 你开了多个终端, 忘了哪个终端在运行服务, 结果关了错误的终端导致服务停了。
3.4 同一个端口能被多个进程监听吗?
一般不能。 一个端口在同一个协议(TCP 或 UDP)下, 同一时间只能被一个进程绑定。
如果你试图启动第二个监听同一端口的进程:
# 终端 1
php artisan serve --port=8120 # 成功
# 终端 2
php artisan serve --port=8120 # 失败!
# Error: listen EADDRINUSE: address already in use 127.0.0.1:8120
例外: SO_REUSEPORT 选项。 某些服务器软件(如 Nginx、Node cluster 模式)可以使用 SO_REUSEPORT 让多个进程"共享"一个端口, 内核会把连接均匀分配给它们。但这是高级场景, 开发中 99% 的端口冲突都是因为端口被占用了。
3.5 查看谁在监听
当你要知道"某个端口有没有被占用", 用这些命令:
# Linux / WSL
ss -ltnp # 查看所有 TCP 监听
ss -ltnp | grep 8120 # 只看 8120
lsof -i :8120 # 看是哪个进程占用了 8120
# Windows PowerShell
Get-NetTCPConnection -LocalPort 8120 # 看 8120 端口状态
netstat -ano | findstr 8120 # 传统方式
四、IP 地址全览——从 127.0.0.1 到公网 IP
4.1 IP 地址是什么
IP 地址是一串标识网络中设备的数字。IPv4 地址是 32 位二进制数, 通常写成四段十进制, 如 192.168.1.1。每段范围 0-255。
IP 地址分为两大类:
公网 IP: 全球唯一, 运营商分配。如 110.242.68.66
私有 IP: 局域网内唯一, 不直接在公网路由。
4.2 私有 IP 地址范围(你日常打交道的是这些)
| 范围 | 常见场景 | 举例 |
|---|---|---|
192.168.x.x |
家庭路由器、公司局域网 | 192.168.1.100 |
10.x.x.x |
企业内网、Docker 默认网桥 | 10.0.0.1 |
172.16.x.x - 172.31.x.x |
Docker Compose、WSL2 NAT 模式 | 172.30.85.15 |
127.x.x.x |
回环接口(本机) | 127.0.0.1 |
4.3 四个你最需要知道的特殊地址
| 地址 | 含义 | 可以用浏览器访问吗? |
|---|---|---|
127.0.0.1 |
当前设备的回环地址 | ✅ 访问本机服务 |
localhost |
主机名, 通常解析到 127.0.0.1 或 ::1 |
✅ 同上 |
0.0.0.0 |
绑定所有网卡(仅用于监听) | ❌ 不能作为访问地址 |
::1 |
IPv6 版本的回环地址 | ✅ 等价于 127.0.0.1(IPv6) |
重要区分: 0.0.0.0 是一个监听地址, 不是访问地址。你不能在浏览器里输入 http://0.0.0.0:8120 期望它工作——这就像对着天空喊"任何人"而不是对着某人喊名字。但服务监听 0.0.0.0 意味着它愿意从任何网卡接收连接, 包括 127.0.0.1 和局域网 IP。
五、监听地址决定"谁能敲门"——四种常见配置
服务启动时, 你可以指定它监听哪个地址。同一个端口, 监听地址不同, 可访问范围完全不同:
5.1 四种监听方式的对比
| 监听方式 | 命令示例 | 谁可以访问 |
|---|---|---|
监听 127.0.0.1 |
php artisan serve --host=127.0.0.1 |
只有本机能访问 |
监听 localhost |
npm run dev -- --host localhost |
解析到 127.0.0.1 或 ::1 |
监听 0.0.0.0 |
php artisan serve --host=0.0.0.0 |
本机、局域网、WSL2、Docker 都能访问 |
| 监听局域网 IP | npm run dev -- --host 192.168.1.100 |
只有通过这个 IP 来的请求才能访问 |
5.2 什么情况该用什么
纯本地开发, 不需要手机或同事访问 → 127.0.0.1(默认, 最安全)
需要手机通过局域网测试页面 → 0.0.0.0 或本机局域网 IP
WSL2 开发场景 → 0.0.0.0(兼容性最好, 详见后续章节)
Docker 容器 → 0.0.0.0(容器默认行为)
数据库只需要本地访问 → 127.0.0.1(安全优先)
安全提醒: 在公共 Wi-Fi 下监听
0.0.0.0, 同一个 Wi-Fi 下的其他人也能访问你电脑上的服务。如果在咖啡厅开发, 建议监听127.0.0.1。
六、一个页面不只是一个请求——浏览器加载网页的完整流程
6.1 第一个请求只拿到 HTML
浏览器访问 http://localhost:8120, 服务器返回的通常是一个 HTML 文档:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/app.css">
<script type="module" src="/js/app.js"></script>
</head>
<body>
<h1>欢迎</h1>
<img src="/images/logo.png" alt="logo">
</body>
</html>
浏览器收到 HTML 后立即开始解析, 发现里面引用了更多资源: CSS、JS、图片、字体等。然后自动发起新的请求去获取它们。
6.2 所以一次页面加载至少包含这些请求
| 资源类型 | Network 面板中的 Type | 由谁提供 |
|---|---|---|
| HTML | document | 后端开发服务器(Laravel/PHP) |
| CSS | stylesheet | 前端开发服务器(Vite)或后端 |
| JavaScript | script | 前端开发服务器(Vite)或后端 |
| 图片 | png/jpeg/svg | 后端或静态文件服务器 |
| 字体 | font | 前端或后端 |
| API 数据 | fetch/xhr | 后端 API |
6.3 这意味着什么——"页面能打开"不等于一切正常
页面能打开(HTML 返回了)✅
但样式全丢了(CSS 请求失败)❌
但按钮点不动(JS 请求失败)❌
但数据是空的(API 请求失败)❌
这就是为什么排查问题时, 只看页面有没有出来是不够的, 必须打开浏览器 Network 面板逐项检查。
6.4 浏览器 Network 面板入门
按 F12 打开开发者工具, 切换到 Network(网络)标签, 刷新页面。你会看到一列请求。
关键字段:
| 字段 | 看什么 |
|---|---|
| Name | 这是哪个文件的请求 |
| Status | 200 正常, 404 不存在, 500 服务器错误, (failed) 根本连不上 |
| Type | document / script / stylesheet / fetch / font / media |
| Size | 资源大小, 从缓存读取时会显示 (disk cache) |
| Time | 请求耗时 |
| Initiator | 谁触发的——HTML、JS 还是页面本身 |
排查时的判断逻辑:
Status 显示 (failed) 或 CORS error
→ 先确认 URL 是否正确 → 确认端口 → 确认监听 → 确认转发
Status 是 200 但样式没变化
→ 可能是缓存, 勾选 Disable cache
Status 404
→ 请求的路径或文件名不对
Status 500
→ 服务器内部错误, 查终端日志
七、多服务开发环境——为什么有不止一个开发服务器
7.1 现代 Web 开发为什么需要前、后端分开
传统模式(如纯 PHP + Blade 模板): 一个后端服务负责一切——HTML、CSS、JS 都是它返回的。
现代模式(如 Laravel + Vite): 后端负责 HTML 和 API, 前端开发服务器负责 CSS/JS 的编译和热更新。
传统: 浏览器 → Laravel:8120 → HTML + CSS + JS (全部由后端返回)
现代: 浏览器 → Laravel:8120 → HTML (后端返回)
浏览器 → Vite:5173 → CSS/JS (前端开发服务器返回)
7.2 Laravel + Vite 的真实请求流
1. 浏览器请求 http://localhost:8120
→ Laravel 返回 HTML, 包含 <script src="http://localhost:5173/js/app.js">
2. 浏览器解析 HTML, 发现需要 http://localhost:5173/js/app.js
→ 自动请求 Vite 开发服务器 (端口 5173)
3. Vite 开发服务器把 app.js(经过编译、HMR 注入)返回给浏览器
所以你的终端里需要同时运行两个进程:
# 终端 1 - 后端
php artisan serve --port=8120
# 终端 2 - 前端
npm run dev # Vite 默认监听 5173
7.3 HMR (Hot Module Replacement) 是什么
HMR 是前端开发服务器提供的一种能力: 修改代码后, 页面自动更新, 不需要手动刷新。
它的工作原理:
1. 浏览器连接 Vite 开发服务器的 WebSocket(ws://localhost:5173)
2. 你修改了一个 .vue 文件并保存
3. Vite 检测到文件变化 → 只编译这个模块 → 通过 WebSocket 通知浏览器
4. 浏览器收到通知 → 只替换变化的模块, 不刷新整个页面
HMR 连接失败的常见表现: 浏览器控制台出现 WebSocket 连接错误, 页面不会自动更新, 需要手动刷新。这在跨环境(WSL2)场景中尤为常见, 因为 WebSocket 连接也需要经过端口转发。
7.4 concurrently——用一个命令管理多个进程
手动开多个终端很麻烦, 可以用 concurrently(或类似工具)在一个终端里管理多个进程:
// package.json
{
"scripts": {
"dev": "concurrently \"php artisan serve --port=8120\" \"npm run dev\""
}
}
npm run dev
# 同时启动 Laravel (8120) 和 Vite (5173)
# Ctrl+C 会同时结束两个进程
八、网络边界——当开发环境不止一个"机器"
8.1 你可能同时在和多个"环境"打交道
在一台电脑上做 Web 开发, 你经常面对的不只是一个操作系统环境:
Windows 主机 → 浏览器、IDE、文件系统
WSL2 虚拟机 → Linux 内核、PHP、Composer、Node
Docker Desktop → 容器化的 MySQL、Redis、Nginx
它们虽然在同一台物理电脑上运行, 但网络层面并不完全互通。理解它们的网络边界, 是解决 WSL2 和 Docker 网络问题的前提。
8.2 WSL2 是什么——不只是"Windows 里的 Linux"
WSL2(Windows Subsystem for Linux 2)不是简单的模拟器或翻译层。它是一个完整的 Linux 内核运行在轻量级 Hyper-V 虚拟机中。
WSL1: 系统调用翻译层 → Linux 程序直接使用 Windows 内核
(网络: 与 Windows 完全共享, 没有独立的 IP)
WSL2: Hyper-V 虚拟机 → 独立 Linux 内核
(网络: 独立网络命名空间, 有独立 IP 地址)
WSL2 拥有:
- 自己的文件系统
- 自己的进程空间
- 自己的网络命名空间(这是关键)
- 自己的 IP 地址
8.3 Docker Desktop 在 WSL2 中的叠加
如果你安装了 Docker Desktop 并使用 WSL2 后端, 网络布局会进一步复杂:
Windows 主机
└── WSL2 VM
├── 你的开发服务 (PHP/Node)
└── Docker 容器 (MySQL/Redis/Nginx)
└── 每个容器有自己的网络命名空间
这形成了一种"套娃"结构。不过 Docker 有自己的端口映射机制(-p 3306:3306), 比 WSL2 的转发更容易理解。
8.4 关键认知——每个环境有自己的 127.0.0.1
这是本文最深层的概念, 也是 WSL2 开发中最容易困惑的地方:
Windows 的 127.0.0.1 → Windows 自己的回环接口 → 访问 Windows 上的服务
WSL2 的 127.0.0.1 → WSL2 VM 自己的回环接口 → 访问 WSL2 上的服务
Docker 的 127.0.0.1 → 容器自己的回环接口 → 访问容器内的服务
三个 127.0.0.1 看起来一样, 但它们是三套完全隔离的网络接口, 互不干扰。
这就引出了一个核心问题: 如果它们是隔离的, 为什么 Windows 浏览器访问 localhost:8120 能到达 WSL2 里运行的服务?
因为 WSL2 内置了一套自动端口转发机制, 打破了这种隔离。
九、WSL2 网络深入——打破隔离的转发机制
9.1 WSL2 的网络拓扑(NAT 模式, 默认)

关键角色:
- Hyper-V 虚拟交换机 (
vEthernet (WSL)): Windows 和 WSL2 VM 之间的虚拟网络设备 - NAT 组件: 负责地址转换, 让 WSL2 通过 Windows 的 IP 访问外网
- wslhost.exe: Windows 侧的转发代理进程 (位于
C:\Windows\System32\lxss\) - localhost relay: WSL2 内 init 进程的一部分, 负责检测端口并通知 wslhost.exe
- VMBus: Hyper-V 提供的虚拟机间高速通信通道
9.2 wslhost.exe 的完整工作流程——问题的答案
这就是"为什么 Windows 的 127.0.0.1 能访问 WSL2 服务"的完整答案:
步骤 1: 你在 WSL2 中启动服务
php artisan serve --host=127.0.0.1 --port=8120
→ 服务在 WSL2 的 127.0.0.1:8120 上监听
步骤 2: WSL2 的 localhost relay 检测到新监听
→ init 进程通过 netlink 和 BPF 监控内核端口绑定事件
→ 发现 8120 端口有新绑定
步骤 3: relay 通知 Windows 侧的 wslhost.exe
→ 通过 Hyper-V Socket (VMBus) 发送消息:
"WSL2 里面 8120 端口有服务了, 帮我在 Windows 侧也打开"
步骤 4: wslhost.exe 在 Windows 的 127.0.0.1:8120 上创建 TCP 监听
→ 从 Windows 角度看, 就好像有一个本地服务在监听 8120
步骤 5: 浏览器连接 Windows 的 127.0.0.1:8120
→ Windows TCP/IP 栈识别为回环流量 → 交给 wslhost.exe
步骤 6: wslhost.exe 封装数据, 通过 VMBus 发送到 WSL2 relay
→ relay 解封装, 转发给 WSL2 内实际监听的 8120 服务
步骤 7: 服务返回响应, 沿原路返回
→ WSL2 服务 → relay → VMBus → wslhost.exe → Windows TCP/IP → 浏览器
9.3 完整数据包路径(一张图看清全过程)
[Windows Chrome]
│ connect(127.0.0.1:8120)
▼
[Windows TCP/IP 栈]
│ 目标地址 127.0.0.1 → 回环接口
▼
[wslhost.exe]
│ 该端口已由 wslhost.exe 接管
│ 封装原始数据 → 通过 Hyper-V Socket 发送
▼
╔══════════════════════════════════════════════╗
║ VMBus 通道 (Hyper-V 内部) ║
║ 不经过物理网卡, 速度接近内存拷贝 ║
╚══════════════════════════════════════════════╝
▼
[WSL2 init / localhost relay]
│ 从 VMBus 接收数据 → 解封装
▼
[WSL2 内应用服务 (PHP:8120)]
│ 处理请求 → 生成响应
└── 响应原路返回
关键洞察: 整个过程对浏览器和 WSL2 内的应用完全透明。浏览器以为自己在和本机 127.0.0.1 通信, PHP 进程也以为自己在和本机 127.0.0.1 通信。它们都不知道 wslhost.exe 和 VMBus 在中间做了转发。
9.4 WSL2 的两种网络模式
| 模式 | 原理 | WSL2 的 IP | localhost 转发方式 | 可用性 |
|---|---|---|---|---|
| NAT(默认) | Hyper-V NAT 转换 | 独立 172.x.x.x |
wslhost.exe + VMBus | 所有 WSL2 版本 |
| Mirrored(镜像) | Windows 网络接口镜像到 WSL2 | 与 Windows 共享 IP | loopback0 接口中继 | Win11 22H2+ |
Mirrored 模式的优势:
- WSL2 可以看到与 Windows 相同的 IP 地址(不需要记住 WSL2 IP)
- IPv6 支持更好
- VPN 兼容性更强
- 可直接从局域网其他设备访问 WSL2 服务
如何切换到 Mirrored 模式:
在 Windows 用户目录(C:\Users\你的用户名\)下创建或编辑 .wslconfig:
[wsl2]
networkingMode=mirrored
然后重启 WSL:
wsl --shutdown
9.5 localhost 转发失效——最高频的 WSL2 网络问题
虽然 localhost 转发是 WSL2 自动管理的, 但在以下情况会失效:
| 原因 | 底层机制 | 典型表现 | 解决方法 |
|---|---|---|---|
| Windows 休眠/睡眠 | Hyper-V 虚拟网络状态未正确恢复, wslhost 转发中断 | 几分钟前还能用, 回来后全不通 | wsl --shutdown 然后重新打开 WSL |
| Windows 快速启动 | 快速启动 = 部分休眠, 跟休眠同样的原理 | 开机后 localhost 不通 | 禁用快速启动, 或 wsl --shutdown |
| 端口被 Windows 保留 | 系统有动态端口排除范围, wslhost 无法绑定 | 某些端口可以, 某些不行 | 换端口, 或用 netsh int ipv4 show excludedportrange 检查 |
| VPN 软件干扰 | VPN 修改了路由表, 回环流量被重定向 | 连上 VPN 后 localhost 全灭 | 排查 VPN 的分流设置, 或用 Mirrored 模式 |
| Windows 防火墙拦截 | 防火墙阻止 wslhost.exe 的网络活动 | 端口连接被拒绝 | 添加防火墙入站规则 |
| Hyper-V 虚拟交换机异常 | 虚拟网卡配置损坏或不匹配 | WSL2 完全无法联网 | wsl --shutdown, 检查虚拟交换机 |
最高频的修复命令:
wsl --shutdown
然后重新打开 WSL 终端, 重新启动开发服务。这个操作会彻底重置 WSL2 VM 和 Hyper-V 网络栈, 解决 90% 的转发问题。不需要重启 Windows。
如何验证问题确实是转发失效而非服务问题:
# Step 1: 在 WSL2 内部测试——确认服务本身正常
curl -I http://127.0.0.1:8120
# 如果返回 200 → 服务本身没问题
# Step 2: 在 Windows 侧测试——确认是否是转发问题
# PowerShell:
Test-NetConnection 127.0.0.1 -Port 8120
# 如果失败但 Step 1 成功 → 转发失效, 执行 wsl --shutdown
9.6 手动端口转发——备选方案
如果自动转发不工作, 可以手动设置端口代理:
# 1. 获取 WSL2 的 IP 地址
wsl hostname -I
# 输出如: 172.30.85.15
# 2. 创建端口转发规则
netsh interface portproxy add v4tov4 `
listenport=8120 `
listenaddress=0.0.0.0 `
connectport=8120 `
connectaddress=172.30.85.15
# 3. 查看现有规则
netsh interface portproxy show all
# 4. 删除规则
netsh interface portproxy delete v4tov4 listenport=8120 listenaddress=0.0.0.0
缺点: WSL2 的 IP 每次重启可能变化, 手动转发规则不会自动更新。所以自动转发(wslhost)是更好的选择, 手动转发只是备选。
十、端口冲突跨环境——Windows 端口被占时 WSL2 还能用吗
这是一个非常典型的场景, 能完美验证 WSL2 的网络隔离和转发机制。
10.1 场景描述
Windows 的 6543 端口被某个进程占用了(比如一个本地 MySQL)
你想在 WSL2 中让服务也使用 6543 端口
Windows 浏览器访问 localhost:6543 会发生什么?
10.2 答案
WSL2 内部: 端口 6543 完全可以用, 因为 Windows 和 WSL2 是独立的网络命名空间。Windows 的端口占用和 WSL2 的端口占用互不干扰。
Windows 浏览器访问 localhost:6543: 取决于:
-
如果 WSL2 服务监听的是
0.0.0.0:6543→ wslhost.exe 尝试在 Windows 的 127.0.0.1:6543 上建立监听 → 失败(端口已被占用)→ Windows 浏览器访问 localhost:6543 会连到 Windows 自己的进程, 不是 WSL2 -
如果 WSL2 服务监听的是
127.0.0.1:6543→ 同上, wslhost.exe 同样需要 Windows 侧的 6543, 也被占用了
10.3 这个问题说明了什么
Windows lo (127.0.0.1) WSL2 lo (127.0.0.1)
│ │
│ 端口 6543 → Windows 进程 ✅ │ 端口 6543 → WSL2 服务 ✅
│ wslhost 无法绑定 ❌ │ 正常运行
│ │
└──── VMBus 隧道是通的 ────────┘
▲
隧道本身没问题, 但 Windows 侧入口被堵住了
核心原理:
- wslhost.exe 的工作原理是"在 Windows 侧帮 WSL2 占一个坑"
- 如果坑已经被别人占了, wslhost 就占不上, 转发就建不起来
- 但 WSL2 内部完全不受影响——它根本不知道 Windows 那边发生了什么
10.4 解决方案
方案一: 用 WSL2 的 eth0 IP 直连(前提: 服务监听 0.0.0.0)
# 获取 WSL2 的 IP
wsl hostname -I
# 假设输出: 172.30.85.15
# 在 Windows 浏览器中访问
http://172.30.85.15:6543
方案二: 换端口
把 WSL2 服务的端口换成一个 Windows 上空闲的端口:
# WSL2 中
php artisan serve --port=6544
Windows 浏览器访问 http://localhost:6544——wslhost.exe 会在 Windows 侧的 6544 上创建监听(如果 6544 空闲)。
10.5 通用原则
WSL2 内的端口资源是独立的, 不与 Windows 共享。
但 wslhost.exe 需要在 Windows 侧绑定同名端口来做转发。
如果 Windows 侧同名端口被占用, 转发失败, 但不影响 WSL2 内部使用。
此时可以通过 WSL2 IP 直连, 或者换到 Windows 空闲的端口。
十一、监听 127.0.0.1 vs 0.0.0.0——WSL2 场景下的关键差异
11.1 对比表
| 监听地址 | WSL2 内部 curl | Windows localhost | 通过 WSL2 IP 直连 | 局域网其他设备 |
|---|---|---|---|---|
127.0.0.1 |
✅ | ⚠️ 仅依赖 wslhost 转发 | ❌ | ❌ |
0.0.0.0 |
✅ | ✅ wslhost 转发 + IP 直连 | ✅ | ✅ (需防火墙规则) |
11.2 为什么 0.0.0.0 更可靠(WSL2 场景)
监听 127.0.0.1:
Windows 访问路径: Windows Chrome → wslhost.exe → VMBus → WSL2 relay → 服务
(只有这一条路)
如果 wslhost 失效 → Windows 浏览器访问不通 ❌
监听 0.0.0.0:
路径 A: Windows Chrome → wslhost.exe → VMBus → WSL2 relay → 服务
路径 B: Windows Chrome → 172.x.x.x:端口 → Hyper-V 虚拟交换机 → WSL2 eth0 → 服务
(不需要 wslhost! 走正常的虚拟网络)
多了一条完全不依赖 wslhost 的备用路径。
11.3 安全提醒
一个重要但容易被忽略的事实:
即使在 WSL2 中监听 127.0.0.1, wslhost.exe 仍然会把它转发到 Windows。
所以不要认为"我在 WSL2 里监听 127.0.0.1, Windows 就访问不到"——
WSL2 的 localhost 转发机制会打破这个隔离。
如果你需要某个服务只在 WSL2 内部使用(比如 Redis 缓存), 又不想让 Windows 侧的应用访问它, 不能仅仅依赖 127.0.0.1 绑定——还需要配置防火墙规则或使用其他方式限制访问。
11.4 开发实践建议
开发服务器 (Vite, PHP, Node) → 监听 0.0.0.0, 兼容性最好
数据库 (MySQL, Redis) → 仅 WSL2 内部使用就监听 127.0.0.1
Docker 容器 → 默认监听 0.0.0.0, 通过 -p 端口映射
需要手机局域网测试 → 必须监听 0.0.0.0 或本机局域网 IP
十二、端口故障排查——四类问题和排查顺序
12.1 端口不通的四种根本原因
排查时, 不要混在一起。每一种的验证方式不同:
| 类型 | 本质 | 典型表现 | 第一验证手段 |
|---|---|---|---|
| 进程没启动 | 没有人在等电话 | Connection refused |
检查终端有无报错, 进程是否在运行 |
| 监听地址不对 | 服务员在, 但没在门口等你 | WSL 内 curl 成功, Windows 不通 |
确认启动命令中的 --host 参数 |
| 端口被占用 | 门牌号已经被用了 | 启动时报 EADDRINUSE |
ss -ltnp 或 Get-NetTCPConnection |
| 端口被系统保留 | 管理层不允许用这个门牌号 | 没其他进程占用, 但无法绑定 | netsh int ipv4 show excludedportrange |
12.2 固定排查顺序(从快到慢, 从近到远)
第一步: 看浏览器 Network 面板
→ 确认失败请求的完整 URL、类型、状态码
→ 判断是 document / script / stylesheet / fetch
第二步: 确认进程是否在运行
→ 终端有没有报错? 服务有没有崩溃?
→ ps -ef | grep php (Linux/WSL)
→ Get-Process (PowerShell)
第三步: 在服务所在环境内部测试
→ WSL 内: curl -I http://127.0.0.1:端口
→ 成功 → 服务本身没问题, 问题在访问层
→ 失败 → 检查监听: ss -ltnp | grep 端口
第四步: 在浏览器所在环境测试连通性
→ Windows PowerShell: Test-NetConnection 127.0.0.1 -Port 端口
→ 成功 → 网络层没问题, 可能是应用层 (HTTP 路由、CORS)
→ 失败 + Step 3 成功 → 跨环境转发问题
第五步: 如果是跨环境, 排查转发
→ wsl --shutdown (WSL2 场景)
→ 检查 Windows 端口保留: netsh int ipv4 show excludedportrange
→ 检查防火墙规则
→ 考虑用 WSL2 IP 直连
第六步: 如果 HTTP 已成功但浏览器仍然拦
→ 检查 CORS (见下一章)
→ 检查 MIME type
→ 检查 mixed content (HTTPS 页面加载 HTTP 资源)
12.3 --strictPort——让端口问题尽早暴露
很多开发服务器在端口被占用时会自动换到下一个端口(比如 5173 被占就换到 5174)。这对新手不友好, 因为你以为服务在 5173, 实际跑在了 5174。
npm run dev -- --port 5173 --strictPort
# 端口不对就报错, 不要悄悄换端口
十三、CORS——浏览器安全策略入门
13.1 CORS 是什么
CORS (Cross-Origin Resource Sharing, 跨源资源共享) 是浏览器的一项安全策略。当一个网页试图向不同"源"的服务发起请求时, 浏览器会检查对方是否允许。
"源" (Origin) 由三部分组成:
协议 + 主机名 + 端口
http://127.0.0.1:8120 → 一个源
http://127.0.0.1:5173 → 另一个源(端口不同)
http://localhost:8120 → 又一个源(主机名不同, 即使都指向 127.0.0.1)
13.2 什么情况会触发 CORS
页面在 http://localhost:8120 加载
└── JS 代码 fetch("http://localhost:5173/api/data")
→ 不同的端口 → 跨源! → 浏览器发送 CORS 预检请求
13.3 CORS 不是所有错误的根因
常见的误解: 看到 Network 面板红了就以为是 CORS。
实际上, 浏览器 Network 面板中 CORS 错误的典型展示是:
Status: (blocked: CORS) 或显示 CORS error
但如果 Status 显示 (failed) 或者连接超时, 那不是 CORS——端口都还没连上, 谈不上跨源策略。
判断顺序:
端口连通了? → HTTP 返回了? → 只有返回了+被浏览器拦截 → 才可能是 CORS
端口都不通 → 先修端口, 别管 CORS
HTTP 500 → 修服务器代码
HTTP 200 但数据不对 → 检查请求参数和路由
13.4 快速解决开发中的 CORS
开发中最简单的解决方法是让前端开发服务器代理 API 请求, 或者在后端配置允许跨源:
// Laravel CORS 配置 (config/cors.php)
'allowed_origins' => ['http://localhost:5173'],
// 或者允许所有(仅限开发环境!)
'allowed_origins' => ['*'],
十四、实战场景速查
场景一: 页面能打开, 但没有样式
判断: HTML 返回成功, CSS/JS 加载失败
排查:
1. Network 面板 → filter: stylesheet → 看 Status 和 Request URL
2. 确认前端开发服务是否在运行(如 Vite:5173)
3. 确认端口是否可达(WSL2 场景先查转发)
4. 检查 HTML 中的资源路径(@vite() 指令等)
场景二: WSL 内 curl 成功, Windows 浏览器不通
判断: 服务正常, 转发问题
解决:
1. wsl --shutdown → 重开 WSL 终端 → 重启服务
2. 如果还不行, 检查端口是否被 Windows 保留
3. 后备方案: 服务改监听 0.0.0.0, 用 wsl hostname -I 获取 IP 直连
场景三: 服务启动后端口变了
判断: 原端口被占用, 开发服务器自动换端口
应对:
1. 使用 --strictPort
2. 固定项目约定端口, 如 --port 5173
场景四: WSL2 休眠后浏览器突然连不上
这是 WSL2 开发中最常见的"玄学"问题
根因: Windows 休眠 → Hyper-V 虚拟网络未恢复 → wslhost 转发失效
验证: wsl curl -I http://127.0.0.1:端口 (WSL2 内成功 → 转发失效)
解决: wsl --shutdown → 重开 WSL → 重启服务
预防: 长期不用时主动 wsl --shutdown, 比被动休眠更稳定
场景五: Windows 端口被占用, WSL2 中同名端口是否可用
WSL2 内可用 ✅(独立网络命名空间)
但 Windows localhost 访问会到 Windows 自己的进程 ❌(wslhost 无法绑定)
解决: 换端口, 或用 WSL2 IP 直连(需服务监听 0.0.0.0)
十五、命令速查
状态查询
| 你想知道 | Windows PowerShell | Linux / WSL2 |
|---|---|---|
| 某端口是否在监听 | Test-NetConnection 127.0.0.1 -Port 8120 |
ss -ltnp | grep 8120 |
| 谁占用了某端口 | Get-NetTCPConnection -LocalPort 8120 |
lsof -i :8120 或 ss -ltnp |
| 所有监听端口 | netstat -ano |
ss -ltnp |
| Windows 端口保留范围 | netsh int ipv4 show excludedportrange protocol=tcp |
不适用 |
| WSL2 的 IP 地址 | wsl hostname -I |
hostname -I 或 ip addr show eth0 |
故障修复
| 问题 | 命令 |
|---|---|
| WSL2 转发失效 | wsl --shutdown |
| 手动端口转发 | netsh interface portproxy add v4tov4 listenport=8120 listenaddress=0.0.0.0 connectport=8120 connectaddress=172.x.x.x |
| 查看转发规则 | netsh interface portproxy show all |
| 删除转发规则 | netsh interface portproxy delete v4tov4 listenport=8120 listenaddress=0.0.0.0 |
十六、总结——你应该带走的十点认知
-
服务是一个正在运行的进程, 进程必须监听某个地址和端口才能被访问。
-
127.0.0.1是回环地址, 数据不经过物理网卡, 完全在内核中处理。 每个网络命名空间(Windows、WSL2、Docker 容器)有自己独立的回环接口——它们的127.0.0.1彼此隔离。 -
端口是进程的"门牌号"。 一个端口同一时间只能被一个进程监听。端口共 65535 个, 0-1023 需要管理员权限。
-
0.0.0.0是监听地址, 不是访问地址。 它表示"在所有网卡上监听", 意味着可以从任何方向访问。 -
一个页面由多个请求组成。 HTML 返回成功不代表 CSS/JS/API 也成功。排查时先看 Network 面板的 Type 和 Status, 定位到具体哪个请求失败了。
-
WSL2 的 localhost 转发是由
wslhost.exe+ VMBus 实现的。 Windows 的127.0.0.1和 WSL2 的127.0.0.1确实不同, 但 wslhost.exe 在中间充当了"接线员"——它监听 Windows 侧端口, 通过 Hyper-V 内部通道转发到 WSL2。 -
WSL2 端口转发最常见的问题是 Windows 休眠后失效。
wsl --shutdown是最高频的修复命令, 不需要重启 Windows。 -
Windows 和 WSL2 的端口资源是独立的。 Windows 端口被占用不影响 WSL2 使用该端口, 但 wslhost 无法在 Windows 侧建立同名端口的转发。此时可以通过 WSL2 的 IP 直连。
-
在 WSL2 中, 监听
0.0.0.0比127.0.0.1多一条备用路径。 当 wslhost 转发失效时, 可以通过 WSL2 的 eth0 IP 直接访问。 -
排查顺序口诀: 浏览器 URL → 服务监听 → 服务内部 curl → 浏览器侧连通性 → 转发/端口保留 → CORS。不要跳步, 不要猜框架问题。
最重要的一句话: 不要先猜框架问题。先确认请求从哪里发出, 要到哪里去, 中间经过哪些网络边界。 框架报错通常已经是"上层建筑"了, 网络层不通, 框架根本没机会报错。解决网络层问题最好的方法, 就是用
curl和Test-NetConnection逐段验证链路的每一环。
[DRAFT_RESTORED]
已恢复你上次未提交的评论草稿。
正文草稿仅保留在当前标签页;若浏览器已记住你的身份信息,昵称、邮箱和个人网站可在其他文章页自动回填。