作为一种在单个 TCP 连接上提供全双工通信的协议,WebSocket 彻底改变了传统 HTTP 协议的请求-响应模式,成为实时 Web 应用的核心技术。对于 PHP 和 C# 开发者而言,掌握 WebSocket 不仅能构建实时聊天、实时数据监控等应用,更是工业物联网(如 SCADA 系统与 Web 前端交互)、协作工具等场景的基础。本文将从协议细节到实战实现进行全面讲解。

一、WebSocket 核心原理

1. 与 HTTP 的本质区别

特性 HTTP WebSocket
通信模式 单向(客户端请求→服务器响应) 双向(客户端↔服务器自由通信)
连接类型 短连接(请求完成后关闭) 长连接(一次握手后保持连接)
数据传输效率 每次请求携带完整 HTTP 头(开销大) 仅握手阶段使用 HTTP,后续传输无冗余
实时性 差(依赖轮询/长轮询,有延迟) 好(数据即时推送)
适用场景 页面加载、API 调用等非实时场景 聊天、实时监控、游戏等实时场景

2. 握手过程(Handshake)

WebSocket 基于 HTTP 协议升级而来,整个握手过程如下:

  1. 客户端发起升级请求(HTTP GET 方法):

    GET /ws-endpoint HTTP/1.1
    Host: example.com
    Upgrade: websocket                  // 声明要升级为 WebSocket 协议
    Connection: Upgrade                 // 配合 Upgrade 使用
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==  // 随机生成的 Base64 字符串(用于验证)
    Sec-WebSocket-Version: 13           // 协议版本(当前主流为 13)
  2. 服务器响应确认升级

    HTTP/1.1 101 Switching Protocols     // 101 状态码表示协议切换
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  // 基于客户端 Key 计算的验证值
  3. 验证机制
    服务器通过以下算法计算 Sec-WebSocket-Accept

    1. 将客户端的 Sec-WebSocket-Key 与固定 GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接
    2. 对拼接结果进行 SHA-1 哈希计算
    3. 将哈希结果进行 Base64 编码,得到 Sec-WebSocket-Accept

    客户端验证该值是否正确,确认服务器支持 WebSocket 后,连接建立。

3. 数据帧格式(Frame)

WebSocket 数据通过帧(Frame)传输,帧结构是开发者处理数据的核心(尤其在自定义实现时):

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |                               |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 126  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

关键字段解析:

  • FIN(1位):1 表示当前帧是消息的最后一帧,0 表示后续还有帧(用于分片大消息)。
  • opcode(4位):数据类型标识,常用值:
    • 0x0:延续帧(分片消息的后续帧)
    • 0x1:文本帧(UTF-8 编码)
    • 0x2:二进制帧
    • 0x8:关闭连接帧
    • 0x9/0xA:Ping/Pong 帧(心跳检测)
  • MASK(1位):客户端发送的帧必须设为 1(需掩码加密),服务器发送的帧设为 0。
  • Payload length(7/16/64位):有效载荷长度,规则:
    • 0-125:直接表示长度
    • 126:后续 2 字节表示 16 位无符号整数
    • 127:后续 8 字节表示 64 位无符号整数
  • Masking-key(32位):仅客户端发送的帧有,用于对 payload 进行异或解密。
  • Payload Data:实际传输的数据(文本或二进制)。

4. 核心特性

  • 全双工:客户端和服务器可同时发送数据,无需等待对方响应。
  • 持久连接:一次握手后保持连接,避免 HTTP 反复建立连接的开销。
  • 轻量传输:数据帧头部最小仅 2 字节,远小于 HTTP 头。
  • 跨域支持:可通过 CORS 机制实现跨域通信(握手阶段验证 Origin)。
  • 心跳机制:通过 Ping/Pong 帧检测连接状态(一方发 Ping,另一方必须回 Pong)。

二、PHP 中的 WebSocket 实现

PHP 由于其传统的“请求-结束”生命周期,原生不适合长连接,但可通过常驻内存的 CLI 模式 + 扩展实现 WebSocket 服务器。推荐使用 Ratchet 库(基于 ReactPHP,最成熟的 PHP WebSocket 框架)。

1. 环境准备

# 安装 Composer(依赖管理工具)
curl -sS https://getcomposer.org/installer | php

# 创建项目并安装 Ratchet
mkdir php-websocket && cd php-websocket
composer require cboden/ratchet

2. 实现 WebSocket 服务器( echo 服务示例)

创建 server.php

<?php
require __DIR__ . '/vendor/autoload.php';

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

// 实现消息组件接口(核心逻辑)
class EchoServer implements MessageComponentInterface {
    protected $clients;  // 存储所有连接的客户端

    public function __construct() {
        $this->clients = new \SplObjectStorage;  // 高效存储对象的容器
    }

    // 客户端连接时触发
    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        echo "新客户端连接:{$conn->resourceId}\n";
    }

    // 收到客户端消息时触发
    public function onMessage(ConnectionInterface $from, $msg) {
        $numRecv = count($this->clients) - 1;
        echo sprintf(
            "客户端 %d 发送消息:'%s'(将广播给 %d 个客户端)\n",
            $from->resourceId,
            $msg,
            $numRecv
        );

        // 广播消息给所有其他客户端
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                $client->send($msg);  // 发送消息
            }
        }
    }

    // 客户端断开连接时触发
    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
        echo "客户端 {$conn->resourceId} 断开连接\n";
    }

    // 发生错误时触发
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "错误:{$e->getMessage()}\n";
        $conn->close();
    }
}

// 启动服务器
$server = new \Ratchet\App('localhost', 8080, '0.0.0.0');  // 地址、端口、绑定IP
$server->route('/echo', new EchoServer);  // 注册路由(客户端连接地址:ws://localhost:8080/echo)
echo "WebSocket 服务器启动:ws://localhost:8080/echo\n";
$server->run();

3. 运行服务器

php server.php

4. 实现 PHP 客户端(可选)

创建 client.php(使用 textalk/websocket 库):

composer require textalk/websocket
<?php
require __DIR__ . '/vendor/autoload.php';

use WebSocket\Client;

$client = new Client("ws://localhost:8080/echo");

// 发送消息
$client->send("Hello from PHP client!");

// 接收消息(阻塞等待)
$response = $client->receive();
echo "收到响应:{$response}\n";

// 关闭连接
$client->close();

5. 浏览器客户端(通用测试工具)

创建 client.html,通过原生 JavaScript 测试:

<!DOCTYPE html>
<html>
<body>
    <input type="text" id="messageInput" placeholder="输入消息">
    <button onclick="sendMessage()">发送</button>
    <div id="messages"></div>

    <script>
        // 连接 WebSocket 服务器
        const ws = new WebSocket('ws://localhost:8080/echo');

        // 连接成功时
        ws.onopen = () => {
            console.log('连接已建立');
        };

        // 收到消息时
        ws.onmessage = (event) => {
            const messages = document.getElementById('messages');
            messages.innerHTML += `<div>收到:${event.data}</div>`;
        };

        // 连接关闭时
        ws.onclose = () => {
            console.log('连接已关闭');
        };

        // 发送消息
        function sendMessage() {
            const input = document.getElementById('messageInput');
            ws.send(input.value);
            input.value = '';
        }
    </script>
</body>
</html>

6. PHP 实现注意事项

  • 性能限制:PHP 单线程模型处理大量并发连接时性能较弱,适合中小型应用(建议并发不超过 1000)。
  • 部署方式:需通过 Supervisor 等工具确保服务器进程常驻(防止意外退出)。
  • 扩展选择:除 Ratchet 外,可考虑 Workerman(更轻量,性能更好)。

三、C# 中的 WebSocket 实现

C# 对 WebSocket 支持非常完善,尤其是 .NET Framework 4.5+ 和 .NET Core 提供了原生 API,适合构建高性能服务器和客户端。

1. ASP.NET Core 中实现 WebSocket 服务器

ASP.NET Core 内置 WebSocket 中间件,无需额外依赖:

创建 Program.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Net.WebSockets;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 启用 WebSocket 中间件
app.UseWebSockets();

// 处理 WebSocket 连接的端点
app.Map("/ws", async context => {
    // 检查请求是否为 WebSocket 升级请求
    if (!context.WebSockets.IsWebSocketRequest) {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        return;
    }

    // 接受 WebSocket 连接
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    Console.WriteLine("客户端已连接");

    var buffer = new byte[1024 * 4];  // 缓冲区(4KB)
    WebSocketReceiveResult result;

    // 循环接收消息
    do {
        // 接收客户端消息
        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

        if (result.MessageType == WebSocketMessageType.Text) {
            // 解析文本消息(UTF-8 编码)
            var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            Console.WriteLine($"收到消息:{message}");

            // 构造响应消息
            var response = Encoding.UTF8.GetBytes($"服务器已收到:{message}");

            // 发送响应(文本帧)
            await webSocket.SendAsync(
                new ArraySegment<byte>(response, 0, response.Length),
                WebSocketMessageType.Text,
                endOfMessage: true,
                CancellationToken.None
            );
        } 
        else if (result.MessageType == WebSocketMessageType.Close) {
            // 客户端请求关闭连接,发送关闭帧响应
            await webSocket.CloseAsync(
                WebSocketCloseStatus.NormalClosure,
                "正常关闭",
                CancellationToken.None
            );
            Console.WriteLine("客户端已断开连接");
        }
    } while (!result.CloseStatus.HasValue);  // 直到收到关闭帧
});

app.Run("http://localhost:5000");  // 启动服务器(WebSocket 地址:ws://localhost:5000/ws)

2. 运行服务器

dotnet new web -n CSharpWebSocketServer
cd CSharpWebSocketServer
# 替换 Program.cs 内容后运行
dotnet run

3. C# 客户端实现(.NET 6+)

创建控制台客户端 WebSocketClient.cs

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class WebSocketClient {
    static async Task Main(string[] args) {
        using var client = new ClientWebSocket();
        var cts = new CancellationTokenSource();  // 用于取消操作

        try {
            // 连接服务器
            await client.ConnectAsync(new Uri("ws://localhost:5000/ws"), cts.Token);
            Console.WriteLine("已连接到服务器");

            // 启动接收消息的后台任务
            var receiveTask = ReceiveMessagesAsync(client, cts.Token);

            // 发送消息
            while (true) {
                Console.Write("输入消息(输入 exit 退出):");
                var input = Console.ReadLine();
                if (input?.Equals("exit", StringComparison.OrdinalIgnoreCase) ?? false) {
                    break;
                }

                var buffer = Encoding.UTF8.GetBytes(input);
                await client.SendAsync(
                    new ArraySegment<byte>(buffer),
                    WebSocketMessageType.Text,
                    endOfMessage: true,
                    cts.Token
                );
            }

            // 关闭连接
            await client.CloseAsync(
                WebSocketCloseStatus.NormalClosure,
                "客户端主动关闭",
                cts.Token
            );
            cts.Cancel();  // 取消接收任务
            await receiveTask;
        } 
        catch (Exception ex) {
            Console.WriteLine($"错误:{ex.Message}");
        }
    }

    // 异步接收消息
    static async Task ReceiveMessagesAsync(ClientWebSocket client, CancellationToken cancellationToken) {
        var buffer = new byte[1024 * 4];
        while (!cancellationToken.IsCancellationRequested) {
            var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
            if (result.MessageType == WebSocketMessageType.Text) {
                var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                Console.WriteLine($"收到服务器消息:{message}");
            } 
            else if (result.MessageType == WebSocketMessageType.Close) {
                Console.WriteLine("服务器请求关闭连接");
                break;
            }
        }
    }
}

4. C# 实现注意事项

  • 异步编程:WebSocket 操作必须使用异步方法(ReceiveAsync/SendAsync),避免阻塞线程。
  • 并发处理:ASP.NET Core 服务器可自动处理多客户端并发(基于线程池),适合高并发场景。
  • 框架选择:除原生 API 外,可使用 SignalR(封装了 WebSocket 等实时通信方式,简化开发)。
  • 安全性:生产环境需使用 wss://(WebSocket Secure,基于 TLS 加密),配置代码:
    // 启用 HTTPS(开发环境可使用自签证书)
    app.Run("https://localhost:5001");  // WebSocket 地址:wss://localhost:5001/ws

四、WebSocket 高级应用与最佳实践

1. 安全性增强

  • 使用 wss://:通过 TLS 1.2+ 加密传输,防止数据被窃听或篡改。
  • 身份验证:在握手阶段通过 Cookie、Token(如 JWT)验证客户端身份:
    // ASP.NET Core 中验证 Token 示例
    app.Map("/ws", async context => {
      if (!context.Request.Headers.TryGetValue("Authorization", out var token)) {
          context.Response.StatusCode = 401;
          return;
      }
      // 验证 token 逻辑...
      // 验证通过后再接受 WebSocket 连接
    });
  • 限制连接频率:防止恶意客户端频繁建立连接(可使用 Redis 记录 IP 连接数)。

2. 心跳机制实现

客户端和服务器需定期发送 Ping 帧检测连接状态:

// 服务器端心跳示例(每 30 秒发送一次 Ping)
var pingTimer = new Timer(async _ => {
    if (webSocket.State == WebSocketState.Open) {
        await webSocket.SendAsync(
            new ArraySegment<byte>(Array.Empty<byte>()),
            WebSocketMessageType.Ping,
            endOfMessage: true,
            CancellationToken.None
        );
    }
}, null, 30000, 30000);  // 30 秒间隔

3. 消息分片(大消息处理)

当消息超过最大帧长度时,需分片传输(通过 FIN 位控制):

// 发送大消息分片示例
var largeMessage = Encoding.UTF8.GetBytes(new string('a', 1024 * 1024));  // 1MB 消息
int offset = 0;
int chunkSize = 4096;  // 每片 4KB

while (offset < largeMessage.Length) {
    int remaining = largeMessage.Length - offset;
    int sendSize = Math.Min(remaining, chunkSize);
    bool isLastFrame = offset + sendSize == largeMessage.Length;

    await webSocket.SendAsync(
        new ArraySegment<byte>(largeMessage, offset, sendSize),
        WebSocketMessageType.Text,
        isLastFrame,
        CancellationToken.None
    );

    offset += sendSize;
}

4. 与其他实时技术的对比

技术 优势 劣势 适用场景
WebSocket 全双工、低延迟、低开销 服务器需维护长连接,资源消耗较高 实时聊天、游戏、协作工具
长轮询(Long Poll) 兼容性好(所有浏览器支持) 延迟高、服务器开销大 对实时性要求不高的场景
Server-Sent Events 服务器单向推送,实现简单 仅服务器→客户端,不支持客户端推送 股票行情、新闻推送等单向场景
MQTT 轻量、适合物联网、支持发布-订阅模式 需额外部署 MQTT broker 物联网设备通信(如传感器数据上报)

五、总结

WebSocket 作为实时 Web 通信的事实标准,其全双工、低延迟特性使其成为现代应用的关键技术。对于 PHP 开发者,Ratchet 库提供了便捷的实现方式,但需注意性能限制;对于 C# 开发者,ASP.NET Core 原生 API 和 SignalR 框架可轻松构建高性能服务器。

实际开发中,需根据场景选择合适的技术栈(如高频实时场景优先 C#),并重视安全性(wss + 身份验证)、连接稳定性(心跳机制)和可扩展性(负载均衡)。掌握 WebSocket 不仅能提升应用的实时交互体验,更是通往工业互联网、实时协作等领域的必备技能。