1,核心概念:
websocket协议是http的升级版。
2,一次握手
websocket在创建连接时会先向服务端发送一个普通的http请求,大致消息为:
GET / HTTP/1.1
Host: localhost:1234
User-Agent: Mozilla/5.0 (WindowsNT 10.0; WOW64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language:zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin:http://xxx.xxx.xxx
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 3hfEc+Te7n7FSrLBsN59ig==
Connection: keep-alive,Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
相比较普通的http请求多了
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: 3hfEc+Te7n7FSrLBsN59ig==
Connection: keep-alive,Upgrade
Upgrade: websocket
服务端通过
Upgrade: websocket
判断是否为websocket请求通过生成新的key,并拼接成新的响应消息
$new_key = base64_encode(sha1($head['Sec-WebSocket-Key']."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13
Connection: Upgrade
Sec-WebSocket-Accept:new-key
完成一次握手双方建立通信通道
完整的服务案例<转载的>
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);
//这个函数很重要
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();
客户端
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript">
var ws,wsmonitor,uid;
var wsurl = 'ws://127.0.0.1:1234';
$(function(){
ws = new WebSocket(wsurl);
ws.onopen = function() {
console.log('websocketed');
var type = 0; //位置这里是一个方法,我给干掉了,写成0了,请根据项目自己来搞吧
var userId = 1;
var data ={articleId:0, type:type};
ws.send(JSON.stringify(data)); //这里是向服务器发送一个JSON的数据 处理请看ArticleMonitor.php的process函数
console.log('send data ok');
};
ws.onmessage = function(res){
console.log(res.data)
};
ws.onclose = function(e){
clearInterval(wsmonitor);
console.log('websocket closed');
}
ws.onerror = function(e){
clearInterval(wsmonitor);
console.log('websocket error');
}
})
</script>
3,常用的函数
关键函数1
socket_create($net参数1,$stream参数2,$protocol参数3)
作用:创建一个socket套接字,说白了,就是一个网络数据流。
返回值:一个套接字,或者是false,参数错误发出E_WARNING警告
参数1是:网络协议,
网络协议有哪些?它的选择项就下面这三个:
AF_INET: IPv4 网络协议。TCP 和 UDP 都可使用此协议。一般都用这个,你懂的。
AF_INET6: IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX: 本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。
参数2:套接字流,选项有:
SOCK_STREAM TCP 协议套接字
SOCK_DGRAM UDP协议套接字
SOCK_SEQPACKET
SOCK_RAW
SOCK_RDM。
参数3:protocol协议,选项有
SOL_TCP: TCP 协议。
SOL_UDP: UDP协议。
关键函数2:
socket_connect($socket参数1,$ip参数2,$port参数3)
作用:连接一个套接字,返回值为true或者false
参数1:socket_create 的函数返回值
参数2:ip地址
参数3:端口号
关键函数3:
socket_bind($socket参数1,$ip参数2,$port参数3)
作用:绑定一个套接字,返回值为true或者false
参数1:socket_create的函数返回值
参数2:ip地址
参数3:端口号
关键函数4:
socket_listen($socket参数1,$backlog 参数2)
作用:监听一个套接字,返回值为true或者false
参数1:socket_create的函数返回值
参数2:最大监听套接字个数
关键函数5:
socket_accept($socket)
作用:接收套接字的资源信息,成功返回套接字的信息资源,失败为false
参数:socket_create的函数返回值
关键函数6:
socket_read($socket参数1,$length参数2)
作用:读取套接字的资源信息,
返回值:成功把套接字的资源转化为字符串信息,失败为false
参数1:socket_create或者socket_accept的函数返回值
参数2:读取的字符串的长度
关键函数7:
socket_write($socket参数1,$msg参数2,$strlen参数3)
作用:把数据写入套接字中
返回值:成功返回字符串的字节长度,失败为false
参数1:socket_create或者socket_accept的函数返回值
参数2:字符串
参数3:字符串的长度
关键函数8:
socket_close($socket)
作用:关闭套接字
返回值:成功返回true,失败为false
参数:socket_create或者socket_accept的函数返回值
关键函数9:
socket_set_option($socket参数1 ,$level 参数2,$optname 参数3,$optval 参数4)
作用:是给套接字设置数据流选项
参数1:socket_create或者socket_accept的函数返回值
参数2:SOL_SOCKET,好像只有这个选项
参数3可为:
SO_REUSEADDR 是让套接字端口释放后立即就可以被再次使用 参数3假如是这个,则参数4可以为true或者false
SO_RCVTIMEO 是套接字的接收资源的最大超时时间
S0_SNDTIMEO 是套接字的发送资源的最大超时时间
参数4与参数3是相关联的,参数3假如是这两个,则参数4是一个这样的数组array('sec'=>1,'usec'=>500000)
数组里面都是设置超时的最大时间,不过,一个是秒为单位,一个是微秒单位,作用都一样
关键函数10:
socket_select($active_sockets,$write,$except,NULL);
socket_select(array &$read, array &$write, array &$except, $tv_sec, $tv_usec = 0)
作用:多路选择
参数1-3:从传入的数组中选择出可读的,可写的,异常的socket,并返回
参数4:
若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合(socket数组)中某个文件描述符发生变化为止
若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值
timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述
一般传入null
关键函数11:
socket_last_error($socket)
作用:是获取套接字的最后一条错误码号
参数:socket_create的返回值
返回值:套接字code
关键函数13:
socket_strerror($code)
作用:获取code的字符串信息
参数:socket_last_error函数的返回值
返回值:套接字的具体错误信息 <通常当做调试用,输出错误信息>
其他重要的函数:
socket_clear_error() 清除socket的错误或者最后的错误代码
socket_close() 关闭一个socket资源
socket_create_pair() 产生一对没有区别的socket到一个数组里
socket_getpeername() 获取远程类似主机的ip地址
socket_getsockname() 获取本地socket的ip地址
socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组
socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构
socket_iovec_delete() 删除一个已经分配的iovec
socket_iovec_fetch() 返回指定的iovec资源的数据
socket_iovec_free() 释放一个iovec资源
socket_iovec_set() 设置iovec的数据新值
socket_readv() 读取从分散/聚合数组过来的数据
socket_recv() 从socket里结束数据到缓存
socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socket
socket_recvmsg() 从iovec里接受消息
socket_send() 这个函数发送数据到已连接的socket
socket_sendmsg() 发送消息到socket
socket_sendto() 发送消息到指定地址的socket
socket_set_block() 在socket里设置为块模式
socket_set_nonblock() socket里设置为非块模式
socket_set_option() 设置socket选项
socket_shutdown() 这个函数允许你关闭读、写、或者指定的socket
socket_strerror() 返回指定错误号的详细错误
socket_writev() 写数据到分散/聚合数组
必须先执行socket_bind,再执行socket_listen,最后才执行socket_accept
非常全面的laravel 案例
laravel之PHP WebSocket研发+WS or WSS (websocket SSL) js websocket