<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">一、websocket协议简介</span>
WebSocket是为解决客户端与服务端实时通信而产生的技术。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
二、php使用的一些websocket函数
resource socket_create ( int $domain , int $type , int $protocol )
bool socket_set_option ( resource $socket , int $level , int $optname , mixed $optval )
bool socket_bind ( resource $socket , string $address [, int $port = 0 ] )
bool socket_listen ( resource $socket [, int $backlog = 0 ] )
以上四个方法都是非阻塞的,而下面的几个方法都是阻塞的或者是有timeout的
resource socket_accept ( resource $socket )
int socket_recv ( resource $socket , string &$buf , int $len , int $flags )
string socket_read ( resource $socket , int $length [, int $type = PHP_BINARY_READ ] )
后面会对socket_recv()和socket_read()进行介绍
三、php服务端代码分析
<?php
/**
* Created by PhpStorm.
* User: changshuiwang
* Date: 2016/9/5
* Time: 14:35
*/
error_reporting(~E_WARNING & ~E_NOTICE);
set_time_limit(0);
class Wserver{
public $address;
public $port;
public $master; <span style="color: rgb(255, 0, 0);">// 连接 server 的 client</span>
public $sockets = array(); <span style="color:#ff0000;">// 不同状态的 socket 管理</span>
//public $handshake = false; <span style="color:#ff0000;">// 判断是否握手</span>
public $request = array(); <span style="color:#ff0000;">//过来的请求</span>
public $response = array();<span style="color:#ff0000;">//服务人员</span>
public $client = array(); <span style="color:#ff0000;">//正在进行的会话</span>
public $wenhou = "你好,有什么可以帮助你的么?"; <span style="color:#ff0000;">//建立请求的问候语</span>
public $refuse = "对不起,请稍等,暂时无空闲客服人员。"; <span style="color:#ff0000;">//没有空客服人员的回话</span>
//public $tag=false; //默认为false,标志位,是否有空客服人员
function __construct($address, $port)
{
$this->address=$address;
$this->port=$port;
// 建立一个 socket 套接字
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
or die("socket_create() failed");
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)
or die("socket_option() failed");
socket_bind($this->master, $this->address, $this->port)
or die("socket_bind() failed");
socket_listen($this->master)
or die("socket_listen() failed");
$this->sockets[] = $this->master;
// debug
echo("Master socket : " . $this->master . "\n");
//socket_set_nonblock($this->master);<span style="color:#ff0000;">//让进程非阻塞,主要是socket_accept(),因为这里没有让master参与到通信中,所以可以不加这一句</span>
}
function start(){
while(true) {
$changed=$this->sockets;<span style="color:#ff0000;">//让当前的socket列表加入到监听中</span>
$write = NULL;
$except = NULL;
$num=socket_select($changed, $write, $except, 0);
if($num==0){
continue;
}
<span style="color:#ff0000;">//$changed是引用变量,调用之后$changed是状态改变的数组,如果来新连接$changed里面会有$this->master</span>
if(in_array($this->master,$changed)){
<span style="color:#ff0000;">//来新连接了</span>
$socket_new = socket_accept($this->master);<span style="color:#ff0000;">//返回值是一个resource</span>
//socket_set_nonblock($socket_new);<span style="color:#ff0000;">//不能让master以外的socket为非阻塞的,因为会涉及到读socket数据</span>
<span style="color:#ff0000;">//通过socket获取数据执行handshake</span>
@$header = socket_read($socket_new, 1024);
$this->perform_handshaking($header, $socket_new, $this->address, $this->port);
@socket_getpeername($socket_new, $ip);
$msg=$this->diff($ip);<span style="color:#ff0000;">//这里是根据ip来区分是客户</span>
switch ($msg){
case 'client':
$tag=$this->addClient($socket_new);
$message=$tag=="client"?$this->wenhou:$this->refuse;//发送的消息
$response = $this->mask(json_encode(array('type'=>'system', 'message'=> $message)));
$this->send_message([$socket_new],$response);
break;
default:
//$message="有新请求";
$this->addResponse($socket_new);
$index=array_search($socket_new,$this->response);
$this->changeStatus($index);
$message="客服人员已来!";
$response = $this->mask(json_encode(array('type'=>'system', 'message'=> $message)));
$this->send_message([$this->client["$index"],$response]);
break;
}
<span style="color:#ff0000;">//从改变的socket数组里面删除</span>
$found_socket = array_search($this->master, $changed);
unset($changed[$found_socket]);
echo $msg." connect\n";
}
<span style="color:#ff0000;">//未加入会话的请求的处理,比如未加入会话但发来消息,或者是在等待过程中断开</span>
foreach ($this->request as $request){
if(in_array($request,$changed)){
}
}
<span style="color:#ff0000;"> //如果是断开连接,socket会先发送一个消息,socket_read()返回false但是socket_recv()会读取到数据,
//所以foreach会执行两次,指的是两次while然后两次foreach
//第一次socket_recv会读取到数据,第二次socket_recv()读取不到数据,就会直接走下面的判断语句
//不能直接使用socket_read()因为会在读到换行时候结束</span>
foreach ($changed as $changed_socket){
<span style="color:#ff0000;">//如果有client数据发送过来</span>
$tag=false;
while(socket_recv($changed_socket, $buf, 1024, 0) >= 1)<span style="color:#ff0000;">//这里就要限制每次发送数据最多为1024,</span>
{
//var_dump("wh");
$tag=true;
//解码发送过来的数据
$received_text = $this->unmask($buf);
//编码需要发出的数据
$response_text = $this->mask(json_encode(array('type'=>'usermsg', 'message'=>$received_text)));
if($response_text=="\000"){<span style="color:#ff0000;">//对应的是断开连接的,如果是断开连接,socket_recv()会收到一串字符</span>
break;
}
if(in_array($changed_socket,$this->client)){
$index=array_search($changed_socket,$this->client);
$this->send_message([$this->response["$index"]],$response_text);
}else{
$index=array_search($changed_socket,$this->response);
$this->send_message([$this->client["$index"]],$response_text);
}
break;
//跳出循坏,因为socket_recv()会阻塞,但是如果socket断开了之后,socket_recv()就不会阻塞了
}
<span style="color:#ff0000;">//已经读了数据的跳过下面的断开连接判断,不然会阻塞</span>
if($tag==true){
continue;
}
<span style="color:#ff0000;">//检查offline的client,断开连接</span>
@$buf = socket_read($changed_socket, 1024, PHP_NORMAL_READ);
if ($buf === false) {<span style="color:#ff0000;">//没有接受到数据的时候,但是这个socket又有变化,也就是断开连接</span>
$found_socket = array_search($changed_socket, $this->sockets);<span style="color:#ff0000;">//返回下标</span>
socket_getpeername($changed_socket, $ip);
unset($this->sockets[$found_socket]);<span style="color:#ff0000;">//必须要手动删除</span>
$msg=$this->diff($ip);
$response = $this->mask(json_encode(array('type'=>'system', 'message'=>'断开连接')));
switch ($msg){
case "client":
$index=array_search($changed_socket,$this->client);
$this->client["$index"]="";
if($this->changeStatus($index)){
$this->send_message([$this->response["$index"]],$response);<span style="color:#ff0000;">//向客服发送断开</span>
$response = $this->mask(json_encode(array('type'=>'system', 'message'=>$this->wenhou)));
$this->send_message([$this->client["$index"]],$response);<span style="color:#ff0000;">//新客户的问候语</span>
}
break;
default:
$index=array_search($changed_socket,$this->response);
unset($this->response["$index"]);<span style="color:#ff0000;">//释放客服的数组元素</span>
$this->send_message([$this->client["$index"]],$response);
$index=array_search($this->client["$index"],$this->sockets);
unset($this->sockets["$index"]);<span style="color:#ff0000;">//删掉监听的socket</span>
break;
}
}
}
}
}
function send_message($clients,$msg)
{
foreach($clients as $changed_socket)
{
socket_write($changed_socket,$msg,strlen($msg));
}
return true;
}
//解码数据
function unmask($text) {
$length = ord($text[1]) & 127;
if($length == 126) {
$masks = substr($text, 4, 4);
$data = substr($text, 8);
}
elseif($length == 127) {
$masks = substr($text, 10, 4);
$data = substr($text, 14);
}
else {
$masks = substr($text, 2, 4);
$data = substr($text, 6);
}
$text = "";
for ($i = 0; $i < strlen($data); ++$i) {
$text .= $data[$i] ^ $masks[$i%4];
}
return $text;
}
//编码数据
function mask($text)
{
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($text);
if($length <= 125)
$header = pack('CC', $b1, $length);
elseif($length > 125 && $length < 65536)
$header = pack('CCn', $b1, 126, $length);
elseif($length >= 65536)
$header = pack('CCNN', $b1, 127, $length);
return $header.$text;
}
//握手的逻辑
function perform_handshaking($receved_header,$client_conn, $host, $port)
{
$headers = array();
$lines = preg_split("/\r\n/", $receved_header);
foreach($lines as $line)
{
$line = chop($line);
if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
{
$headers[$matches[1]] = $matches[2];
}
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $host\r\n" .
"WebSocket-Location: ws://$host:$port/demo/shout.php\r\n".
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
socket_write($client_conn,$upgrade,strlen($upgrade));
}
/**
* 查询空客服,如果没有就返回false
* @return bool|int|string
*/
function searchEmptyResponse(){
foreach ($this->response as $k=>$v){
if(empty($this->response[$k])){
return $k;
}else{
return false;
}
}
}
/**
* 加入到正在会话中
* @param $socket
*/
function addClient($socket){
if(empty($this->response)||count($this->response)==count($this->client)){
//满了
$this->request[]=$socket;
return "request";
}else{
$index=$this->searchEmptyResponse();
$this->client[$index]=$socket;//加入正在会话中
$this->sockets[]=$socket;//加入到监控数组
return "client";
}
}
/**
* 加入到客服中
* @param $socket
*/
function addResponse($socket){
$this->response[]=$socket;
$this->sockets[]=$socket;
}
/**
* 区分是客服还是请求
* @param $socket
* @param $ip
*/
public function diff($ip){
if($ip=="xxxxxx"){
//客服
return "response";
}else{
return "client";
}
}
/**
* 将原先等候的加入到正在会话中
* @param $loc
* @return bool
*/
private function changeStatus($loc){
if(empty($this->request)){
//等待队列为空
return false;
}else{
<span style="white-space:pre"> </span> foreach ($this->request as $k=>$v){
$index=$k;
break;
}
$this->client["$loc"]=$this->request["$index"];
$this->sockets[]=$this->request["$index"];
unset($this->request["$index"]);
return true;
}
}
//private function
}
$server=new Wserver('xxxxxx','10000');
$server->start();
<?php
/**
* Created by PhpStorm.
* User: changshuiwang
* Date: 2016/9/5
* Time: 14:46
*/
?>
<!DOCTYPE html>
<script src="jquery-1.6.2.min.js"></script>
<script>
$(function(){
var wsServer = 'ws://xxxx:xxxx';
var ws = new WebSocket(wsServer);
//alert("22");
ws.onopen = function(e){
//alert("连接成功");
}
ws.onclose = function(e){
alert("连接关闭");
}
ws.onerror = function (e) {
//alert("连接错误");
}
ws.onmessage = function (evt) {
//alert(evt);
//alert("22");
var data=JSON.parse(evt.data);
alert(data.message);
//var message=JSON.stringify(evt.data);
//alert(message);
//发送字符串,服务器端只需要unmask就可以了,如果是json串,后端就要json_decode
};
$(":button").click(function(){
var message=$("#message").val();
console.log(ws.send(message));
})
})
</script>
<body>
<div style="width:200px;height:600px;border-color:#0000FF;border: 2px solid;margin-bottom:20px;">
11
</div>
<form action="#">
<textarea id="message" name="message" style="width:200px;height:50px;resize:none;">
</textarea>
<input type="button" name="send" value="发送">
</form>
</body>
其实这只是一个小demo,不过写到这里就发现,完全没有写下去的必要了,php用来监听socket是用的socket_select(),对应的底层实现应该就是select机制,现在都用的epoll机制了。关于select()和epoll(),大家看看这篇我读过的最好的epoll讲解