备案网站主办单位冲突/网站优化推广招聘
首先感谢https://www.cnblogs.com/saonian/p/5504456.html和https://www.cnblogs.com/chyingp/p/websocket-deep-in.html这两篇文章,让我能够理解和写出websocket的握手和消息编解码部分
socket相关部分可见前文https://my.oschina.net/u/3470006/blog/4791421,本文基于以上进行websocket的开发
websocket相比于之前所说的多了握手和消息编解码,其它都是相同的,先展示基础socket代码
$Ip='127.0.0.1';
$Port='2888';
$Server=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($Server,$Ip,$Port);
socket_listen($Server);
do{
$Client=socket_accept($Server);
//消息处理
socket_close($Client);
}
while(true);
服务器需要对客户端的第一个消息进行处理,就是传统的http get请求,自己要提取其中的Sec-WebSocket-Key部分,对其进行加密处理,过程是将提取的key和一个规定的字符串直接连接,然后进行sha1加密得到二进制结果再用base64编码,再将结果以传统http响应格式进行输出,这里将其编写为一个单独的函数
function HandMessageHandle($Message,$Client){
echo '开始握手'.PHP_EOL;
$Message=explode("\r\n",$Message);
$Key='';
foreach ($Message as $Item){
//固定提取Key
if(strpos($Item,'Sec-WebSocket-Key: ')>-1){
$Key=substr($Item,19);
break;
}
}
//握手就是用客户端key与固定字符串
$HandString='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$HandKey=base64_encode(sha1($Key.$HandString,true));
//发送握手响应
$HandResponse="http/1.1 101 switching protocols
upgrade:websocket
connection:upgrade
sec-webSocket-accept:{$HandKey}
";
socket_write($Client,$HandResponse);
}
后续就是服务端和客户端之间的直接的消息交互了,消息交互是基于数据帧的,这部分略微复杂些
首先是消息的解码,简单来说(数据帧解析可见https://www.cnblogs.com/chyingp/p/websocket-deep-in.html)
数据的第1个字节为标识部分
第2个字节写了掩码和数据长度
再后面可能为0或4字节是掩码的key,没有掩码就是0字节,有就是4字节
再后面就是正式的数据部分,分为扩展数据和应用数据,没有声明扩展就全是应用数据
知道了消息的构成后就可以进行消息的解析了,以下为解码函数
function MessageDecode($Message){
//暂不考虑数据分片的问题
//前两个字节可以忽略,客户端发来的消息掩码key是固定的4字节
$MaskKey=substr($Message,2,4);
//截取正式消息部分,跳过固定的2+4个字节,这里认为没有扩展数据全是应用数据
$MessageData=substr($Message,6);
//解码过程为对数据部分逐字节与掩码key的(数据字节索引模4)字节进行异或运算
$ClientMessage='';
for($i=0;$i
$ClientMessage.=$MessageData[$i]^$MaskKey[$i%4];
}
return $ClientMessage;
}
服务端发送的消息不需要进行掩码操作,为简略我处理不使用数据分片,于是可写出消息的编码函数
function MessageEncode($Message){
//不采用数据分片(1),不使用扩展(000),文本帧(0001)
$FirstByte=chr(bindec('10000001'));
//不使用掩码(0),声明数据长度,前面的0可直接忽略
$SecondByte=chr(bindec(decbin(strlen($Message))));
return $FirstByte.$SecondByte.$Message;
}
放出完整的服务端代码
$Ip='127.0.0.1';
$Port='2888';
$Server=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($Server,$Ip,$Port);
socket_listen($Server);
do{
$Client=socket_accept($Server);
//消息处理
MessageHandle($Client);
socket_close($Client);
}
while(true);
function MessageHandle($Client){
//处理握手消息
$HandMessage=socket_read($Client,8192);
HandMessageHandle($HandMessage,$Client);
echo '握手完成'.PHP_EOL;
while (false!==$Message=socket_read($Client,8192)){
//获取客户端消息
$ClientMessage=MessageDecode($Message);
echo '接收到客户端消息:'.$ClientMessage,PHP_EOL;
//发送一个回应消息
socket_write($Client,MessageEncode('你发送了消息:'.$ClientMessage));
}
}
function HandMessageHandle($Message,$Client){
$Message=explode("\r\n",$Message);
$Key='';
foreach ($Message as $Item){
//固定提取Key
if(strpos($Item,'Sec-WebSocket-Key: ')>-1){
$Key=substr($Item,19);
break;
}
}
//握手就是用客户端key与固定字符串
$HandString='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$HandKey=base64_encode(sha1($Key.$HandString,true));
//发送握手响应
$HandResponse="http/1.1 101 switching protocols
upgrade:websocket
connection:upgrade
sec-webSocket-accept:{$HandKey}
";
socket_write($Client,$HandResponse);
}
function MessageDecode($Message){
//暂不考虑数据分片的问题
//前两个字节可以忽略,客户端发来的消息掩码key是固定的4字节
$MaskKey=substr($Message,2,4);
//截取正式消息部分,跳过固定的2+4个字节,这里认为没有扩展数据全是应用数据
$MessageData=substr($Message,6);
//解码过程为对数据部分逐字节与掩码key的(数据字节索引模4)字节进行异或运算
$ClientMessage='';
for($i=0;$i
$ClientMessage.=$MessageData[$i]^$MaskKey[$i%4];
}
return $ClientMessage;
}
function MessageEncode($Message){
//不采用数据分片(1),不使用扩展(000),文本帧(0001)
$FirstByte=chr(bindec('10000001'));
//不使用掩码(0),声明数据长度,前面的0可直接护忽略
$SecondByte=chr(bindec(decbin(strlen($Message))));
return $FirstByte.$SecondByte.$Message;
}
而客户端的代码是很简单,就没必要做什么解析了
Websocketlet Ip='127.0.0.1';
let Port='2888';
let WS=new WebSocket(`ws://${Ip}:${Port}`)
WS.onopen=function () {
console.log("已连接");
};
WS.onclose=function (Info) {
console.log("已关闭",Info);
}
WS.οnerrοr=function (Error) {
console.log("错误",Error);
}
WS.onmessage=function (Message) {
console.log("消息",Message);
}
命令行运行服务端程序,再把客户端的网页打开在控制台就能看到已连接的提示,可以直接在控制台使用WS.send("你想发送的消息")来发送消息