Swoole WebSocket握手与消息发送

Swoole WebSocket:握手言和,消息飞舞,打造你的专属聊天室!

各位观众老爷们,大家好!我是你们的编程老司机,今天咱们不飚车,来聊聊如何用Swoole打造一个风骚的WebSocket服务器!准备好了吗?系好安全带,Let’s go!💨

话说这WebSocket,就好比一座横跨浏览器和服务器之间的桥梁,有了它,服务器就能主动向客户端推送消息,再也不用客户端苦苦哀求(轮询)了。想象一下,股市行情实时更新,聊天消息秒速到达,是不是感觉世界都亮了?✨

Swoole,就是我们建造这座桥梁的超级工具。它就像一位身经百战的工程师,高效、稳定,能让你的WebSocket服务器性能嗖嗖地往上涨!🚀

今天,我们就来深入剖析Swoole WebSocket的握手过程,以及如何让消息像飞鸽传书一样在客户端和服务器之间自由穿梭。

第一章:WebSocket的“比武招亲”——握手过程

WebSocket的握手过程,就好比一场“比武招亲”,客户端和服务器要先“过几招”,确认彼此身份,才能正式“结为连理”。这个过程至关重要,如果握手失败,那就像表白被拒一样,只能黯然离场了。💔

1. 客户端的“抛绣球”——握手请求

首先,客户端会发起一个HTTP Upgrade请求,就像抛出一个绣球,希望能得到服务器的青睐。这个请求长得像这样:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
  • GET /chat: 请求的URI,可以自定义,通常表示聊天室的入口。
  • Host: example.com: 服务器的域名或IP地址。
  • Upgrade: websocket: 表明客户端想要升级协议到WebSocket。
  • Connection: Upgrade: 配合Upgrade使用,告诉服务器要升级连接。
  • Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==: 一个随机的Base64编码字符串,服务器会用它来生成握手响应。
  • Origin: http://example.com: 客户端的域名,用于安全验证。
  • Sec-WebSocket-Version: 13: WebSocket协议的版本。

2. 服务器的“英雄救美”——握手响应

服务器收到绣球后,如果觉得客户端“资质尚可”,就会返回一个HTTP 101 Switching Protocols响应,就像英雄救美一样,接受了客户端的请求。这个响应长得像这样:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • HTTP/1.1 101 Switching Protocols: 状态码101,表示协议切换成功。
  • Upgrade: websocket: 确认协议升级到WebSocket。
  • Connection: Upgrade: 确认连接升级。
  • Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=: 这是握手过程中最关键的部分。服务器会根据客户端发送的Sec-WebSocket-Key生成这个值,用于验证客户端的身份。生成规则是:将Sec-WebSocket-Key加上一个固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后进行SHA-1哈希,再进行Base64编码。

3. “执子之手,与子偕老”——握手完成

客户端收到服务器的握手响应后,会验证Sec-WebSocket-Accept的值是否正确。如果正确,就表示握手成功,客户端和服务器就可以开始愉快的通信了!🎉

如果握手失败,服务器可能会返回400 Bad Request等错误状态码,并关闭连接。

Swoole中的握手事件

在Swoole中,我们可以通过onHandshake事件来处理握手过程。这个事件会在收到客户端的握手请求后触发。

<?php

use SwooleHttpServer;
use SwooleHttpRequest;
use SwooleHttpResponse;

$server = new Server("0.0.0.0", 9501);

$server->on('open', function (SwooleWebSocketServer $server, $request) {
    echo "server: handshake success with fd{$request->fd}n";
});

$server->on('message', function (SwooleWebSocketServer $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}n";
    $server->push($frame->fd, "this is server push message");
});

$server->on('close', function (SwooleWebSocketServer $server, int $fd) {
    echo "client {$fd} closedn";
});

$server->on('handshake', function (Request $request, Response $response) {
    // -------- 检查来源
    $origin = $request->header['origin'] ?? '';
    if (!in_array($origin, [
        'http://localhost:8080',
        'http://127.0.0.1:8080',
        'null' //file://
    ])) {
        $response->status(400);
        $response->end("非法来源");
        return false;
    }

    // websocket握手连接算法验证
    $secWebSocketKey = $request->header['sec-websocket-key'];
    $patten = '#^[+/0-9A-Za-z]{13}==$#';
    if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) {
        $response->status(400);
        $response->end();
        return false;
    }
    echo "握手验证n";
    $key = base64_encode(sha1(
        $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
        true
    ));

    $headers = [
        'Upgrade' => 'websocket',
        'Connection' => 'Upgrade',
        'Sec-WebSocket-Accept' => $key,
        'Sec-WebSocket-Version' => '13',
    ];

    // WebSocket connection to 'ws://127.0.0.1:9501/'
    // failed: Error during WebSocket handshake:
    // Unexpected response code: 400
    foreach ($headers as $key => $val) {
        $response->header($key, $val);
    }

    $response->status(101);
    $response->end();
    return true;
});

$server->start();

在这个例子中,我们首先检查了客户端的来源是否合法,然后验证了Sec-WebSocket-Key的值。如果验证通过,就构造握手响应,并发送给客户端。

握手过程总结

步骤 描述 客户端 服务器
1 发起握手请求 发送包含UpgradeConnectionSec-WebSocket-Key等头部信息的HTTP请求。 接收客户端的握手请求,并从中提取Sec-WebSocket-Key
2 验证身份 验证服务器返回的Sec-WebSocket-Accept值是否正确。 根据Sec-WebSocket-Key生成Sec-WebSocket-Accept,并验证客户端的来源是否合法。
3 发送握手响应 发送包含HTTP/1.1 101 Switching ProtocolsUpgradeConnectionSec-WebSocket-Accept等头部信息的HTTP响应。
4 握手完成 如果Sec-WebSocket-Accept验证通过,则认为握手成功,可以开始通信。 认为握手成功,可以开始通信。

第二章:WebSocket的“飞鸽传书”——消息发送

握手成功后,我们就可以像古代的飞鸽传书一样,在客户端和服务器之间自由地发送消息了。不过,WebSocket的消息发送可比飞鸽传书高效多了,几乎是实时的!🕊️

1. WebSocket的数据帧

WebSocket的消息是通过数据帧(Data Frame)来传输的。每个数据帧都包含以下信息:

  • FIN: 1 bit,表示是否是消息的最后一个帧。如果一个消息被分成多个帧发送,那么只有最后一个帧的FIN位是1,其他帧的FIN位是0。
  • RSV1, RSV2, RSV3: 3 bits,保留位,通常为0。
  • Opcode: 4 bits,表示数据帧的类型。常见的Opcode有:
    • 0x0:延续帧(Continuation Frame),用于分片消息。
    • 0x1:文本帧(Text Frame),表示文本数据。
    • 0x2:二进制帧(Binary Frame),表示二进制数据。
    • 0x8:连接关闭帧(Connection Close Frame),用于关闭连接。
    • 0x9:Ping帧,用于心跳检测。
    • 0xA:Pong帧,用于响应Ping帧。
  • Mask: 1 bit,表示是否使用掩码。如果是客户端发送给服务器的消息,Mask位必须是1。
  • Payload Length: 7 bits, 7+16 bits, or 7+64 bits,表示Payload数据的长度。
  • Masking-key: 32 bits,掩码密钥,只有当Mask位是1时才存在。
  • Payload data: 实际的数据内容。

2. 消息的编码和解码

在发送消息之前,我们需要将数据编码成WebSocket的数据帧格式。在接收消息之后,我们需要将数据帧解码成原始数据。

Swoole已经帮我们处理了这些细节,我们只需要使用push方法发送消息,使用onMessage事件接收消息即可。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('open', function (Server $server, $request) {
    echo "server: handshake success with fd{$request->fd}n";
});

$server->on('message', function (Server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}n";
    $server->push($frame->fd, "this is server push message"); // 发送消息
});

$server->on('close', function (Server $server, int $fd) {
    echo "client {$fd} closedn";
});

$server->start();

在这个例子中,我们使用$server->push($frame->fd, "this is server push message");向客户端发送了一条消息。

在客户端,我们可以使用JavaScript来接收和发送消息:

const ws = new WebSocket('ws://127.0.0.1:9501');

ws.onopen = () => {
  console.log('WebSocket connected');
  ws.send('Hello, Server!'); // 发送消息
};

ws.onmessage = (event) => {
  console.log('Received: ' + event.data);
};

ws.onclose = () => {
  console.log('WebSocket closed');
};

3. 消息的类型

WebSocket可以发送两种类型的数据:文本数据和二进制数据。

  • 文本数据: 通常用于发送文本消息,例如聊天消息、JSON数据等。
  • 二进制数据: 通常用于发送图像、音频、视频等二进制文件。

在Swoole中,我们可以通过opcode属性来判断消息的类型。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('message', function (Server $server, $frame) {
    if ($frame->opcode == WEBSOCKET_OPCODE_TEXT) {
        echo "Received text data: " . $frame->data . "n";
    } elseif ($frame->opcode == WEBSOCKET_OPCODE_BINARY) {
        echo "Received binary data, length: " . strlen($frame->data) . "n";
        // 处理二进制数据
    }
});

$server->start();

4. 消息的分片

如果消息的长度超过了WebSocket允许的最大长度,我们需要将消息分成多个帧发送。这就是消息分片。

在Swoole中,我们可以通过fin属性来判断是否是消息的最后一个帧。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('message', function (Server $server, $frame) {
    static $buffer = '';

    if ($frame->fin) {
        $buffer .= $frame->data;
        echo "Received complete message: " . $buffer . "n";
        $buffer = ''; // 清空缓冲区
    } else {
        $buffer .= $frame->data;
        echo "Received partial message, length: " . strlen($frame->data) . "n";
    }
});

$server->start();

消息发送总结

步骤 描述 服务器 客户端
1 准备消息数据 将要发送的数据编码成WebSocket的数据帧格式。可以使用Swoole提供的push方法,Swoole会自动处理编码细节。 将要发送的数据编码成WebSocket的数据帧格式。可以使用JavaScript提供的send方法,浏览器会自动处理编码细节。
2 发送消息 使用push方法将数据帧发送给客户端。 使用send方法将数据帧发送给服务器。
3 接收消息 onMessage事件中接收客户端发送的数据帧。可以通过opcode属性判断消息的类型,通过fin属性判断是否是消息的最后一个帧。 onmessage事件中接收服务器发送的数据帧。可以通过event.data属性获取消息的内容。
4 处理消息 根据消息的类型和内容,进行相应的处理。例如,如果是文本消息,可以将其显示在聊天界面上;如果是二进制消息,可以将其保存到文件中。如果消息被分片,需要将多个帧组合成完整的消息。 根据消息的类型和内容,进行相应的处理。例如,如果是文本消息,可以将其显示在聊天界面上;如果是二进制消息,可以将其保存到文件中。

第三章:Swoole WebSocket的进阶技巧

掌握了握手和消息发送的基本知识,我们就可以进一步探索Swoole WebSocket的进阶技巧,让我们的服务器更加强大!💪

1. 心跳检测

为了保持WebSocket连接的活跃性,我们可以使用心跳检测。客户端和服务器会定期发送Ping帧和Pong帧,以确认对方是否仍然在线。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('open', function (Server $server, $request) {
    echo "server: handshake success with fd{$request->fd}n";
    // 设置定时器,定期发送Ping帧
    swoole_timer_tick(10000, function ($timer_id, $fd) use ($server) {
        if ($server->exists($fd)) {
            $server->push($fd, "ping", WEBSOCKET_OPCODE_PING);
        } else {
            swoole_timer_clear($timer_id);
        }
    }, $request->fd);
});

$server->on('message', function (Server $server, $frame) {
    if ($frame->opcode == WEBSOCKET_OPCODE_PING) {
        echo "Received ping from {$frame->fd}n";
        // 响应Pong帧
        $server->push($frame->fd, "pong", WEBSOCKET_OPCODE_PONG);
    } elseif ($frame->opcode == WEBSOCKET_OPCODE_PONG) {
        echo "Received pong from {$frame->fd}n";
    } else {
        echo "Received message from {$frame->fd}: {$frame->data}n";
    }
});

$server->start();

2. 身份验证

为了保护WebSocket服务器的安全性,我们可以使用身份验证。客户端需要在握手过程中提供身份验证信息,服务器验证通过后才能建立连接。

<?php

use SwooleHttpServer;
use SwooleHttpRequest;
use SwooleHttpResponse;

$server = new Server("0.0.0.0", 9501);

$server->on('handshake', function (Request $request, Response $response) {
    // 检查token
    $token = $request->get['token'] ?? '';
    if ($token != 'mysecrettoken') {
        $response->status(401);
        $response->end("Unauthorized");
        return false;
    }

    // websocket握手连接算法验证
    $secWebSocketKey = $request->header['sec-websocket-key'];
    $patten = '#^[+/0-9A-Za-z]{13}==$#';
    if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) {
        $response->status(400);
        $response->end();
        return false;
    }
    echo "握手验证n";
    $key = base64_encode(sha1(
        $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
        true
    ));

    $headers = [
        'Upgrade' => 'websocket',
        'Connection' => 'Upgrade',
        'Sec-WebSocket-Accept' => $key,
        'Sec-WebSocket-Version' => '13',
    ];

    foreach ($headers as $key => $val) {
        $response->header($key, $val);
    }

    $response->status(101);
    $response->end();
    return true;
});

$server->on('open', function (SwooleWebSocketServer $server, $request) {
    echo "server: handshake success with fd{$request->fd}n";
});

$server->on('message', function (SwooleWebSocketServer $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}n";
    $server->push($frame->fd, "this is server push message");
});

$server->on('close', function (SwooleWebSocketServer $server, int $fd) {
    echo "client {$fd} closedn";
});

$server->start();

3. 广播消息

我们可以将消息广播给所有连接到服务器的客户端。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('message', function (Server $server, $frame) {
    $message = "Broadcast: " . $frame->data;
    foreach ($server->connections as $fd) {
        $server->push($fd, $message);
    }
});

$server->start();

4. 使用框架

为了简化开发,我们可以使用一些基于Swoole的WebSocket框架,例如Hyperf、Mix等。这些框架提供了更加高级的功能,例如路由、中间件、依赖注入等。

进阶技巧总结

技巧 描述
心跳检测 定期发送Ping帧和Pong帧,以保持WebSocket连接的活跃性。
身份验证 在握手过程中验证客户端的身份,以保护WebSocket服务器的安全性。
广播消息 将消息广播给所有连接到服务器的客户端。
使用框架 使用基于Swoole的WebSocket框架,可以简化开发,并提供更加高级的功能。

第四章:打造你的专属聊天室

有了以上知识,我们就可以开始打造自己的专属聊天室了!想象一下,你可以和朋友们在自己的聊天室里畅所欲言,岂不美哉?🍻

1. 前端页面

首先,我们需要一个美观的聊天室页面。可以使用HTML、CSS和JavaScript来构建。

<!DOCTYPE html>
<html>
<head>
  <title>My Chat Room</title>
  <style>
    #messages {
      height: 300px;
      overflow-y: scroll;
      border: 1px solid #ccc;
      padding: 10px;
    }
  </style>
</head>
<body>
  <h1>My Chat Room</h1>
  <div id="messages"></div>
  <input type="text" id="messageInput">
  <button id="sendButton">Send</button>

  <script>
    const ws = new WebSocket('ws://127.0.0.1:9501');
    const messagesDiv = document.getElementById('messages');
    const messageInput = document.getElementById('messageInput');
    const sendButton = document.getElementById('sendButton');

    ws.onopen = () => {
      console.log('WebSocket connected');
    };

    ws.onmessage = (event) => {
      const message = document.createElement('p');
      message.textContent = event.data;
      messagesDiv.appendChild(message);
      messagesDiv.scrollTop = messagesDiv.scrollHeight; // 滚动到最底部
    };

    ws.onclose = () => {
      console.log('WebSocket closed');
    };

    sendButton.addEventListener('click', () => {
      const messageText = messageInput.value;
      ws.send(messageText);
      messageInput.value = '';
    });
  </script>
</body>
</html>

2. 后端服务器

然后,我们需要一个Swoole WebSocket服务器来处理客户端的连接和消息。

<?php

use SwooleWebSocketServer;

$server = new Server("0.0.0.0", 9501);

$server->on('message', function (Server $server, $frame) {
    $message = $frame->data;
    foreach ($server->connections as $fd) {
        $server->push($fd, $message);
    }
});

$server->start();

3. 运行聊天室

将前端页面保存为index.html,将后端服务器代码保存为server.php

  1. 启动Swoole WebSocket服务器:php server.php
  2. 在浏览器中打开index.html

现在,你就可以在聊天室里发送和接收消息了!🎉

4. 完善聊天室

你可以进一步完善聊天室的功能,例如:

  • 显示用户的昵称
  • 支持私聊
  • 添加表情
  • 保存聊天记录

打造专属聊天室总结

  1. 设计前端页面: 使用HTML、CSS和JavaScript创建一个美观的聊天室页面,包括消息显示区域、输入框和发送按钮。
  2. 编写后端服务器: 使用Swoole WebSocket服务器来处理客户端的连接和消息。
  3. 运行聊天室: 启动Swoole WebSocket服务器,并在浏览器中打开前端页面。
  4. 完善功能: 添加更多功能,例如显示用户昵称、支持私聊、添加表情、保存聊天记录等,使你的聊天室更加完善。

总结

今天,我们一起探索了Swoole WebSocket的握手过程和消息发送机制,并学习了一些进阶技巧。希望这些知识能帮助你打造出自己的专属聊天室,或者构建更加强大的实时应用!

记住,编程就像一场探险,只有不断学习和实践,才能发现更多的乐趣!加油!💪

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注