Artikel ini membawakan anda pengetahuan yang berkaitan tentang php+socket Ia terutamanya memperkenalkan cara menggunakan soket asli php untuk melaksanakan bilik sembang web yang ringkas. Rakan-rakan yang berminat boleh lihat di bawah ini semoga bermanfaat untuk semua.
Kata Pengantar
Artikel ini melaksanakan bilik sembang web ringkas menggunakan soket asli PHP bahagian bawah artikel.
Jika tidak ada yang lain, ini sepatutnya menjadi artikel terakhir dalam siri artikel ini Apabila saya menulis artikel bersiri ini, saya fikir ia akan menjadi perkara yang sangat mudah, tetapi selepas menulis beberapa artikel ini, saya hampir membaca. melalui kod Workerman. Jadi jangan terlalu bercita-cita tinggi dan mempunyai cita-cita yang terlalu kecil Anda mesti mencubanya sendiri. Adalah lebih baik untuk menulisnya untuk membuktikan bahawa anda benar-benar memahami sesuatu
Pengenalan kepada websocket<.>
protokol webSocket Ia adalah protokol komunikasi rangkaian yang dilahirkan pada tahun 2008 dan menjadi standard antarabangsa pada tahun 2011.RFC6455 mentakrifkan standard komunikasinya. webSocket ialah protokol untuk komunikasi dupleks penuh pada sambungan TCP tunggal yang HTML5 mula sediakan Pelayan boleh menolak mesej secara aktif kepada klien, dan klien juga boleh menghantar mesej secara aktif kepada pelayan. WebSocket menetapkan spesifikasi protokol komunikasi Melalui mekanisme jabat tangan, sambungan seperti tcp boleh diwujudkan antara klien (pelayar) dan pelayan (pelayan web), dengan itu memudahkan komunikasi cs.
Mengapa websocket diperlukan
Protokol HTTP ialah protokol lapisan aplikasi tanpa kewarganegaraan, tanpa sambungan, sehala. Ia menggunakan model Permintaan komunikasi hanya boleh dimulakan oleh pelanggan, dan pelayan bertindak balas kepada permintaan Model komunikasi ini mempunyai kelemahan: adalah mustahil bagi pelayan untuk memulakan mesej secara aktif kepada pelanggan. Keupayaan serentak permintaan HTTP tradisional dicapai dengan memulakan berbilang sambungan TCP untuk mengakses pelayan pada masa yang sama membolehkan kami mengeluarkan berbilang permintaan pada sambungan ws secara serentak, iaitu, selepas permintaan A dihantar belum tiba lagi. Anda boleh terus mengeluarkan permintaan B. Disebabkan ciri permulaan TCP yang perlahan dan kehilangan sambungan itu sendiri, ciri protokol soket web ini telah meningkatkan kecekapan dengan banyak. 请求 => 响应
Jabatan tangan pelanggan dan pelayan
Protokol websocket memerlukan jabat tangan sebelum sambungan [^2] , biasanya terdapat kaedah jabat tangan berikut:Kaedah jabat tangan yang terdahulu, dengan dua kekunci, menggunakan penyulitan md5
Sec-WebSocket-key
+ key
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
yang dilaporkan oleh pelanggan
pada rentetan, kemudian menyulitkan hasil melalui SHA1
, dan akhirnya mengembalikannya kepada klien 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 $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)); }
artikel terdahulu Artikel ini tidak akan memperkenalkannya secara terperinci.
Gunakan ujian bahagian hadapan, buka mana-mana konsol penyemak imbas kami (konsol) dan masukkan kandungan berikut Keadaan sedia bagi objek soket web yang dikembalikan ialah 1, yang bermaksud jabat tangan ini berjaya. kandungan akhir. Artikel ini tidak akan memperkenalkannya secara terperinci Anda boleh merujuk kepadaTutorial Rookie:
console.log(new WebSocket('ws://192.162.2.166:8888')); // 运行后返回: WebSocket { binaryType: "blob" bufferedAmount: 0 extensions: "" onclose: null onerror: null onmessage: null onopen: null protocol: "" readyState: 1 url: "ws://192.162.2.166:8888/"}
Menghantar dan Menerima Data
使用 websocket 协议传输协议需要遵循特定的格式规范,详情请参考 datatracker.ietf.org/doc/html/rfc6...
为了方便,这里直接贴出加解密代码,以下代码借鉴与 workerman 的 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()
函数 ,本文就不写原理与语法了,详情可参考 之前的文章,以下代码修改自前文 Siri PHP+Socket untuk melaksanakan bilik sembang websocket
... 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>
[^1]:是通讯传输的一个术语。 通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合
[^2]: 为了建立 websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)
推荐学习:《PHP视频教程》
Atas ialah kandungan terperinci Siri PHP+Socket untuk melaksanakan bilik sembang websocket. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!