socket根本
基本的socket编程技能,如果你不知道,也不要慌,以防万一,我已经为大家准备好了,请参考PHP 编写基本的 Socket 程序
位运算由于在一样平常的php编程当中,很少碰着会有位操作的情形,以是遗忘和不熟习就天经地义了,我们可以参考php官方文档,但是我还是要讲一点,异或(^)操作,请看下面,这个结论很主要,请大家一定要记住,牢记牢记,主要的事情讲三遍。
a ^ b = c 可以推导出 c ^ b = a
二进制数据和文本数据
是不是有的时候打开一个文件显示乱码,就像下面这样
由于你打开的是二进制数据,二进制数据和文本数据的最根本的差异便是在数字的存储,举个例子,假设数字 int a=100,我们假设它会占用4个字节的空间,但是把稳了,如果将它作为字符串存储,结果只须要三个字节(每一位占用一个字节),文本软件不管这些啊,都当做文本,显示的内容就成了乱码了。因此如果某个二进制文件不是你写入的,想要解析它的内容,不太现实。
大端序和小端序,网络字节序之以是存在这种说法,是由于不同的CPU架构下,多字节数据在内容中的存储格式有所不同,这里我们以int(假设为4字节)数据m(数据采取16进制格式)为例,m=0x12345678,来进行解释,请仔细体会a,b,c,d的内存地址依次增大。
小端序,低字节存储在低位地址,高字节存储在高位地址,什么意思呢?此时0x78存储在a,0x56存储b,0x34存储c,0x12存储d。大端序,高位字节存储在低位,低位字节存储在高位,此时0x78存储在d,0x56存储c,0x34存储b,0x12存储a。网络字节序,网络字节序是大端字节序,这已经成为标准。
从上面的剖析可以知道,当我们从网络数据中解析多字节数据时,是一定要考虑字节的顺序的,这便是我这里着重强调的缘故原由。
协议的出身Websocket协议如今运用非常广泛,,造成这一征象的很大缘故原由,在于http协议的短暂性,客户端和做事器之间每一次的要求应答都须要建立TCP三次握手,这对付流量很大的做事器来说是非常胆怯的(系统级资源),以是这个时候websocket出身了,详细的出身日期是哪一年已经不得而知了,但是真正的标准化韶光是在2011年,由IETF正式完成,详细请参考RFC6455。
协议事情流程下面有一张图,可以解释这一点,该图片来自Google,
websocket协议和http协议都属于运用层协议(在TCP/IP之上),但是websocket协议相对付http协议多了一个握手(这个握手不这天常平常所说的tcp三次握手啊,把稳了)的过程,从上面的图可以很清晰地看出来,http是是一个文本协议,但是websocket有所不同,它有自己严格的字节格式,稍后会讲到。
数据包格式看到这张图,有没有想到TCP、IP的协议,不过这个图相对来说要大略一些,后面我会给大家详细地讲解每一部分的含义,逐步来,不要慌,慌个啥子额。
协议流程概览
该协议由2部分组成,握手和数据传输,握手部分并不繁芜,并且握手是建立在HTTP协议之上的,下面我们先来看一下协议的握手过程。
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
做事器相应如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
无论是要求或者是相应包,头部字段的顺序是没有哀求的,这个中有些字段相信大家都非常熟习了,就算不熟习,百度一下,还是很随意马虎搞清楚的,我们来仔细的谈论一下Websocket所特有的一些字段:
Upgrade字段这个字段表示须要升级到的协议,这个字段是必须的,并且它的值必须是websocket。
Connection这个字段表示须要升级协议,也是必须的,它的值必须是Upgrade。
Sec-WebSocket-Key和Sec-WebSocket-Accept这个是用来客户端和做事器握手利用的,必须通报,由于做事器会利用这个值进行一定的转换然后回传给客户端,客户端再检讨这个值, 是否和自己打算的值一样,如果不一样,那么客户端会认为,做事端是有问题的,那么结果只能是连接失落败了。在先容详细的操作之前,我们还须要先容一个常量GUID,它的值为258EAFA5-E914-47DA-95CA-C5AB0DC85B11,这个值是固定的,任何的Websocket做事器和客户端(包括浏览器)必须定义这个值。现在我们重点来看一下这个字段,如果客户端通报的值为 dGhlIHNhbXBsZSBub25jZQ==,那么用PHP代码来表示的话,就会是下面这样:
$GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";$sec_websocket_key = "dGhlIHNhbXBsZSBub25jZQ==";$result = base64_encode(sha1($sec_websocket_key . $GUID));
这个打算出的$result值终极会被回传给客户真个http相应头Sec-WebSocket-Accept,客户端会验证这个值,这个便是客户真个事了。
Sec-WebSocket-Versionwebsocket协议的版本号,根据RFC6455的文档,我们知道,这个值必须是13,其它的任何值都弗成,下面是它的描述:
The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13. NOTE: Although draft versions of this document (-09, -10, -11,and -12) were posted (they were mostly comprised of editorial changes and clarifications and not changes to the wire protocol), values 9, 10, 11, and 12 were not used as valid values for Sec-WebSocket-Version. These values were reserved in the IANA registry but were not and will not be used.
Sec-WebSocket-Protocol选择websocket所利用的子协议,这个字段不是必须的,取决于详细的实现,如果你利用的是Google浏览器的话,那么这个值是不会通报的。
握手阶段在讲解完了Websocket紧张的http头部字段之后,我们来看一下做事真个检讨代码,这里我把实例程序中的代码贴出来,给大家剖析一哈
/ @param $client_socket_handle @throws Exception / private function shakehand($client_socket_handle) { if (socket_recv($client_socket_handle, $buffer, 1000, 0) < 0) { throw new Exception(socket_strerror(socket_last_error($this->socket_handle))); } while (1) { if (preg_match("/([^\r]+)\r\n/", $buffer, $match) > 0) { $content = $match[1]; if (strncmp($content, "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key")) == 0) { $this->websocket_key = trim(substr($content, strlen("Sec-WebSocket-Key:")), " \r\n"); } $buffer = substr($buffer, strlen($content) + 2); } else { break; } } //相应客户端 $this->writeToSocket($client_socket_handle, "HTTP/1.1 101 Switching Protocol\r\n"); $this->writeToSocket($client_socket_handle, "Upgrade: websocket\r\n"); $this->writeToSocket($client_socket_handle, "Connection: upgrade\r\n"); $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Accept:" . $this->calculateResponseKey() . "\r\n"); $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Version: 13\r\n\r\n"); }
首先我们从客户端socket中读取1000字节的内容,这1000的字节足以读出所有的头部了(但是在企业级代码中,我们不能这么写,我们永久不能假设全体http头部有多大,在这篇博文中,我们为了突出问题的重点,简化了很多代码,但是你放心,对我们来说,丝毫没有影响,socket_recv请参考我上面所说的),接下来的while循环遍历我们读取到的内容,要看懂循环里面的代码,我们有必要提下http协议的格式了,看下图
我以为上面的图片,已经足以描述http协议的格式了,如果你还不懂,没紧要,给大家推举一篇来自简书的博文(HTTP协议格式详解),现在对付我们来说,最关心的是当前要求的Sec-WebSocket-Key头部,由于这个值须要返回给客户端,获取到这个值之后,我们把它存储在当前工具中。紧接着我们须要回应客户端吧,如果你不知道它的格式,我轻微讲一下:
对付websocket握手来说,如果做事端赞许客户真个连接的话,那么返回的状态码必须是101 ,至于后面的文本,不一定得是 Switching Protocol,只是别人都这么传,那就这么传了。其次,Upgrade: websocket,Connection: upgrade还有Sec-WebSocket-Version: 13,必须通报给客户端,这个是固定的,该当没有啥难度吧,其余的,Sec-WebSocket-Accept我们前面已经说了,它的打算代码,我上面已经贴出来了,这个打算办法也是固定的,千万不要忘却每一行后面得有\r\n啊,末了一行后面得有两个\r\n。
剖析数据协议看了上面握手的代码之后,是不是以为自己要上天了,觉得真是太大略了??骚年,醒醒,醒醒。哈哈,真是太年轻了,年轻便是好
看到我上面贴出来的websocket数据包格式了么,是时候解开它面纱的时候了,这部分可能有点儿难度,不要怕,有我在。下面我来来个原子级别的剖析。
FINFIN位,也是全体片段的第一个字节的最高位,他只能是0或者是1,这个位的浸染只有一个,如果它为1,表示这个片段是全体的末了一个片段,如果是0,表示这个片段之后,还有其它的片段。是不是听着直接懵逼了,啥是 片段?啥是 ?非常好,看来我装逼的时候已经来临了,废话不多说。为了搞清楚这几个观点,代码为敬
(new WebSocket()).send("我是奥巴马");
这是一段JAVASCRIPT代码,send函数的参数便是一条,非常短,但是把稳了,我们不能假设任何韶光,任何地点,都这么短,当它变得很长的时候,客户端就有可能对它进行切割,比如,我有一个字符串,大小为4M,我把它分为4个1M的字符串,那么每一个1M的字符串,就只能成为一个片段,每个片段独立发送,四个片段组合在一起形成了一条,每一个片段的格式都是固定的,格式和上面的贴图是一样的,按照刚才说的,前面的三个片段,FIN都是0,第四个才是1,清楚了么?So easy!!
RSV1,RSV2,RSV3这三位是保留给扩展利用的,基本不会用到,反正我没用到,以是我们可以把它们当做空气就行,永久设置为0,便是这么果断。
opcodeopcode顾名思义便是操作码,占用第一个字节的低四位,以是opcode可以代表16种不同的值。你是不是想问,opcode是用来干嘛的? opcode是用 来解析当前片段的载荷(携带的数据)的,详细的后面会再次解释。
0x00,表示当前片段是连续片段,这是啥意思呢?还记得上面谈论FIN的时候,一条被分割成多条片段?如果当前片段不是第一个,那么opcode必须设置为0。0x01,表示当前片段所携带的数据是文本数据(记得最开始说的文本数据和二进制数据的差异??),如果有多个片段的话,只须要在第一个片段设置该值,属于同一条中后面的片段,只须要设置为0即可。0x02,表示当前片段所携带的数据是二进制数据,如果有多个片段的话,只须要在第一个片段设置该值,属于同一条中后面的片段,只须要设置为0即可。0x03-0x07,保留给将来利用,也便是说暂时还没用到。0x08,表示关闭websocket连接,这个后面我会再一次讲到,先放着0x09,发送Ping片段,说白了,它紧张是用来检测远程端点是否还存活,我想检讨我的工具是不是已经去世了,但是这个片段可以携带数据,如果端点的一方发送了Ping,那么接管方,必须返回Pong片段,用中国人的话来说,便是礼尚往来嘛。0xA,发送Pong,用以回答Ping,是不是很大略?0xB-F,保留给将来利用,也便是说暂时还没用到。MASK表示当前片段所携带的数据是否经由加密,位置为第二个字节的最高位,统共1位,它的值不是你想设置就设置的啊,RFC6455 明确规定,所有从客户端发送给做事器的数据必须加密,以是mask的值必须是1。还有,所有从做事器发往客户真个数据,一定不能加密,以是呢,mask必须为0,便是这么大略粗暴。
Payload Length这部分是用来定义负载数据的长度的,统共7位,以是最大值为127,就这么大略?哼哼,不会的。
payloadlength<=125,此时数据的长度便是payloadlength的大小。payloadlength=126,那么紧接着payloadlength的2个字节,就用来表示数据的大小,以是当数据大小大于125,小于65535的时候,payload_length设置为126,后面剖析代码的时候,我会再次讲到。payloadlength=127,也便是payloadlength取最大值,那么紧接着payload_length的8个字节,就用来表示数据的大小,此可以表示的数据可就相称大了,后面剖析代码的时候,我会再次讲到。Mask key它的位置紧接着数据长度的后面,大小为0或者是4个字节。前面剖析了mask的浸染,如果mask为1的话,数据须要加密,此时mask key占用4个字节,否则长度为0,至于mask key如何用来解密数据的,后面会再次讲到。
payload data这里便是我们从客户端吸收到的数据,不过它是经由加密的,“我是奥巴马”,之前payload_length的长度,便是经由加密之后的数据的长度,而不是原始数据的长度。
讲解完上面的内容之后,我们可以开始剖析如何用php来解析Websocket片段了。
解析数据包
这篇博文的开头我就说过了,当前的websocket实现会专注于websocket最为精华,最困难的部分,以是会忽略掉一些内容,如果你理解了下面讲的内容,别的的一些细枝末节都不是问题。
打算数据的长度//等待客户端新传输的数据 if (!socket_recv($client_socket_handle, $buffer, 1000, 0)) { throw new Exception(socket_strerror(socket_last_error($client_socket_handle))); } //解析的长度 $payload_length = ord($buffer[1]) & 0x7f;//第二个字符的低7位 if ($payload_length >= 0 && $payload_length < 125) { $this->current_message_length = $payload_length; $payload_type = 1; echo $payload_length . "\n"; } else if ($payload_length == 126) { $payload_type = 2; $this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff); echo $this->current_message_length; } else { $payload_type = 3; $this->current_message_length = (ord($buffer[2]) << 56) | (ord($buffer[3]) << 48) | (ord($buffer[4]) << 40) | (ord($buffer[5]) << 32) | (ord($buffer[6]) << 24) | (ord($buffer[7]) << 16) | (ord($buffer[8]) << 8) | (ord($buffer[7]) << 0); }
对付上面的代码,下面进行逐行解析
$payload_length = ord($buffer[1]) & 0x7f;//第二个字符的低7位
读取第二个字节的低7位,也便是之前谈论的payload_length,0x7f转换为二进制便是01111111,ord($buffer[1]) 便是把第二个字符转换为对应的ASCII数值,两个进行与运算,就可以得到第二个字节的低7位对应的数值(与运算不熟习的朋友,请先查看我在这篇博文前面给大家指定的链接),
if ($payload_length >= 0 && $payload_length < 125) { $this->current_message_length = $payload_length; $payload_type = 1; echo $payload_length . "\n"; }
当payload_length的长度小于125的话,数据长度就即是片段长度。
if ($payload_length == 126) { $payload_type = 2; $this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff); echo $this->current_message_length; }
当payload_length的长度即是126的时候,就有些麻烦了,此时第3和第4个字节组合为一个无符号16位整数,还记得我们之前说的,网络字节序吗?高位字节在前,低位字节在后面,以是当我们读的时候,第3个字节便是高8位,第4个字节便是低8位,以是我们首先将高8位左移8位再和低8位做或运算。
$payload_type = 3;$this->current_message_length = (ord($buffer[2]) << 56) | (ord($buffer[3]) << 48) | (ord($buffer[4]) << 40) | (ord($buffer[5]) << 32) | (ord($buffer[6]) << 24) | (ord($buffer[7]) << 16) | (ord($buffer[8]) << 8) | (ord($buffer[9]) << 0);
当payload_length的长度即是127的时候,此时的第3到第10位组合为一个无符号64位整数,以是最高的8位须要左移56位,后面的依次类推,低8位保持不动。
解析mask key//解析掩码,这个必须有的,掩码统共4个字节$mask_key_offset = ($payload_type == 1 ? 0 : ($payload_type == 2 ? 2 : 8)) + 2;$this->mask_key = substr($buffer, $mask_key_offset, 4);
要找到maskey,首先必须找到它在当前片段的偏移,如果payloadlength<=125,那么偏移便是2,如果payloadlength==126,那么偏移便是(2+2)=4,如果payload_length>126,那么偏移便是(2+8)=10,同时mask key的大小为4个字节,以是找到了偏移和长度,mask key就可以获取到了。
解密数据//获取加密的内容$real_message = substr($buffer, $mask_key_offset + 4);$i = 0;$parsed_ret = '';//解析加密的数据while ($i < strlen($real_message)) { $parsed_ret .= chr((ord($real_message[$i]) ^ ord(($this->mask_key[$i % 4])))); $i++;}
解密数据的第一步便是要找到加密数据在当前片段中的偏移,很大略,这个值即是maskkey的偏移(上面已经求过了)+maskkey本身的长度4,那么怎么来解密数据呢?看上面的代码,就可以看出来,解密的过程实在便是遍历加密数据的每一个字符的ASCII值和数据(当前遍历的位置对4取模,得出的数据必定是0,1,2,3,将得出的数据找到maskkey对应位置的ASCII值)进行异或运算求得,这个算法是RFC6455规定的,全天下都是这样。
返回数据给客户端从客户端发送到做事器和做事器通报给客户真个数据格式都遵照着同样的数据包格式,以是在我的实现中,代码如下:
function echoContentToClient($client_socket, $content){ $len = strlen($content); //第一个字节 $char_seq = chr(0x80 | 1); $b_2 = 0; //fill length if ($len > 0 && $len <= 125) { $char_seq .= chr(($b_2 | $len)); } else if ($len <= 65535) { $char_seq .= chr(($b_2 | 126)); $char_seq .= (chr($len >> 8) . chr($len & 0xff)); } else { $char_seq .= chr(($b_2 | 127)); $char_seq .= (chr($len >> 56) . chr($len >> 48) . chr($len >> 40) . chr($len >> 32) . chr($len >> 24) . chr($len >> 16) . chr($len >> 8) . chr($len >> 0)); } $char_seq .= $content; $this->writeToSocket($client_socket, $char_seq);}
为了简便起见,第一个字节中FIN=1,opcode设置为1,接下来检讨数据的长度,这部分内容和解析数据长度的步骤刚好相反,就不再剖析了,如果你把之前的都看懂了,这里也该当没有问题,但是特殊把稳了,之前我们就已经提到过,做事器返回给客户真个数据,不能加密,以是mask必须设置为0,mask key的长度为0。
运行实例就和本篇博文开篇所提到的,我写了一个大略的websocket实现,请一定要下载自己运行起来,光看是没有用的:php-websocket-base-implemention
如何运行websocket做事器
为了你可以看到实际运行的结果,请打开websocket.html文件,页面上涌现这个就表示运行成功了。
运行之前,请检讨端口8080是否被占用,当然你可以修正websocket.html,改为其他的都可以,确保不被占用就可以了,如果你仍旧无法运行,请联系我,如果你想看到其他的内容,也请修正websocket.html文件,然后重启做事器。