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 | 发起握手请求 | 发送包含Upgrade 、Connection 、Sec-WebSocket-Key 等头部信息的HTTP请求。 |
接收客户端的握手请求,并从中提取Sec-WebSocket-Key 。 |
2 | 验证身份 | 验证服务器返回的Sec-WebSocket-Accept 值是否正确。 |
根据Sec-WebSocket-Key 生成Sec-WebSocket-Accept ,并验证客户端的来源是否合法。 |
3 | 发送握手响应 | 无 | 发送包含HTTP/1.1 101 Switching Protocols 、Upgrade 、Connection 、Sec-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
。
- 启动Swoole WebSocket服务器:
php server.php
- 在浏览器中打开
index.html
。
现在,你就可以在聊天室里发送和接收消息了!🎉
4. 完善聊天室
你可以进一步完善聊天室的功能,例如:
- 显示用户的昵称
- 支持私聊
- 添加表情
- 保存聊天记录
打造专属聊天室总结
- 设计前端页面: 使用HTML、CSS和JavaScript创建一个美观的聊天室页面,包括消息显示区域、输入框和发送按钮。
- 编写后端服务器: 使用Swoole WebSocket服务器来处理客户端的连接和消息。
- 运行聊天室: 启动Swoole WebSocket服务器,并在浏览器中打开前端页面。
- 完善功能: 添加更多功能,例如显示用户昵称、支持私聊、添加表情、保存聊天记录等,使你的聊天室更加完善。
总结
今天,我们一起探索了Swoole WebSocket的握手过程和消息发送机制,并学习了一些进阶技巧。希望这些知识能帮助你打造出自己的专属聊天室,或者构建更加强大的实时应用!
记住,编程就像一场探险,只有不断学习和实践,才能发现更多的乐趣!加油!💪