GANKUDADIZ
BACK_TO_BLOG
TECH_LOG :: 2026.06.22

本地开发服务, 端口与网络访问基础

Avatar
By Gankudadiz · 7 min read · 28 views

从浏览器输入地址到看到页面的完整链路, 深入理解进程、端口、地址、转发和跨环境访问。面向新手, 由浅入深。


零、从一个具体场景开始

假设你正在开发一个 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):

  1. 浏览器先查自己的 DNS 缓存
  2. 没找到就查操作系统缓存
  3. 还没找到就问路由器
  4. 路由器问运营商的 DNS 服务器
  5. 最终拿到 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.2127.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 模式, 默认)

关键角色:

  1. Hyper-V 虚拟交换机 (vEthernet (WSL)): Windows 和 WSL2 VM 之间的虚拟网络设备
  2. NAT 组件: 负责地址转换, 让 WSL2 通过 Windows 的 IP 访问外网
  3. wslhost.exe: Windows 侧的转发代理进程 (位于 C:\Windows\System32\lxss\)
  4. localhost relay: WSL2 内 init 进程的一部分, 负责检测端口并通知 wslhost.exe
  5. 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 -ltnpGet-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 :8120ss -ltnp
所有监听端口 netstat -ano ss -ltnp
Windows 端口保留范围 netsh int ipv4 show excludedportrange protocol=tcp 不适用
WSL2 的 IP 地址 wsl hostname -I hostname -Iip 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

十六、总结——你应该带走的十点认知

  1. 服务是一个正在运行的进程, 进程必须监听某个地址和端口才能被访问。

  2. 127.0.0.1 是回环地址, 数据不经过物理网卡, 完全在内核中处理。 每个网络命名空间(Windows、WSL2、Docker 容器)有自己独立的回环接口——它们的 127.0.0.1 彼此隔离。

  3. 端口是进程的"门牌号"。 一个端口同一时间只能被一个进程监听。端口共 65535 个, 0-1023 需要管理员权限。

  4. 0.0.0.0 是监听地址, 不是访问地址。 它表示"在所有网卡上监听", 意味着可以从任何方向访问。

  5. 一个页面由多个请求组成。 HTML 返回成功不代表 CSS/JS/API 也成功。排查时先看 Network 面板的 Type 和 Status, 定位到具体哪个请求失败了。

  6. WSL2 的 localhost 转发是由 wslhost.exe + VMBus 实现的。 Windows 的 127.0.0.1 和 WSL2 的 127.0.0.1 确实不同, 但 wslhost.exe 在中间充当了"接线员"——它监听 Windows 侧端口, 通过 Hyper-V 内部通道转发到 WSL2。

  7. WSL2 端口转发最常见的问题是 Windows 休眠后失效。 wsl --shutdown 是最高频的修复命令, 不需要重启 Windows。

  8. Windows 和 WSL2 的端口资源是独立的。 Windows 端口被占用不影响 WSL2 使用该端口, 但 wslhost 无法在 Windows 侧建立同名端口的转发。此时可以通过 WSL2 的 IP 直连。

  9. 在 WSL2 中, 监听 0.0.0.0127.0.0.1 多一条备用路径。 当 wslhost 转发失效时, 可以通过 WSL2 的 eth0 IP 直接访问。

  10. 排查顺序口诀: 浏览器 URL → 服务监听 → 服务内部 curl → 浏览器侧连通性 → 转发/端口保留 → CORS。不要跳步, 不要猜框架问题。


最重要的一句话: 不要先猜框架问题。先确认请求从哪里发出, 要到哪里去, 中间经过哪些网络边界。 框架报错通常已经是"上层建筑"了, 网络层不通, 框架根本没机会报错。解决网络层问题最好的方法, 就是用 curlTest-NetConnection 逐段验证链路的每一环。

评论 (0)

还没有评论,来留下第一条想法吧。

[DRAFT_RESTORED]

已恢复你上次未提交的评论草稿。

正文草稿仅保留在当前标签页;若浏览器已记住你的身份信息,昵称、邮箱和个人网站可在其他文章页自动回填。

ACTION:

[AUTHOR_PROFILE_REMEMBERED]

浏览器已记住你的昵称、邮箱和个人网站,切换到其他文章页时会自动回填。