<svg viewBox="0 0 960 520" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.bg { fill: #0f172a; }
.card { fill: #1e293b; rx: 8; ry: 8; stroke: #334155; stroke-width: 1.5; }
.text-title { fill: #f8fafc; font-family: system-ui, sans-serif; font-weight: 700; font-size: 22px; }
.text-subtitle { fill: #94a3b8; font-family: system-ui, sans-serif; font-size: 14px; }
.text-header { fill: #38bdf8; font-family: system-ui, sans-serif; font-weight: 600; font-size: 15px; }
.text-body { fill: #e2e8f0; font-family: system-ui, sans-serif; font-size: 13px; }
.text-muted { fill: #64748b; font-family: system-ui, sans-serif; font-size: 12px; }
.line-main { stroke: #475569; stroke-width: 2; stroke-dasharray: 4,4; }
.arrow-req { stroke: #38bdf8; stroke-width: 2; marker-end: url(#arrow); }
.arrow-res { stroke: #10b981; stroke-width: 2; marker-end: url(#arrow-green); }
.arrow-hole { stroke: #f59e0b; stroke-width: 2; stroke-dasharray: 3,3; }
</style>
<marker orient="auto-start-reverse" markerHeight="6" markerWidth="6" refY="5" refX="5" viewBox="0 0 10 10" id="arrow">
<path fill="#38bdf8" d="M 0 0 L 10 5 L 0 10 z"></path>
</marker>
<marker orient="auto-start-reverse" markerHeight="6" markerWidth="6" refY="5" refX="5" viewBox="0 0 10 10" id="arrow-green">
<path fill="#10b981" d="M 0 0 L 10 5 L 0 10 z"></path>
</marker>
</defs>
<rect class="bg" height="520" width="960"></rect>
<text class="text-title" y="45" x="40">图 1.3：UDP 打洞 (Hole Punching) 交互时序图</text>
<text class="text-subtitle" y="70" x="40">展示两端借助公网 STUN / 信令服务器交换映射端口并建立直连全过程</text>
<rect class="card" height="40" width="180" y="100" x="80"></rect>
<text class="text-header" y="125" x="110">🎮 玩家 A (NAT A)</text>
<line class="line-main" y2="450" x2="170" y1="140" x1="170"></line>
<rect stroke="#38bdf8" class="card" height="40" width="180" y="100" x="390"></rect>
<text fill="#38bdf8" class="text-header" y="125" x="410">🌐 STUN / 信令服务器</text>
<line class="line-main" y2="450" x2="480" y1="140" x1="480"></line>
<rect class="card" height="40" width="180" y="100" x="700"></rect>
<text class="text-header" y="125" x="730">🎮 玩家 B (NAT B)</text>
<line class="line-main" y2="450" x2="790" y1="140" x1="790"></line>
<line class="arrow-req" y2="190" x2="480" y1="170" x1="170"></line>
<text class="text-body" y="175" x="210">① A 请求获取公网映射 (122.x:40001)</text>
<line class="arrow-req" y2="190" x2="480" y1="170" x1="790"></line>
<text class="text-body" y="175" x="520">① B 请求获取公网映射 (123.x:50000)</text>
<line class="arrow-res" y2="250" x2="170" y1="230" x1="480"></line>
<line class="arrow-res" y2="250" x2="790" y1="230" x1="480"></line>
<text fill="#6ee7b7" class="text-body" y="235" x="310">② 信令服务器交换双方公网 IP:Port</text>
<line class="arrow-hole" y2="330" x2="780" y1="290" x1="170"></line>
<text fill="#fef08a" class="text-body" y="300" x="250">③ A 向 B 尝试发送 UDP 包（打洞/可能被过滤）</text>
<line class="arrow-hole" y2="350" x2="180" y1="310" x1="790"></line>
<text fill="#fef08a" class="text-body" y="340" x="500">③ B 向 A 尝试发送 UDP 包（建立防线信任）</text>
<rect stroke="#10b981" rx="6" fill="#022c22" height="40" width="520" y="385" x="220"></rect>
<text fill="#6ee7b7" class="text-body" y="410" x="260">⚡ ④ 双向 P2P 直连通道打洞成功！数据不再经过服务器</text>
</svg>
