Websocket在线聊天
websocket通信流程
请求报文
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
与传统 HTTP 报文不同的地方:
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 3hfEc+Te7n7FSrLBsN59ig==
Connection: keep-alive,Upgrade
Upgrade: websocket
Upgrade: websocket,Connection: Upgrade就告诉了服务端,客户端想从HTTP协议升级到websocket协议,Sec-WebSocket-Key头的内容是浏览器随机生成的经过base64编码的值。服务端收到这样请求后就回一个固定格式的消息,然后客户端与服务端就建立起了websocket连接,之后的消息传递就遵从websocket协议,这就是我们所说的webscoket一次握手,服务端回应的消息格式如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13
Connection: Upgrade
Sec-WebSocket-Accept:new-key
new-key
的值通过先获取客户端请求头里面的Sec-WebSocket-Key
值与258EAFA5-E914-47DA-95CA-C5AB0DC85B11
进行字符串连接后进行sha1
加密,再base64
编码得到。
php代码如下:
$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
如何使用websocket
客户端:
客户端,js提供了接口,我们主要用的有三个(详细的可以自行上菜鸟教程查看):
(1)var ws = new WebSocket("ws://localhost:9998/echo");
建立websocket连接。参数是个url:协议://域名(或者ip):端口/路径
webscoket支持ws和wss对应着http和https(后面服务器部署时再具体讲)
(2)ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("数据已接收...");
};
这个回调函数用于接收服务端消息
(3)ws.send("发送数据");
这个函数用于向服务端发送数据
服务端:
服务端我们可以选择用现成的库,也可以自己写,这里我们自己写一个,==服务端本质也是一个socket==(不熟悉php socket可以自行百度下,这里就不再赘述),不过与客户端进行数据传输的时候要遵从websocket协议消息格式。通过上面我们知道,要想建立websocket连接,服务端需要回应客户端的握手消息。
建立server.php:
class WebSocketServer{
private $sockets;//所有socket连接池包括服务端socket
private $users;//所有连接用户
private $server;//服务端 socket
public function __construct($ip,$port){
$this->server=socket_create(AF_INET,SOCK_STREAM,0);
$this->sockets=array($this->server);
$this->users=array();
socket_bind($this->server,$ip,$port);
socket_listen($this->server,3);
echo "[*]Listening:".$ip.":".$port."\n";
}
public function run(){
$write=NULL;
$except=NULL;
while (true){
$active_sockets=$this->sockets;
socket_select($active_sockets,$write,$except,NULL);
//这个函数很重要
//前三个参数时传入的是数组的引用,会依次从传入的数组中选择出可读的,可写的,异常的socket,我们只需要选择出可读的socket
//最后一个参数tv_sec很重要
//第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合(socket数组)中某个文件描
//述符发生变化为止;
//第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无
//变化返回0,有变化返回一个正值;
//第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,
//否则在超时后不管怎样一定返回,返回值同上述。
foreach ($active_sockets as $socket){
if ($socket==$this->server){
//服务端 socket可读说明有新用户连接
$user=socket_accept($this->server);
$key=uniqid();
$this->sockets[]=$user;
$this->users[$key]=array(
'socket'=>$user,
'handshake'=>false //是否完成websocket握手
);
}else{
//用户socket可读
$buffer='';
$bytes=socket_recv($socket,$buffer,1024,0);
$key=$this->find_user_by_socket($socket); //通过socket在users数组中找出user
if ($bytes==0){
//没有数据 关闭连接
$this->disconnect($socket);
}else{
//没有握手就先握手
if (!$this->users[$key]['handshake']){
$this->handshake($key,$buffer);
}else{
//握手后
//解析消息 websocket协议有自己的消息格式
//解码 编码过程固定的
$msg=$this->msg_decode($buffer);
echo $msg;
//编码后发送回去
$res_msg=$this->msg_encode($msg);
socket_write($socket,$res_msg,strlen($res_msg));
}
}
}
}
}
}
//解除连接
private function disconnect($socket){
$key=$this->find_user_by_socket($socket);
unset($this->users[$key]);
foreach ($this->sockets as $k=>$v){
if ($v==$socket)
unset($this->sockets[$k]);
}
socket_shutdown($socket);
socket_close($socket);
}
//通过socket在users数组中找出user
private function find_user_by_socket($socket){
foreach ($this->users as $key=>$user){
if ($user['socket']==$socket){
return $key;
}
}
return -1;
}
private function handshake($k,$buffer){
//截取Sec-WebSocket-Key的值并加密
$buf = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);
$key = trim(substr($buf,0,strpos($buf,"\r\n")));
$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
//按照协议组合信息进行返回
$new_message = "HTTP/1.1 101 Switching Protocols\r\n";
$new_message .= "Upgrade: websocket\r\n";
$new_message .= "Sec-WebSocket-Version: 13\r\n";
$new_message .= "Connection: Upgrade\r\n";
$new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
socket_write($this->users[$k]['socket'],$new_message,strlen($new_message));
//对已经握手的client做标志
$this->users[$k]['handshake']=true;
return true;
}
//编码 把消息打包成websocket协议支持的格式
private function msg_encode( $buffer ){
$len = strlen($buffer);
if ($len <= 125) {
return "\x81" . chr($len) . $buffer;
} else if ($len <= 65535) {
return "\x81" . chr(126) . pack("n", $len) . $buffer;
} else {
return "\x81" . char(127) . pack("xxxxN", $len) . $buffer;
}
}
//解码 解析websocket数据帧
private function msg_decode( $buffer )
{
$len = $masks = $data = $decoded = null;
$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;
}
}
$ws=new WebSocketServer('127.0.0.1',1234);
$ws->run();
客户端(浏览器)
开发
配置
打开php.ini配置文件,搜索 extension=php_sockets.dll,把前面的‘;‘分号删掉。修改之后重启服务。
检查一
检查二
<?php
if(extension_loaded('sockets')){
echo "1";
}else{
echo "0";
}
前端
理论
浏览器通过websocket
对象公开所有必须的客户端功能,以下 API 用于创建websocket
对象:
var ws = new WebSocket(url);
#url : ws://ip:port/资源名称
websocket事件
事件 | 事件处理程序 | 描述 |
---|---|---|
open | ws对象.onopen | 建立连接时触发 |
message | ws对象.onmessage | 客户端接收服务端数据时触发 |
error | ws对象.onerror | 通信发生错误时触发 |
close | ws对象.onclose | 连接关闭时触发 |
websocket方法
方法 | 描述 |
---|---|
send() | 使用连接发送数据 |
//创建WebSocket Server对象,监听0.0.0.0:9502端口。
$ws = new Swoole\WebSocket\Server('0.0.0.0', 9502);
//监听WebSocket连接打开事件。
$ws->on('Open', function ($ws, $request) {
$ws->push($request->fd, "hello, welcome\n");
});
//监听WebSocket消息事件。
$ws->on('Message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
});
//监听WebSocket连接关闭事件。
$ws->on('Close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});
$ws->start();