CS2 Pixel Streaming 公网部署文档
适用场景
- UE5 Pixel Streaming 项目(CS2.exe)
- 服务器在路由器/NAT 后面,只能开 TCP 端口,无法开 UDP 端口
- 需要让公网用户通过浏览器访问像素流
如果你的网络环境能随便开 UDP 端口,这套方案是杀鸡用牛刀,直接用 PixelStreaming 默认配置即可。本文档专门解决"UDP 不通"这个痛点。
整体架构
外网浏览器
│ TCP 8007 (HTTP + WebSocket)
▼
nginx (8007) ──/cs2/───▶ Cirrus HTTP (127.0.0.1:8018)
└/cs2ws/─▶ Cirrus WS (127.0.0.1:8018)
│
│ TCP 3478 (TURN over TCP) ──直连服务器──▶ coturn (3478)
│ │
│ │ 内网 UDP
│ ▼
│ CS2.exe (8890)
│ ▲
└──────信令(WS)──────▶ Cirrus ──────信令──┘
关键设计 :WebRTC 媒体流走 TURN-over-TCP,所有外网流量只用 TCP(8007 + 3478)。
coturn 把 relay 地址通告为局域网 IP (192.168.0.184),CS2.exe 通过内网 UDP 与 coturn 通信,完全不需要把 UDP 端口暴露到公网。
网络端口规划
| 端口 | 协议 | 用途 | 暴露到公网 |
|---|---|---|---|
| 8007 | TCP | nginx 反向代理入口 | 是 |
| 3478 | TCP | TURN 服务端口 | 是 |
| 8018 | TCP | Cirrus 信令服务器(HTTP+WS) | 否(只本机) |
| 8890 | TCP | Cirrus ↔ CS2.exe 信令 | 否 |
| 49160-49200 | UDP | TURN relay(内网用) | 否 |
外网只需开 TCP 8007 和 TCP 3478 两个端口。
前置准备(换服务器时全部要重做)
1. 确认服务器信息
记录以下三个值,后续配置全部围绕它们:
| 变量 | 示例 | 怎么查 |
|---|---|---|
| 公网 IP | 公网IP |
浏览器搜"IP" 或 curl ifconfig.me |
| 内网 IP | 192.168.0.184 |
PowerShell ipconfig,看主网卡的 IPv4 地址 |
| 内网网关 | 192.168.0.1 |
同上,看"默认网关" |
2. 防火墙开放 TCP 端口
PowerShell 管理员运行:
powershell
New-NetFirewallRule -DisplayName "nginx-8007" -Direction Inbound -Protocol TCP -LocalPort 8007 -Action Allow
New-NetFirewallRule -DisplayName "TURN-TCP-3478" -Direction Inbound -Protocol TCP -LocalPort 3478 -Action Allow
3. 路由器端口转发
在路由器后台(本文档以 H3C GR3200 为例)的「虚拟服务器」(或「端口映射」)里加两条:
| 名称 | 协议 | 外部端口 | 内部 IP | 内部端口 |
|---|---|---|---|---|
| nginx-8007 | TCP | 8007 | 192.168.0.184 | 8007 |
| TURN-3478 | TCP | 3478 | 192.168.0.184 | 3478 |
注意:内部 IP 必须和当时记录的服务器内网 IP 一致。建议在路由器 DHCP 设置里把服务器 MAC 绑定固定 IP,避免重启后变化。
4. 云安全组(仅云服务器需要)
如果是阿里云/腾讯云/华为云的 ECS,还要在安全组入方向放行:
- TCP 8007
- TCP 3478
授权对象 0.0.0.0/0。
5. 端口验证
任意外网设备(手机 4G 关 WiFi 是最快的方式)访问端口检测网站,验证 8007 和 3478 都显示「开启」:
或在 PowerShell:
powershell
Test-NetConnection 你的公网IP -Port 8007
Test-NetConnection 你的公网IP -Port 3478
服务器侧配置(共 4 个文件)
工程目录: C:\Users\Administrator\Desktop\CS2Build\
PixelStreaming 信令目录: Windows\CS2\Samples\PixelStreaming\WebServers\SignallingWebServer\
文件 1: turnserver.conf
路径: 信令目录\turnserver.conf
conf
listening-port=3478
listening-ip=0.0.0.0
relay-ip=192.168.0.184
external-ip=192.168.0.184
min-port=49160
max-port=49200
realm=PixelStreaming
lt-cred-mech
user=PixelStreamingUser:AnotherTURNintheroad
fingerprint
no-multicast-peers
no-tls
no-dtls
no-cli
verbose
log-file=stdout
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
allowed-peer-ip=127.0.0.1
allowed-peer-ip=192.168.0.184
换服务器时要改:
relay-ip→ 新服务器的内网 IPexternal-ip→ 新服务器的内网 IP(故意写内网,不要写公网!这是绕开 UDP 端口的关键)allowed-peer-ip第二行 → 新服务器的内网 IP- 账号密码可以改,但要和文件 2 里的对应一致
文件 2: config.json
路径: 信令目录\config.json
json
{
"UseFrontend": false,
"UseMatchmaker": false,
"UseHTTPS": false,
"LogToFile": true,
"LogVerbose": true,
"HomepageFile": "player.html",
"AdditionalRoutes": {},
"EnableWebserver": true,
"MatchmakerAddress": "",
"MatchmakerPort": 9999,
"PublicIp": "公网IP",
"HttpPort": 8018,
"HttpsPort": 443,
"StreamerPort": 8890,
"SFUPort": 8891,
"MaxPlayerCount": -1,
"peerConnectionOptions": "{\"iceServers\":[{\"urls\":[\"turn:公网IP:3478?transport=tcp\"],\"username\":\"PixelStreamingUser\",\"credential\":\"AnotherTURNintheroad\"}],\"iceTransportPolicy\":\"relay\"}"
}
换服务器时要改:
PublicIp→ 新服务器的公网 IPpeerConnectionOptions里的turn:公网IP:3478→ 新服务器的公网 IP(浏览器从外网连 TURN 用的)- 账号密码(
username/credential) → 与 turnserver.conf 里的一致
关键参数解释:
iceTransportPolicy: "relay"强制只走 TURN 中继,不尝试 P2P 直连(P2P 在我们这种网络环境下永远会失败,跳过省时间)?transport=tcp强制走 TURN-TCP,不走 UDP
文件 3: start_cs2_all.bat
路径: Windows\start_cs2_all.bat
bat
@echo off
chcp 65001 >nul
title CS2 Pixel Streaming
echo [1/4] Starting coturn TURN server (TCP 3478)...
start "CS2_TURN" cmd /k "cd /d C:\Users\Administrator\Desktop\CS2Build\Windows\CS2\Samples\PixelStreaming\WebServers\SignallingWebServer && platform_scripts\cmd\coturn\turnserver.exe -c turnserver.conf"
timeout /t 3 /nobreak >nul
echo [2/4] Starting Pixel Streaming SignallingWebServer...
start "CS2_Signalling" cmd /k "cd /d C:\Users\Administrator\Desktop\CS2Build\Windows\CS2\Samples\PixelStreaming\WebServers\SignallingWebServer && platform_scripts\cmd\node\node.exe cirrus.js"
timeout /t 5 /nobreak >nul
echo [3/4] Starting CS2.exe...
start "CS2_UE" "C:\Users\Administrator\Desktop\CS2Build\Windows\CS2.exe" -AudioMixer -PixelStreamingIP=127.0.0.1 -PixelStreamingPort=8890 -log -RenderOffScreen
timeout /t 2 /nobreak >nul
echo [4/4] Done.
echo Local URL: http://127.0.0.1:8018/player.html
echo Remote URL: http://公网IP:8007/cs2/player.html
pause
注意:
- 启动顺序必须是 TURN → Cirrus → CS2.exe,前者没起来后者会连不上
- Cirrus 启动不再传
--PublicIp=127.0.0.1这个参数,让它自己读 config.json -RenderOffScreen让 UE 不开窗口,服务器无显示器也能跑
换服务器时要改:
- 所有
C:\Users\Administrator\Desktop\CS2Build\路径(如果工程放在别的目录) - 最后两行 echo 的 URL
文件 4: nginx.conf(片段)
路径: E:\nginx-1.24.0\conf\nginx.conf
在 http {} 块里,确保有以下 map(用于 WebSocket Upgrade):
nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
在 server { listen 8007; ... } 里加两个 location:
nginx
location /cs2/ {
proxy_pass http://127.0.0.1:8018/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
location /cs2ws/ {
proxy_pass http://127.0.0.1:8018/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_buffering off;
}
/cs2/ 给 HTML/JS 等静态资源走,/cs2ws/ 专门给 WebSocket 信令走(player 端通过 ?ss= 指定)。
修改后:
cmd
cd /d E:\nginx-1.24.0
nginx.exe -s reload
启动 / 重启流程
完整重启
-
关掉旧进程:
cmdtaskkill /f /im CS2.exe taskkill /f /im node.exe taskkill /f /im turnserver.exe -
双击运行
Windows\start_cs2_all.bat,等 4 个步骤跑完。 -
会出现 3 个黑窗口:
CS2_TURN--- coturn,正常应输出IPv4. TCP listener opened on : 0.0.0.0:3478CS2_Signalling--- cirrus.js,正常应输出WebSocket listening for Streamer connections on :8890和Http listening on *: 8018CS2_UE--- CS2 引擎日志,等 30 秒左右出现PixelStreaming相关日志即可
-
nginx 应该已经在跑,如果没跑:
cmdcd /d E:\nginx-1.24.0 start nginx.exe
访问地址
- 服务器本机测试:
http://127.0.0.1:8018/player.html - 外网用户访问:
http://公网IP:8007/cs2/player.html
故障排查
浏览器一直黑屏
打开 F12 控制台:
| 现象 | 原因 | 解决 |
|---|---|---|
| WebSocket 报 1006 | nginx /cs2ws/ 没配对,或 cirrus 没起来 |
看 cirrus 窗口日志,检查 nginx 配置 |
ICE candidate 没出现 relay 类型 |
TURN 没生效,iceTransportPolicy 没读到 | 检查 config.json 的 peerConnectionOptions JSON 是否完整、转义对了 |
Set video source 出现但画面不动 |
UE 端没产帧 | 看 CS2_UE 窗口日志,常见是 GPU/编码器没初始化 |
ICE 候选出现 公网IP:50xxx |
TURN 工作了 | 应该能看到画面 |
TURN 窗口报 bind: address already in use
3478 被占用。查谁占着:
powershell
netstat -ano | findstr ":3478"
kill 掉占用的进程,或者改 turnserver.conf 的端口(同时改 config.json 和路由器转发)。
TURN 窗口启动了但浏览器连不上
最可能是路由器虚拟服务器规则没生效或失效。重新走"前置准备"的端口验证步骤。
远程访问黑屏但本机 (127.0.0.1:8018) 正常
99% 是 TURN 链路没通。手动外网测 3478:
powershell
Test-NetConnection 公网IP -Port 3478
返回 True 才说明路由器+防火墙整条链路打通。
路由器换了 / 重启后服务器内网 IP 变了
turnserver.conf 里 relay-ip / external-ip / allowed-peer-ip 都要改成新的内网 IP,然后重启 turnserver。建议在路由器里把服务器 MAC 绑定 IP,一劳永逸。
换服务器时的修改清单
按这个清单逐项过一遍,基本不会漏:
- 记录新服务器公网 IP、内网 IP
- Windows 防火墙开 TCP 8007、TCP 3478
- 路由器虚拟服务器加 TCP 8007、TCP 3478 转发到新内网 IP
- 云安全组(如有)放行 TCP 8007、TCP 3478
- 外网端口检测,8007 和 3478 显示"开启"
- 修改
turnserver.conf(3 处内网 IP) - 修改
config.json(2 处公网 IP) - 修改
start_cs2_all.bat(末尾 URL,若工程路径变了还要改路径) - 修改
nginx.conf(若有需要)并 reload - 重启 nginx,运行
start_cs2_all.bat - 外网手机/电脑访问
http://公网IP:8007/cs2/player.html,看到画面
附:为什么这套架构能跑通(原理)
外网用户只能通过 TCP 8007 和 TCP 3478 访问服务器,而 WebRTC 媒体流默认走 UDP,无法穿透。本架构的核心 trick:
-
强制 TURN 中继 : 在
peerConnectionOptions里设iceTransportPolicy: "relay",浏览器和 streamer 都不再尝试 P2P 直连,统一走 TURN 中继。 -
TURN 在外网用 TCP : TURN URL 加
?transport=tcp,浏览器和服务器之间的 TURN 控制+数据通道全在 TCP 3478 这一条连接上完成,不需要单独的 UDP 端口。 -
TURN 在内网用 UDP : coturn 的 relay 地址配成内网 IP (
192.168.0.184),CS2.exe 通过内网 UDP 与 coturn 通信。这部分流量不出网卡,路由器看不见,自然不需要开 UDP 端口。 -
TURN 同时是浏览器和 streamer 的 TURN 客户端: cirrus 把同一份 ICE 配置发给两边,两边都 allocate 到 TURN,所有数据通过 TURN 内部转发,绕开所有 NAT 问题。
性能代价:
- TURN-TCP 比直连 UDP 慢,延迟会高一些(通常 +20~80ms)
- TCP 的拥塞控制对实时视频不友好,丢包重传会导致卡顿放大
- 多个用户同时观看会消耗服务器带宽(每路视频都要 TURN 转一遍)
如果以后服务器能开 UDP 端口,把 peerConnectionOptions 里的 iceTransportPolicy 删掉、?transport=tcp 也删掉,自动会优先走 UDP,体验会好很多。
文件清单速查
C:\Users\Administrator\Desktop\CS2Build\
├── 部署文档.md ← 本文档
├── Windows\
│ ├── start_cs2_all.bat ← 一键启动脚本
│ ├── CS2.exe ← UE5 引擎
│ └── CS2\Samples\PixelStreaming\WebServers\SignallingWebServer\
│ ├── cirrus.js ← 信令服务器(不要改)
│ ├── config.json ← 信令配置(改公网 IP + TURN)
│ ├── turnserver.conf ← TURN 配置(改内网 IP)
│ ├── start_turnserver.bat ← 单独启动 TURN(调试用)
│ └── platform_scripts\cmd\coturn\
│ └── turnserver.exe ← coturn 程序
E:\nginx-1.24.0\
├── nginx.exe
└── conf\nginx.conf ← 加 /cs2/ 和 /cs2ws/ location
start_cs2_all.bat
bash
@echo off
chcp 65001 >nul
title CS2 Pixel Streaming
echo [1/4] Starting coturn TURN server (TCP 3478)...
start "CS2_TURN" cmd /k "cd /d C:\Users\Administrator\Desktop\CS2Build\Windows\CS2\Samples\PixelStreaming\WebServers\SignallingWebServer && platform_scripts\cmd\coturn\turnserver.exe -c turnserver.conf"
timeout /t 3 /nobreak >nul
echo [2/4] Starting Pixel Streaming SignallingWebServer...
start "CS2_Signalling" cmd /k "cd /d C:\Users\Administrator\Desktop\CS2Build\Windows\CS2\Samples\PixelStreaming\WebServers\SignallingWebServer && platform_scripts\cmd\node\node.exe cirrus.js"
timeout /t 5 /nobreak >nul
echo [3/4] Starting CS2.exe...
start "CS2_UE" "C:\Users\Administrator\Desktop\CS2Build\Windows\CS2.exe" -AudioMixer -PixelStreamingIP=127.0.0.1 -PixelStreamingPort=8890 -log -RenderOffScreen
timeout /t 2 /nobreak >nul
echo [4/4] Done.
echo Local URL: http://127.0.0.1:8018/player.html
echo Remote URL: http://公网IP:8007/cs2/player.html
pause