本篇文章给大家带来了关于php+socket的相关知识,其中主要介绍了怎么使用php原生socket实现一个简易的web聊天室?感兴趣的朋友下面一起来看一下,希望对大家有帮助。
php原生socket实现websocket聊天室前言
这篇文章实现了使用php原生socket实现了一个简易的web聊天室,最终代码在文章最底部。
不出意外的话这应该是这个系列文章的最后一篇了,写这个系列文章时本以为是很简单的东西,但实际几篇写下来使我几乎通读了 workerman 的代码,所以永远不要眼高手低,一定还是要自己尝试,最好是写出来,才能证明自己真正的弄懂了一件事情
websocket介绍
websocket 协议是一种网络通信协议,在 2008 年诞生,2011 年成为国际标准,rfc6455 定义了它的通信标准,如今所有浏览器都已支持了该协议。websocket 是 html5 开始提供的一种在单个 tcp 连接上进行全双工[^1]通讯的协议,服务器可以主动向客户端推送消息,客户端也可以主动向服务端发送消息。
websocket 约定了一个通信协议的规范,通过握手机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似 tcp 的连接,从而方便 cs 通信。
为什么需要websocket
http 协议是一种无状态的、无连接的、单向的应用层协议。它采用了 请求 => 响应 模型,通信请求仅能由客户端发起,服务端对请求做出应答处理,这种通信模型有一个弊端:无法实现服务端主动向客户端发起消息。传统的 http 请求,其并发能力都是依赖同时发起多个 tcp 连接访问服务器实现的而 websocket 则允许我们在一条 ws 连接上同时并发多个请求,即在 a 请求发出后 a 响应还未到达,就可以继续发出 b 请求。由于 tcp 的慢启动特性,以及连接本身的握手损耗,都使得 websocket 协议的这一特性有很大的效率提升。
特点建立在 tcp 协议之上,服务端的实现相对比较容易
与 http 协议有良好的兼容性,默认端口也是 80 和 443,并且握手阶段采用 http 协议,因此握手时不容易被屏蔽,能通过各种 http 代理服务器。
数据格式比较轻量,性能开销小,通信高效。
可以发送文本,也可以发送二进制数据。
没有同源限制,客户端可以与任意服务器进行通信。
协议标识符是 ws(如果加密则为 wss),服务地址就是 url。
php实现websocket客户端与服务端握手
websocket 协议在连接前需要握手[^2],通常握手方式有以下几种方式
基于 flash 的握手协议(不建议)
基于 md5 加密方式的握手协议
较早的握手方法,有两个 key,使用 md5 加密
基于 sha1 加密方式的握手协议
当前主要的握手协议,本文将以此协议为主
获取客户端上报的 sec-websocket-key
拼接 key + 258eafa5-e914-47da-95ca-c5ab0dc85b11
对字符串做 sha1 计算,再把得到的结果通过 base64 加密,最后再返回给客户端
客户端请求信息如下:
get /chat http/1.1host: server.example.comupgrade: websocketconnection: upgradesec-websocket-key: dghlihnhbxbszsbub25jzq==origin: http://example.comsec-websocket-protocol: chat, superchatsec-websocket-version: 13
客户端需返回如下数据:
http/1.1 101 switching protocolsupgrade: websocketsec-websocket-version: 13connection: upgradesec-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));}
相关语法解释可参考 之前的文章,本文章不做详细介绍。
使用前端测试,打开我们的任意浏览器控制台(console)输入以下内容,返回的 websocket 对象的 readystate 为 1 即为握手成功,此为前端内容,本文不多做介绍,详情可参考 菜鸟教程:
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/"}
发送数据与接收数据
使用 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聊天室我们紧接着上文的代码继续优化,以实现简易的web聊天室
多路复用
其实就是加一下 socket_select() 函数 ,本文就不写原理与语法了,详情可参考 之前的文章,以下代码修改自前文 发送数据与接收数据
...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);...
实现聊天室最终成果演示
我们将上述代码改造成类,并在类变量储存用户信息,添加消息处理等逻辑,最后贴出代码,建议保存下来自己尝试一下,也许会有全新的认知,后端代码:
<?phpnew 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视频教程》
以上就是php+socket系列之实现websocket聊天室的详细内容。