この記事では、php ソケットに関する関連知識を紹介します。主に、php ネイティブ ソケットを使用して簡単な Web チャット ルームを実装する方法を紹介します。興味のある方は以下をご覧ください。皆様のお役に立てれば幸いです。
序文
この記事では、php ネイティブ ソケットを使用して単純な Web チャット ルームを実装します。最終コードは次のとおりです。記事の一番下。
なんなら、これがこのシリーズの最後の記事になるはずです。このシリーズの記事を書いたときは、とても簡単なことだと思っていましたが、ここ数回の記事を書いているうちに、ほとんど読んでしまいました。 Workerman のコードを使用します。ですから、あまり野心的になることはありませんが、自分で試してみてください。何かを本当に理解していることを証明するには、それを書き留めるのが最善です。
WebSocket の概要
webSocket プロトコル 2008 年に誕生し、2011 年に国際標準となったネットワーク通信プロトコルで、通信規格は RFC6455 で定められており、現在はすべてのブラウザでサポートされています。 webSocket は、HTML5 が提供し始めた 1 つの TCP 接続上で全二重通信を行うためのプロトコルで、サーバーからクライアントにメッセージをアクティブにプッシュしたり、クライアントもサーバーにメッセージをアクティブに送信したりできます。
WebSocket は通信プロトコルの仕様を規定しており、ハンドシェイク機構によりクライアント (ブラウザ) とサーバー (Web サーバー) 間で tcp のような接続を確立し、CS 通信を容易にします。
WebSocket が必要な理由
HTTP プロトコルは、ステートレス、コネクションレス、一方向のアプリケーション層プロトコルです。 request=>response
モデルを採用しています。通信リクエストはクライアントによってのみ開始でき、サーバーはリクエストに応答します。この通信モデルには欠点があります。サーバーが積極的に送信することは不可能です。クライアントへのメッセージ。メッセージを開始します。従来の HTTP リクエストの同時実行機能は、複数の TCP 接続を開始してサーバーに同時にアクセスすることによって実現されます。Websocket を使用すると、ws 接続で複数のリクエストを同時に発行できます。つまり、A リクエストが送信された後、A レスポンスはまだ到着していません。引き続き B リクエストを発行できます。 TCP のスロー スタート機能と接続自体のハンドシェイク損失により、WebSocket プロトコルのこの機能により効率が大幅に向上しました。
クライアントとサーバーのハンドシェイク
websocket プロトコルでは、接続前にハンドシェイクが必要です[^2]。通常、次のハンドシェイク メソッドがあります。 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
計算を実行し、その結果を使用します。base64 で暗号化されています。
、そして最終的にクライアントに返されます
GET /chat HTTP/1.1Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
クライアントは次のデータを返す必要があります。 :
HTTP/1.1 101 Switching Protocols Upgrade: websocket Sec-WebSocket-Version: 13Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
このプロトコルに従って PHP を通じて実装します:
<?php $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true); socket_bind($socket, 0, 8888); socket_listen($socket); while (true) { $conn_sock = socket_accept($socket); $request = socket_read($conn_sock, 102400); $new_key = getShaKey($request); $response = "HTTP/1.1 101 Switching Protocols\r\n"; $response .= "Upgrade: websocket\r\n"; $response .= "Sec-WebSocket-Version: 13\r\n"; $response .= "Connection: Upgrade\r\n"; $response .= "Sec-WebSocket-Accept: {$new_key}\r\n\r\n"; socket_write($conn_sock, $response); } function getShaKey($request) { // 获取 Sec-WebSocket-key preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match); // 拼接 key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 $new_key = trim($match[1]) . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 对字符串做 `SHA1` 计算,再把得到的结果通过 `base64` 加密 return base64_encode(sha1($new_key, true)); }
関連する構文の説明は
以前の記事を参照してください。この記事では詳しくは紹介しません。 フロントエンド テストを使用し、ブラウザ コンソール (コンソール) のいずれかを開き、次のコンテンツを入力します。返された WebSocket オブジェクトのreadyState は 1 で、ハンドシェイクが成功したことを意味します。これはフロントエンドです。この記事では詳しくは紹介しませんが、
新人チュートリアルデータの送信と受信を参照してください。 使用 websocket 协议传输协议需要遵循特定的格式规范,详情请参考 datatracker.ietf.org/doc/html/rfc6... 为了方便,这里直接贴出加解密代码,以下代码借鉴与 workerman 的 我们修改刚才 客户端与服务端握手 阶段的代码,修改后全代码全文如下,该段代码实现了将客户端发送的消息转为大写后返回给客户端(当然只是为了演示): 使用 在线测试工具 进行测试,可以看到消息已经可以正常发送接收,接下来的文章将继续优化代码,实现简易聊天室,敬请关注: 我们紧接着上文的代码继续优化,以实现简易的web聊天室 多路复用 其实就是加一下 最终成果演示 我们将上述代码改造成类,并在类变量储存用户信息,添加消息处理等逻辑,最后贴出代码,建议保存下来自己尝试一下,也许会有全新的认知,后端代码: 前端代码如下(前端内容不在本文讨论范围之内,具体可参考 菜鸟教程): [^1]:是通讯传输的一个术语。 通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合 推荐学习:《PHP视频教程》 以上がWebSocketチャットルームを実現するPHP+Socketシリーズの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。src/Protocols/Websocket.php
文件:// 解码客户端发送的消息
function decode($buffer)
{
$len = \ord($buffer[1]) & 127;
if ($len === 126) {
$masks = \substr($buffer, 4, 4);
$data = \substr($buffer, 8);
} else {
if ($len === 127) {
$masks = \substr($buffer, 10, 4);
$data = \substr($buffer, 14);
} else {
$masks = \substr($buffer, 2, 4);
$data = \substr($buffer, 6);
}
}
$dataLength = \strlen($data);
$masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
return $data ^ $masks;
}
// 编码发送给客户端的消息
function encode($buffer)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = \strlen($buffer);
$first_byte = "\x81";
if ($len <= 125) {
$encode_buffer = $first_byte . \chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
}
}
return $encode_buffer;
}
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($socket, 0, 8888);
socket_listen($socket);
while (true) {
$conn_sock = socket_accept($socket);
$request = socket_read($conn_sock, 102400);
$new_key = getShaKey($request);
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response .= "Upgrade: websocket\r\n";
$response .= "Sec-WebSocket-Version: 13\r\n";
$response .= "Connection: Upgrade\r\n";
$response .= "Sec-WebSocket-Accept: {$new_key}\r\n\r\n";
// 发送握手数据
socket_write($conn_sock, $response);
// 新增内容,获取客户端发送的消息并转为大写还给客户端
$msg = socket_read($conn_sock, 102400);
socket_write($conn_sock, encode(strtoupper(decode($msg))));
}
function getShaKey($request)
{
// 获取 Sec-WebSocket-key
preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);
// 拼接 key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
$new_key = trim($match[1]) . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// 对字符串做 `SHA1` 计算,再把得到的结果通过 `base64` 加密
return base64_encode(sha1($new_key, true));
}
function decode($buffer)
{
$len = \ord($buffer[1]) & 127;
if ($len === 126) {
$masks = \substr($buffer, 4, 4);
$data = \substr($buffer, 8);
} else {
if ($len === 127) {
$masks = \substr($buffer, 10, 4);
$data = \substr($buffer, 14);
} else {
$masks = \substr($buffer, 2, 4);
$data = \substr($buffer, 6);
}
}
$dataLength = \strlen($data);
$masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
return $data ^ $masks;
}
function encode($buffer)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = \strlen($buffer);
$first_byte = "\x81";
if ($len <= 125) {
$encode_buffer = $first_byte . \chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
}
}
return $encode_buffer;
}
实现web聊天室
socket_select()
函数 ,本文就不写原理与语法了,详情可参考 之前的文章,以下代码修改自前文 WebSocketチャットルームを実現するPHP+Socketシリーズ...
socket_listen($socket);
+$sockets[] = $socket;
+$user = [];
while (true) {
+ $tmp_sockets = $sockets;
+ socket_select($tmp_sockets, $write, $except, null);
+ foreach ($tmp_sockets as $sock) {
+ if ($sock == $socket) {
+ $sockets[] = socket_accept($socket);
+ $user[] = ['socket' => $socket, 'handshake' => false];
+ } else {
+ $curr_user = $user[array_search($sock, $user)];
+ if ($curr_user['handshake']) { // 已握手
+ $msg = socket_read($sock, 102400);
+ echo '客户端发来消息' . decode($msg);
+ socket_write($sock, encode('这是来自服务端的消息'));
+ } else {
+ // 握手
+ }
+ }
+ }
- $conn_sock = socket_accept($socket);
- $request = socket_read($conn_sock, 102400);
...
实现聊天室
<?php
new WebSocket();
class Websocket
{
/**
* @var resource
*/
protected $socket;
/**
* @var array 用户列表
*/
protected $user = [];
/**
* @var array 存放所有 socket 资源
*/
protected $socket_list = [];
public function __construct()
{
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($this->socket, 0, 8888);
socket_listen($this->socket);
// 将 socket 资源放入 socket_list
$this->socket_list[] = $this->socket;
while (true) {
$tmp_sockets = $this->socket_list;
socket_select($tmp_sockets, $write, $except, null);
foreach ($tmp_sockets as $sock) {
if ($sock == $this->socket) {
$conn_sock = socket_accept($sock);
$this->socket_list[] = $conn_sock;
$this->user[] = ['socket' => $conn_sock, 'handshake' => false, 'name' => '无名氏'];
} else {
$request = socket_read($sock, 102400);
$k = $this->getUserIndex($sock);
if (!$request) {
continue;
}
// 用户端断开连接
if ((\ord($request[0]) & 0xf) == 0x8) {
$this->close($k);
continue;
}
if (!$this->user[$k]['handshake']) {
// 握手
$this->handshake($k, $request);
} else {
// 已握手
$this->send($k, $request);
}
}
}
}
}
/**
* 关闭连接
*
* @param $k
*/
protected function close($k)
{
$u_name = $this->user[$k]['name'] ?? '无名氏';
socket_close($this->user[$k]['socket']);
$socket_key = array_search($this->user[$k]['socket'], $this->socket_list);
unset($this->socket_list[$socket_key]);
unset($this->user[$k]);
$user = [];
foreach ($this->user as $v) {
$user[] = $v['name'];
}
$res = [
'type' => 'close',
'users' => $user,
'msg' => $u_name . '已退出',
'time' => date('Y-m-d H:i:s')
];
$this->sendAllUser($res);
}
/**
* 获取用户索引
*
* @param $socket
* @return int|string
*/
protected function getUserIndex($socket)
{
foreach ($this->user as $k => $v) {
if ($v['socket'] == $socket) {
return $k;
}
}
}
/**
* 握手
* @param $k
* @param $request
*/
protected function handshake($k, $request)
{
preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);
$key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$response = "HTTP/1.1 101 Switching Protocols\r\n";
$response .= "Upgrade: websocket\r\n";
$response .= "Connection: Upgrade\r\n";
$response .= "Sec-WebSocket-Accept: {$key}\r\n\r\n";
socket_write($this->user[$k]['socket'], $response);
$this->user[$k]['handshake'] = true;
}
/**
* 接收并处理消息
*
* @param $k
* @param $msg
*/
public function send($k, $msg)
{
$msg = $this->decode($msg);
$msg = json_decode($msg, true);
if (!isset($msg['type'])) {
return;
}
switch ($msg['type']) {
case 'login': // 登录
$this->user[$k]['name'] = $msg['name'] ?? '无名氏';
$users = [];
foreach ($this->user as $v) {
$users[] = $v['name'];
}
$res = [
'type' => 'login',
'name' => $this->user[$k]['name'],
'msg' => $this->user[$k]['name'] . ': login success',
'users' => $users,
];
$this->sendAllUser($res);
break;
case 'message': // 接收并发送消息
$res = [
'type' => 'message',
'name' => $this->user[$k]['name'] ?? '无名氏',
'msg' => $msg['msg'],
'time' => date('H:i:s'),
];
$this->sendAllUser($res);
break;
}
}
/**
* 发送给所有人
*
*/
protected function sendAllUser($msg)
{
if (is_array($msg)) {
$msg = json_encode($msg);
}
$msg = $this->encode($msg);
foreach ($this->user as $k => $v) {
socket_write($v['socket'], $msg, strlen($msg));
}
}
/**
* 解码
*
* @param $buffer
* @return string
*/
protected function decode($buffer)
{
$len = \ord($buffer[1]) & 127;
if ($len === 126) {
$masks = \substr($buffer, 4, 4);
$data = \substr($buffer, 8);
} else {
if ($len === 127) {
$masks = \substr($buffer, 10, 4);
$data = \substr($buffer, 14);
} else {
$masks = \substr($buffer, 2, 4);
$data = \substr($buffer, 6);
}
}
$dataLength = \strlen($data);
$masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
return $data ^ $masks;
}
protected function encode($buffer)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = \strlen($buffer);
$first_byte = "\x81";
if ($len <= 125) {
$encode_buffer = $first_byte . \chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
}
}
return $encode_buffer;
}
}
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
h3 {
display: flex;
justify-content: center;
margin: 30px auto;
}
.but-box {
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
#box {
display: flex;
margin: 5px auto;
border-radius: 5px;
border: 1px #ccc solid;
height: 400px;
width: 700px;
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
#msg-box {
width: 480px;
margin-right: 111px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
#user-box {
width: 110px;
overflow-y: auto;
overflow-x: hidden;
float: left;
border-left: 1px #ccc solid;
height: 100%;
background-color: #F1F1F1;
}
button {
float: right;
width: 80px;
height: 35px;
font-size: 18px;
}
input {
width: 100%;
height: 30px;
padding: 2px;
line-height: 20px;
outline: none;
border: solid 1px #CCC;
}
.but-box p {
margin-right: 160px;
}
</style>
<body>
<h3>这是一个php socket实现的web聊天室</h3>
<div id="box">
<div id="msg-box"></div>
<div id="user-box"></div>
</div>
<div>
<p><textarea cols="60" rows="3" style="resize:none;pedding: 10px" id="content"> </textarea></p>
<button id="send">发送</button>
</div>
<script src="https://cdn.bootcss.com/jquery/2.2.1/jquery.min.js"></script>
<script>
let ws = new WebSocket('ws://124.222.85.67:8888');
ws.onopen = function (event) {
console.log('连接成功');
var name = prompt('请输入用户名:');
ws.send(JSON.stringify({
type: 'login',
name: name
}));
if (!name) {
alert('好你个坏蛋,竟然没有输入用户名');
}
};
ws.onmessage = function (event) {
let data = JSON.parse(event.data);
console.log(data);
switch (data.type) {
case 'close':
case 'login':
$("#user-box").html('');
data.users.forEach(function (item) {
$("#user-box").append(`<p style="color: grey;">${item}</p>`);
});
if (data.msg) {
$("#msg-box").append(`<p style="color: grey;">${data.msg}</p>`);
}
break;
case 'message':
$("#msg-box").append(`<p><span style="color: #0A89FF">${data.time}</span><span style="color: red">${data.name}</span>${data.msg}</p>`);
break;
}
};
ws.onclose = function (event) {
alert('连接关闭');
};
document.onkeydown = function (event) {
if (event.keyCode == 13) {
send();
}
}
$("#send").click(function () {
send();
});
function send() {
let content = $("#content").val();
$("#content").val('');
if (!content) {
return;
}
ws.send(JSON.stringify({
type: 'message',
msg: content
}));
}
</script>
</body>
</html>
[^2]: 为了建立 websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)