18、WebSocket 与 WebRTC:WebSocket 协议实现、WebRTC 的 PeerConnection、音视频采集与编码、NAT 穿透与 STUN/TURN

WebSocket 和 WebRTC,这两个东西经常被放在一起聊。但说实话,它们解决的问题完全不同。WebSocket 是「我要跟服务器保持一条实时通道」,WebRTC 是「我要跟另一个浏览器直接传视频」。我当年刚接触时也混淆过,后来踩了坑才明白——一个走 TCP,一个走 UDP,底层逻辑都不一样。

WebSocket 协议实现

WebSocket 说白了就是 HTTP 升级上来的。你想想看,浏览器先发一个普通的 HTTP 请求,带个 Upgrade: websocket 头,服务器同意后,这条连接就变成了全双工通道。

我在项目中遇到过一个问题:有些老旧的代理服务器不认识 Upgrade 头,直接就把请求掐断了。嗯,这里要注意,WebSocket 的握手必须走 101 状态码,否则就是失败。

核心流程:

  • 客户端发 GET 请求,带 Upgrade、Connection、Sec-WebSocket-Key
  • 服务器计算 Sec-WebSocket-Accept,返回 101
  • 之后双方通过 frame 通信,不再走 HTTP

数据帧的结构其实很简单。一个 frame 包含 opcode、payload length、masking key 和实际数据。我习惯用 0x81 表示一个文本帧,0x89 表示 ping 帧。Android 上实现 WebSocket 客户端,我一般直接用 OkHttp 的 WebSocket 支持,底层已经帮你处理了 masking 和分片。

// 一个简单的 WebSocket 客户端示例
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
    .url("wss://example.com/chat")
    .build();
WebSocket ws = client.newWebSocket(request, new WebSocketListener() {
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        webSocket.send("Hello, server!");
    }
    @Override
    public void onMessage(WebSocket webSocket, String text) {
        Log.d("WS", "收到消息: " + text);
    }
});

避坑指南:我曾经在弱网环境下发现 WebSocket 断连后不会自动重连。OkHttp 的 WebSocket 默认没有心跳机制,你需要自己实现 ping/pong 来保活。建议每 30 秒发一次 ping,超时 10 秒没收到 pong 就主动重连。

WebRTC 的 PeerConnection

WebRTC 的核心是 PeerConnection。它负责在两个浏览器之间建立一条点对点的通道。你想想看,没有中间服务器,两个浏览器怎么知道对方在哪?这就是信令服务器的活儿了。

PeerConnection 的工作流程分三步:

  1. 创建 Offer/Answer:发起方创建 SDP offer,接收方回复 SDP answer
  2. ICE 候选交换:双方收集自己的候选地址,通过信令通道交换
  3. 连接建立:ICE 协商出最优路径,DTLS 握手,SRTP 加密

我在 Android 上实现时,用的是 org.webrtc.PeerConnection。这个类需要配合 PeerConnectionFactory 来创建。记得初始化时一定要设置 FieldTrials,否则有些特性默认是关的。

// 创建 PeerConnection 的典型流程
PeerConnectionFactory factory = PeerConnectionFactory.builder()
    .setOptions(new PeerConnectionFactory.Options())
    .createPeerConnectionFactory();

PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(
    Arrays.asList(new PeerConnection.IceServer("stun:stun.l.google.com:19302"))
);

PeerConnection pc = factory.createPeerConnection(config, new MyObserver());

注意:PeerConnection 的创建必须在主线程,但回调可能在其他线程。我遇到过回调里直接更新 UI 导致崩溃的情况。建议所有 UI 操作都 post 到主线程。

音视频采集与编码

音视频采集在 Android 上主要靠 Camera2 和 AudioRecord。但 WebRTC 封装了一套自己的采集接口,叫 VideoCapturerAudioSource

我习惯用 Camera2Enumerator 来获取摄像头列表。注意,有些设备的前置摄像头不支持 720p 以上分辨率,采集前最好先检查一下。

// 采集视频
Camera2Enumerator enumerator = new Camera2Enumerator(context);
String[] deviceNames = enumerator.getDeviceNames();
VideoCapturer capturer = enumerator.createCapturer(deviceNames[0], null);

// 创建视频源
VideoSource videoSource = factory.createVideoSource(capturer);
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
capturer.startCapture(1280, 720, 30);

编码这块,WebRTC 默认用 VP8 或 VP9。Android 上硬件编码器支持 H.264,但不同厂商的实现有差异。我曾经在小米手机上遇到 H.264 编码出来的画面全是绿色条纹,后来换成 VP8 就好了。嗯,这里建议优先用 VP8,兼容性最好。

编码参数建议:

  • 分辨率:720p 起步,1080p 看网络状况
  • 帧率:25-30fps,不要超过摄像头支持上限
  • 码率:视频 1-2 Mbps,音频 32-64 kbps
  • 关键帧间隔:2 秒一个 IDR 帧

NAT 穿透与 STUN/TURN

这是 WebRTC 里最头疼的部分。两个设备都在内网,怎么找到对方?STUN 和 TURN 就是干这个的。

STUN:客户端问 STUN 服务器「我的公网地址是什么?」。服务器返回公网 IP 和端口。如果 NAT 是 Full Cone 或 Restricted Cone,这个地址就能直接连通。

TURN:如果 NAT 是 Symmetric 类型,STUN 就没用了。这时候需要 TURN 服务器做中继。所有数据都经过 TURN 转发,延迟会高一些,但至少能通。

我在项目中遇到过最坑的情况:某运营商的 NAT 是 Symmetric 且端口分配随机,STUN 拿到的地址根本连不上。最后只能强制走 TURN。建议生产环境至少配两个 STUN 和一个 TURN。

// 配置 ICE 服务器
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
iceServers.add(PeerConnection.IceServer.builder("turn:turn.example.com:3478")
    .setUsername("user")
    .setPassword("pass")
    .createIceServer());

PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);

经验之谈:我曾经调试一个跨运营商通话,发现 ICE 候选里出现了 IPv6 地址。有些 NAT 设备对 IPv6 支持不好,导致连接失败。建议在 RTCConfiguration 里设置 iceTransportsTypeRELAY 来强制走 TURN,或者设置 disableIPv6 为 true。

WebRTC 连接建立流程 浏览器 A 浏览器 B 信令服务器 STUN 服务器 TURN 服务器 NAT A NAT B SDP Offer SDP Answer 查询公网地址 查询公网地址 中继数据(NAT 穿透失败时) P2P 直连(ICE 协商成功) 信令 STUN TURN P2P

这张图展示了 WebRTC 的完整连接流程。先通过信令服务器交换 SDP,然后 STUN 查询公网地址,最后 ICE 协商出最优路径。如果 NAT 太严格,就退化为 TURN 中继。

性能警告:TURN 中继会消耗服务器带宽。一个 720p 视频流大约需要 2 Mbps 上行和 2 Mbps 下行。如果你的用户量很大,TURN 服务器的带宽成本会很高。我见过一个项目因为没算好带宽,一个月 TURN 流量费花了十几万。

好了,WebSocket 和 WebRTC 的核心内容就这些。WebSocket 适合做信令和消息推送,WebRTC 适合做实时音视频。两者配合使用,就能构建出完整的实时通信系统。

公众号:蓝海资料掘金营,微信deep3321