PHP webSocket实现网页聊天室
一.简介
http请求只能由客户端主动发起,服务器响应的模式, 服务器无法主动向客户端推数据,websocket的出现完美的解决了这一问题。 websocket和http处于同一层,都是基于TCP协议的,客户端和服务器使用websocket通讯的时候需要握手和传输数据两步, 握手借助http状态码101 switch protocol从http协议转换到websocket协议,之后便和http协议无关了。
二.握手
websocket首先由浏览器主动发起一个http请求,主要请求头内容如下: Connection: 告知服务器当前请求连接是升级的 Upgrade: websocket Upgrade 告诉服务器这个http链接是升级的websocket链接 Sec-WebSocket-Version: 13 协议版本 Sec-WebSocket-Key: IYiYjdXLDgHybP4teMOnsQ== 验证key
服务器响应头如下 HTTP/1.1 101 Switching Protocols 表示变换协议 Upgrade: websocket Connection:Upgrade 服务器返回的告知客户端同意使用升级并使用websocket协议,用来完善HTTP升级响应 Sec-WebSocket-Accept:Ev/nT3aIpWH9deAfyYMPbBwkQWo= 客户端 Sec-WebSocket-Key经过加密后的字符串算法 base64_encode(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11));
三.数据幀构造和解析
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
构造协议文本幀的算法(PHP)
其中的opcode为1代表一个文本帧
private function encode($data){ $len = strlen($data); $encode = ''; if($len < 126){ $encode = chr(0x81) . chr($len) . $data; }else if($len >= 126 && $len <= 65535){ $low = $len & 0x00FF; $high = ($len & 0xFF00) >> 8; $encode = chr(0x81) . chr(0x7F) . chr($high) . chr($low) . $data; }else{ encode = chr(0x81) . } return $encode; }
如果playload len < 126,则playloadlen 是数据的真实长度 如果playload len = 126,数据的长度等于playload len后面2个字节对应的无符号整数就是数据的真实长度 如果playload len = 127,数据的长度等于playload len后面8个字节对应的无符号整数就是数据的真实长度
之前对位运算并不熟悉,这里也写下构建数据帧详细的步骤 php使用chr将数据转换为标准ascii所指定的单个字符 长度 < 126 FIN + RSV1 + RSV2 + RSV3 + opcode = 0x81 = 10000001, 再加上数据长度和数据 长度 >=126 <= 65535 FIN + RSV1 + RSV2 + RSV3 + opcode = 0x81 = 10000001 加上 Payload len = 0x7E = 126 由于ASCII范围为 0-127即1个字节,所以必须将2个字节拆分成单个字节即高位$high和低位$low来表示 $low = $len & (11111111 = 0x00FF)这样就取得了$len第二个字节的值。 因为$len是两个字节 取第一个字节的值需要 $len & (1111111100000000 = 0xFF00) 然后向右移8个位
解析文本帧的算法
private function decode($data){ if(!$data) return array(); //第一个字节和00001111按位与运算取得的后4位数据就是opcode $opcode = ord(substr($data, 0, 1)) & 0x0f; //第二个字节和10000000按位与运算,保留第一位的值,然后右移7位取得的就是ismask $ismask = (ord(substr($data, 1, 1)) & 0x80) >> 7; //第二个字节和01000000按位与运算取得后7位的值就是playloadlen $playloadlen = ord(substr($data, 1, 1)) & 0x7f; $cdata = $maskkey = $decode = ''; if($playloadlen < 126){ $maskkey = substr($data, 2, 4); $cdata = substr($data, 6); }else if($playloadlen == 126){ $maskkey = substr($data, 4, 4); $cdata = substr($data, 8); }else if($playloadlen == 127){ $maskkey = substr($data, 10, 4); $cdata = substr($data, 14); } if($cdata && $maskkey){ for($i = 0; $i < strlen($cdata); $i++){ $decode{$i} = $cdata{$i} ^ $maskkey[$i % 4]; } $decode = join('', $decode); } return array($opcode, $ismask, $decode); }
websocket规定客户端发送给服务端的数据必须经过掩码处理,服务器端发送给客户端的数据无需掩码处理, 解码算法: 将playload的原始数据的每个字符下标与4取模,然后将这个原始字符与前面取模后相应位置的掩码字符进行异或运算即可 data[i] = source[i] ^ maskkey[i / 4];
四.PHP服务端
之前对于socket的select方法也不是很了解,
function socket_select (array &$read, array &$write, array &$except, $tv_sec, [, int $tv_usec = 0 ])
1.新连接到来时,被监听的端口是活跃的,如果是新数据到来或者客户端关闭链接时,活跃的是对应的客户端socket而不是服务器上被监听的端口 2.如果客户端发来数据没有被读走,则socket_select将会始终显示客户端是活跃状态并将其保存在read数组中 3.如果客户端先关闭了,则必须手动关闭服务器上相对应的客户端socket,否则socket_select也始终显示该客户端活跃(这个道理跟”有新连接到来然后没有用socket_access把它读出来,导致监听的端口一直活跃”是一样的)
$read是一个引用变量,每次执行的时候传入我们需要监听的socket资源,执行过后,返回活跃的socket资源,核心伪代码如下
$socket = socketcreate(); $socket_select = array($socket); while(true){ $read = $socket_select; socket_select($read, $write, $except, null); foreach($read as $sock){ if($sock == $socket){//新连接到来时 }else{//客户端发送数据或者客户端关闭的时候 } } }
五.客户端
客户端websocket api就很简单了
// 创建一个 websocket 连接 var ws = new WebSocket("ws:XXXXX"); // websocket 创建成功事件 ws.onopen = function () { }; // websocket 接收到消息事件 ws.onmessage = function (e) { var msg = JSON.parse(e.data); } // websocket 错误事件 ws.onerror = function () { }; //websocket 关闭事件 ws.close = function () { };
也可以看看GoEasy文库的其他资料。