做的比较简单,主要是一个思想和对 socket 的学习。
这个只能自己玩玩,没有做发送的数处理等一系列的小问题。。。
php代码:
<?php $address = '0.0.0.0'; //0.0.0.0代表所有内网的ip,你也可以是0,代表所有的内网和外网的ip都可以访问。 $port = 8888; $socket = new So($address , $port); $socket->run(); class So { private $sockets;//报存所有的链接套接字 private $master; //服务产生的套接字 private $user; //报存链接的用户信息。 function __construct($address,$port) { //创建socket并把保存socket套接字 $this->connect($address,$port); } //服务端 socket套接字创建。 private function connect($address,$port){ $socket = socket_create(AF_INET, SOCK_STREAM ,SOL_TCP); socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,true); socket_bind($socket,$address,$port); socket_listen($socket); //吧系统的套接字保存起来 $this->master = $socket; $this->sockets[] = $socket; } public function run() { $master = $this->master; $write = NUll; $except = NUll; while (true) { $sockets = $this->sockets; // var_dump($sockets); //阻塞执行,一旦有新变动就继续执行,并获取变动的套接字,新的链接会让服务产生的套接字发生变动。 //变动只是一个动词,表示状态发生变化。 //socket_select 是实现多用户链接的关键所在,就是因为他能找到是谁在变动。 socket_select($sockets,$write,$except ,NULL); //这里的排序是为了干掉$sockets的默认下标 sort($sockets); $sock = $sockets[0]; //判断是新链接的套接字。 if ($sock == $master) { //用户请求过来,服务器通过系统的套接字创建一个新的套接字分配给他。 $client = socket_accept($sock); //吧这个套接字存入套接字数组。 $this->sockets[] = $client ; //在用户数组,保存这个用户的连接套接字和连接状态 $this->user[] = ['socket'=>$client,'state'=>false]; }else{ $request = socket_read($sock,1024); //获取当前链接的套接字用户的健值。 $key = $this->setKey($sock); $len = strlen($request); //用户直接关闭浏览器的时候会接受一个长度为8的数据, if ($len == 8) { $this->close($key); continue; } //判断是否已经握手成功 if (!$this->user[$key]['state']) { // 调用握手方法,$sock需要握手的套接字 $this->handleShake($request,$sock); //握手成功修改这个用户的链接状态为true $this->user[$key]['state'] = true; }else{ //获取收据,和发送数据 $msg = $this->decode($request); // echo $msg; $this->send($msg,$key); // $msg = $this->encode('hello wored !'); // socket_write($sock, $msg, strlen($msg)); } } } } //获取当前套接字的健值 public function setKey($value){ foreach ($this->user as $k => $v) { if ($value == $v['socket']) { return $k; } } } //与客户端websocket握手 private function handleShake($request,$sock){ var_dump($request); preg_match('/Sec-WebSocket-Key: (.*)\r\n/',$request,$match); $accept = base64_encode(sha1($match[1].'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true)); $buffer = "HTTP/1.1 101 Switching Protocols\r\n"; $buffer .= "Connection: Upgrade \r\n"; $buffer .= "Sec-WebSocket-Accept: $accept\r\n"; $buffer .= "Sec-WebSocket-Version: 13 \r\n"; $buffer .= "Upgrade: websocket \r\n\r\n"; socket_write($sock , $buffer, strlen($buffer)); } //接受数据 private function send($msg,$key) { $list = explode('===',$msg); // var_dump($list); switch ($list[0]) { case 'login': $this->user[$key]['name']=$list[1]; $msg = ['msg'=>$list[1].'登录聊天室','type'=>'login']; $this->write(json_encode($msg)); $this->usersWeite(); break; case 'text': $text = ['text'=>$list[1],'sender'=>$list[3],'type'=>'text']; $this->write(json_encode($text)); break; default: # code... break; } } //发送当前连接的用户 private function usersWeite() { $userList = array_column($this->user, 'name'); $users = ['user'=>$userList,'type'=>'users']; $this->write(json_encode($users)); } //发送数据 private function write($msg,$key = 'all') { $msg = $this->encode($msg); if ($key == 'all') { foreach ($this->sockets as $sock) { if ($sock != $this->master) { socket_write($sock , $msg, strlen($msg)); } } }else{ // socket_write($sock , $msg, strlen($msg)); } } public function close($key) { //先获取指定的套接字 $socket = $this->user[$key]['socket']; //关闭该套接字 socket_close($socket); //删除该用户信息 unset($this->user[$key]); //重新组装套接字数组 $this->sockets = array_column($this->user,'socket'); //系统套接字必须在首位。 array_unshift($this->sockets,$this->master); //更新客户端 在线用户列表 $this->usersWeite(); } /* 对websocket传输过来的数据解码。你无需理解,照搬就可以。 */ private function decode($received){ $len = $masks = $data = $decoded = null; $buffer = $received; $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); } } for ($index = 0; $index < strlen($data); $index++) { $decoded .= $data[$index] ^ $masks[$index % 4]; } return $decoded; } /* 对传输给websocket的数据编码。你无需理解,照搬就可以。 */ private function encode($msg){ if (!is_scalar($msg)) { print_r("只允许发送标量数据"); } // 数据长度 $len = strlen($msg); // 这边仅实现传输文本帧!第一个字节,文本帧 1000 0001 => 129 // 如果需要例如二进制帧,用于传输大文件,请另行实现 $first_byte = chr(129); if ($len <= 125) { // payload length = 7bit 支持的最大范围! $second_byte = chr($len); } else { if ($len <= 65535) { // payload length = 7 , extended payload length = 16bit,支持的最大范围 65535 // 最后16bit 被解释为无符号整数,排序为:大端字节序(网络字节序) $second_byte = chr(126) . pack('n' , $len); } else { // payload length = 7,extended payload length = 64bit // 最后 64 位被解释为无符号整数,大端字节序(网络字节序) $second_byte = chr(127) . pack('J' , $len); } } // 注意了,发送给客户端的数据不需要处理 // 详情查看 websocket 文档!! $encoded_data = $first_byte . $second_byte . $msg; // 这个就是发送给客户端的数据! return $encoded_data; } }
htem:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> <style type="text/css" media="screen"> li{ list-style-type: none; } #centre{ width: 70%; height: 600px; margin: 0 auto; background-color: #ddd; } #chat{ width: 70%; height: 540px; display: inline-block; background-color: #f3f3f3; vertical-align: top; margin:30px 0 30px 20px; } #user{ display: inline-block; width: 20%; height: 540px; background-color: #d3d3d3; vertical-align: top; margin:30px 0 30px 40px; } #content{ height:80%; } #sand{ height:20%; background-color: #fff } .user_login{ text-align: center; color: #9c8e8e; } textarea{ width: 90%; height: 88%; resize:none; vertical-align: bottom; display: inline-block; margin-top: 5px; margin-left: 10px; } </style> </head> <body> <div id="centre"> <div id="chat"> <div id="content"> <ul> </ul> </div> <div id="sand"> <textarea name="text" id="text"></textarea> <button type="button">发送</button> </div> </div> <div id="user"></div> </div> </body> <script> var name = ''; var so = new WebSocket('ws://127.0.0.1:8888');//这里记得改成你自己的ip和端口 console.log(so) so.onopen = function(){ console.log('链接成功'); //输入名字 setName(); } so.onmessage=function(e){ let data = JSON.parse(e.data); if (data.type == 'login') { $('#content ul').append('<li>'+data.msg+'</li>'); } if (data.type == 'users') { $('#user').html('') for (var i = 0; i < data.user.length; i++) { $('#user').append('<li>'+data.user[i]+'</li>'); } } if(data.type == 'text'){ var sender = data.sender == name ? '我' : data.sender; $('#content ul').append('<li>'+sender+':'+data.text+'</li>'); } console.log(data) } function setName(){ while(true){ name = window.prompt('请输入你的名字!',''); if(!name){ alert('必须输入名字才能继续访问') }else{ break; } } login = 'login==='+name so.send(login) } $('button').click(function(e){ var text = $('#text').val(); if(text != ''){ text = 'text==='+text+'===name==='+name so.send(text) $('#text').val(''); }else{ alert('你还没有写入消息呢!') } }) </script> </html>