这几天研究了下php实现webSocket的方法,网上查了不少博文,涉及到的知识点不少。但却非常值得学,因为这方面典型的应用场景非常的多,消息推送,聊天室,所有需要长连接的地方都会用到它。
当然可能有人会说有Workerman的框架,不用自己实现它,从工作角度来看,没错。
但是,框架封装的东西屏蔽了原生的实现方法,看着是简单,但利于工作却并不利于学习,我学习它不是为了项目使用,而是想真正了解webSocket的实现原理
有太多技术披上了名为框架的糖衣炮弹,华丽的同时把你死死和框架焊在一起
我不觉得会用了框架的简单实例就懂了webSocket,那只是写框架的人懂了,你只是流水线上可随 意替换的工人。
在本博文中,我将假定看我博文的所有人都是毫无webSocket编程经验的人(不然为什么要看啊!),我会尽我所能把需要了解的知识点都说清楚,让与我同样困惑的人真正了解什么是webSocket。
而因为webSocket是socket编程的子集,即属于socket编程范围内,所以在了解webSocket之前,要先学什么是socket套接字!
照着这个思路我找了好几天,终于有些收获,于是想分享一下,共同进步
本篇博文后面会用一个聊天室的例子说明,上面的注释多到丧心病狂。但这个例子是某个博主的,并不是我写的,我声明一下,我只是借来用,并加了注释。
我转了很多个博文的内容为自己写下这个学习总结,记录下所有知识点,可能有点长,读完需要点耐心。
原文链接:http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html
(如果对TCP/IP协议没啥实感的话,推荐读一读阮大神的文章)
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
这里有一张图,表明了这些协议的关系。
TCP/IP协议族包括运输层、网络层、链路层。现在你知道TCP/IP与UDP的关系了吧。
在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
你会使用它们吗?
前人已经给我们做了好多的事了,网络间的通信也就简单了许多,但毕竟还是有挺多工作要做的。以前听到Socket编程,觉得它是比较高深的编程知识,但是只要弄清Socket编程的工作原理,神秘的面纱也就揭开了。
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理,也许TCP/IP协议族就是诞生于生活中,这也不一定。
阮一峰老师微博中讲到:socket就是插座。服务器的socket,就是服务器提供插座,等着客户端的插头插进来。一旦插入完成,服务器-客户端的通信就建立了。
socket套接字编程是网络库的相关API,用来做网络通信,很多高级语言都能调用socket套接字进行网络编程
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
转载:http://blog.csdn.net/hguisu/article/details/7453390
概念理解
在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
同步/异步主要针对C端:
同步:
所谓同步,就是在c端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当c端一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕
阻塞/非阻塞主要针对S端:
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。
快递的例子:比如到你某个时候到A楼一层(假如是内核缓冲区)取快递,但是你不知道快递什么时候过来,你又不能干别的事,只能死等着。但你可以睡觉(进程处于休眠状态),因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
还是等快递的例子:如果用忙轮询的方法,每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。如果没来,立即返回。而快递来了,就放在A楼一层,等你去取。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
1. 同步,就是我客户端(c端调用者)调用一个功能,该功能没有结束前,我(c端调用者)死等结果。
2. 异步,就是我(c端调用者)调用一个功能,不需要知道该功能结果,该功能有结果后通知我(c端调用者)即回调通知。
同步/异步主要针对C端, 但是跟S端不是完全没有关系,同步/异步机制必须S端配合才能实现.同步/异步是由c端自己控制,但是S端是否阻塞/非阻塞, C端完全不需要关心.
3. 阻塞, 就是调用我(s端被调用者,函数),我(s端被调用者,函数)没有接收完数据或者没有得到结果之前,我不会返回。
4. 非阻塞, 就是调用我(s端被调用者,函数),我(s端被调用者,函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
同步和异步都只针对于本机SOCKET而言的。
同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
阻塞和非阻塞是指当server端的进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;
而同步和异步是指client端访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。(等待"通知")
转自https://www.cnblogs.com/loveyoume/p/6076101.html
socket相关函数:
----------------------------------------------------------------------------------------------
socket_accept() 接受一个Socket连接
socket_bind() 把socket绑定在一个IP地址和端口上
socket_clear_error() 清除socket的错误或者最后的错误代码
socket_close() 关闭一个socket资源
socket_connect() 开始一个socket连接
socket_create_listen() 在指定端口打开一个socket监听
socket_create_pair() 产生一对没有区别的socket到一个数组里
socket_create() 产生一个socket,相当于产生一个socket的数据结构
socket_get_option() 获取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_last_error() 获取当前socket的最后错误代码
socket_listen() 监听由指定socket的所有连接
socket_read() 读取指定长度的数据
socket_readv() 读取从分散/聚合数组过来的数据
socket_recv() 从socket里结束数据到缓存
socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socket
socket_recvmsg() 从iovec里接受消息
socket_select() 多路选择
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_write() 写数据到socket缓存
socket_writev() 写数据到分散/聚合数组
socket编程就是要我们自己创建服务端和客户端,也就是说,``socket编程``——就是要我们自己建立一个类似于mysql的服务端和客户端的应用。
说到这里,我想问一句,你说这socket让人头疼不?它既不建立个服务端,也不建立个客户端给我们应用,非要让我们自己去应用socket的函数,创建一个属于我们自己的网络协议套接应用,这是不是很让你头疼呢?头疼也没办法,要是你需要自己的应用,你还是不得不跟socket打交道。呵呵,这只是题外话,不多说,下面进入正题。
在你没有被socket编程搞蒙之前,我还是让你看看socket的几个关键函数,先给你解释一下它们各自的作用。不然,要是对socket编程一点基础都没有的人看到了,我怕你看了之后,就果断跳过这篇文章,从此对socket产生恐惧症了。呵呵,又多说了。
socket的关键函数1:
socket_create($net参数1,$stream参数2,$protocol参数3)
作用:创建一个socket套接字,说白了,就是一个网络数据流。
返回值:一个套接字,或者是false,参数错误发出E_WARNING警告
php的在线手册那里说得更清楚:
socket_create创建并返回一个套接字,也称作一个通讯节点。一个典型的网络连接由 2 个套接字构成,一个运行在客户端,另一个运行在服务器端。
上面一句话是从php在线手册那里复制过来的。看到没有,这里说得意思是不是和我上面反反复复提到的客户端与服务端一模一样?呵呵。
参数1是:网络协议,
网络协议有哪些?它的选择项就下面这三个:
AF_INET: IPv4 网络协议。TCP 和 UDP 都可使用此协议。一般都用这个,你懂的。
AF_INET6: IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX: 本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。
参数2:套接字流,选项有:
SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW SOCK_RDM。
这里只对前两个进行解释:
SOCK_STREAM TCP 协议套接字。
SOCK_DGRAM UDP协议套接字。
欲了解更多请链接这里:http://php.net/manual/zh/function.socket-create.php
参数3:protocol协议,选项有:
SOL_TCP: TCP 协议。
SOL_UDP: UDP协议。
从这里可以看出,其实socket_create函数的第二个参数和第三个参数是相关联的。
比如,假如你第一个参数应用IPv4协议:AF_INET,然后,第二个参数应用的是TCP套接字:SOCK_STREAM,
那么第三个参数必须要用SOL_TCP,这个应该不难理解。
TCP 协议套接字嘛,当然只能用TCP协议了,是不是?如果你应用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的函数返回值
这八个函数是socket的核心函数,下面列举两个个比较重要的函数
socket_last_error($socket),参数为socket_create的返回值,作用是获取套接字的最后一条错误码号,返回值套接字code
socket_strerror($code),参数为socket_last_error函数的返回值,获取code的字符串信息,返回值也就是套接字的错误信息
这两个函数在socket编程中还是很重要的,在写socket编程的时候,我觉得你还是得利用起来,特别是新手,可以当做调试用
应用层协议(application layer protocol)定义了运行在不同端系统上(客户端、服务端)的应用程序进程如何相互传递报文,例如HTTP、WebSocket都属于应用层协议。例如一个简单的应用层次协议可以如下{"module":"user","action":"getInfo","uid":456}\n"
。此协议是以"\n"
(注意这里"\n"
代表的是回车)标记请求结束,消息体是字符串。
:阮大神互联网协议入门
短连接是指通讯双方有数据交互时,就建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。像WEB网站的HTTP服务一般都用短连接。
长连接,指在一个连接上可以连续发送多个数据包。数据交互期间不断开
就像任何一个人都有名字,任何一个位置都有坐标一样。套接字也是用序号(指针)来区分彼此的。这就是文件描述符,每个套接字的文件描述符都不同,但它不是地址,而是地址指向,你看,去工厂上班的工人都有员工编号吧,但他具体在哪工作编号上可没写,需要从编号再往下开始查。
如感兴趣具体请看http://www.php.cn/linux-369356.html这篇文章
转自官方例子:https://www.php.net/manual/zh/book.sockets.php
服务器端代码 sockectServers.php:
用控制台打开服务
客户端页面 clientSocket.php
1, "usec" => 0));
//发送套接流的最大超时时间为6秒
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));
/****************设置socket连接选项,这两个步骤你可以省略*************/
//连接服务端的套接流,这一步就是使客户端与服务器端的套接流建立联系
if(socket_connect($socket,'127.0.0.1',8888) == false){
echo 'connect fail massege:'.socket_strerror(socket_last_error());
}else{
$message = 'l love you 我爱你 socket';
//转为GBK编码,处理乱码问题,这要看你的编码情况而定,每个人的编码都不同
$message = mb_convert_encoding($message,'GBK','UTF-8');
//向服务端写入字符串信息
if(socket_write($socket,$message,strlen($message)) == false){
echo 'fail to write'.socket_strerror(socket_last_error());
}else{
echo 'client write success'.PHP_EOL;
//读取服务端返回来的套接流信息
while($callback = socket_read($socket,1024)){
echo 'server return message is:'.PHP_EOL.$callback;
}
}
}
socket_close($socket);//工作完毕,关闭套接流
?>
结果:
WebSocket是什么,有什么优点
WebSocket是一个持久化的协议,这是相对于http非持久化来说的。
举个简单的例子,http1.0的生命周期是以request作为界定的,也就是一个request,一个response,对于http来说,本次client与server的会话到此结束;而在http1.1中,稍微有所改进,即添加了keep-alive,也就是在一个http连接中可以进行多个request请求和多个response接受操作。然而在实时通信中,并没有多大的作用,http只能由client发起请求,server才能返回信息,即server不能主动向client推送信息,无法满足实时通信的要求。而WebSocket可以进行持久化连接,即client只需进行一次握手,成功后即可持续进行数据通信,值得关注的是WebSocket实现client与server之间全双工通信,即server端有数据更新时可以主动推送给client端。
这里还需要说明一个关键函数将在下个代码示例中使用:function socket_select (array &$read, array &$write, array &$except, $tv_sec, $tv_usec = null)
转自:https://blog.csdn.net/Im_KK/article/details/45033533
作用:
传入需要监听的一组socket套接字描述符(或是几组?),监听他们的变化,并且把不活跃的从传入的数组中删除。
array $参数可以理解为一个数组,这个数组中存放的是文件描述符(上面有解释)。当它有变化(就是有新消息读写或者有客户端连接/断开或是报错)时,socket_select函数才会返回,继续往下执行。
$read数组中活动的socket,并且把不活跃的从read数组中删除
$write是监听是否有客户端写数据,传入NULL是不关心是否有写变化。
$except是$sockets里面要被排除的元素,传入NULL是”监听”全部。
最后一个参数是超时时间
如果为0:则立即结束
如果为n>1: 则最多在n秒后结束,如遇某一个连接有新动态,则提前返回
如果为null:如遇某一个连接有新动态,则返回
说明:
1 新连接到来时,被监听的端口是活跃的,如果是新数据到来或者客户端关闭链接时,活跃的是对应的客户端socket而不是服务器上被监听的端口
2 如果客户端发来数据没有被读走,则socket_select将会始终显示客户端是活跃状态并将其保存在readfds数组中
3 如果客户端先关闭了,则必须手动关闭服务器上相对应的客户端socket,否则socket_select也始终显示该客户端活跃(这个道理跟"有新连接到来然后没有用socket_access把它读出来,导致监听的端口一直活跃"是一样的)
---------------------
要实现WebSocket协议,首先需要浏览器主动发起一个HTTP请求,就是握手过程。
这个请求头包含“Upgrade”字段,内容为“websocket”(注:upgrade字段用于改变HTTP协议版本或换用其他协议,这里显然是换用了websocket协议),还有一个最重要的字段“Sec-WebSocket-Key”,这是一个随机的经过base64
编码的字符串,像密钥一样用于服务器和客户端的握手过程。一旦服务器君接收到来自客户端的upgrade请求,便会将请求头中的“Sec-WebSocket-Key”字段提取出来,追加一个固定的“魔串”:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,并进行SHA-1
加密,然后再次经过base64
编码生成一个新的key,作为响应头中的“Sec-WebSocket-Accept”字段的内容返回给浏览器。一旦浏览器接收到来自服务器的响应,便会解析响应中的“Sec-WebSocket-Accept”字段,与自己加密编码后的串进行匹配,一旦匹配成功,便有建立连接的可能了(因为还依赖许多其他因素)。
何为 固定的“魔串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ?
这个字符串是websocket协议钦定的握手协议校验的算法需要用到的值,没有他就完不成握手,websocket无法建立通信
服务器此时需要返回一个特定的值给客户端,如果值传不对就是握手失败
下面是需要传的值的官方固定算法:
服务器需要传的值 = Base64编码函数(散列函数(客户端发来的Sec-WebSocket-Key的值 + 魔串)) :
即:
结果 = Base64_code(sha1 算法(XXVGPIXziGdC1a2mYSFECA== + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ))
$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
服务器此时需要拼一个成功握手的返回信息才行,在php中就是拼一个字符长串,把 计算的结果塞进去发送给浏览器
PHP代码:
$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));//经过散列加密后形成新的key值,散列加密的密钥是第二个参数
//以下是字符串拼一个服务器相应的协议信息(客户端请求的是webSokect升级协议,我们要拼出一个成功建立连接的信息响应给客户端)
//按照协议组合信息进行返回
$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));
WebSocket传输的数据都是以Frame
帧(帧,听不懂?就是比特位!一位二进制数就是一比特位。值只有0和1,一字节是8比特位)的形式实现的,就像TCP/UDP协议中的报文段Segment
。下面就是一个Frame:(以bit为单位表示)
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 ... |
+---------------------------------------------------------------+
按照RFC中的描述:
FIN: 1 bit
表示这是一个消息的最后的一帧。第一个帧也可能是最后一个。
%x0 : 还有后续帧
%x1 : 最后一帧
RSV1、2、3: 1 bit each
除非一个扩展经过协商赋予了非零值以某种含义,否则必须为0
如果没有定义非零值,并且收到了非零的RSV,则websocket链接会失败
Opcode: 4 bit
解释说明 “Payload data” 的用途/功能
如果收到了未知的opcode,最后会断开链接
定义了以下几个opcode值:
%x0 : 代表连续的帧
%x1 : text帧
%x2 : binary帧
%x3-7 : 为非控制帧而预留的
%x8 : 关闭握手帧
%x9 : ping帧
%xA : pong帧
%xB-F : 为非控制帧而预留的
Mask: 1 bit
定义“payload data”是否被添加掩码
如果置1, “Masking-key”就会被赋值
所有从客户端发往服务器的帧都会被置1
Payload length: 7 bit | 7+16 bit | 7+64 bit
“payload data” 的长度如果在0~125 bytes范围内,它就是“payload length”,
如果是126 bytes, 紧随其后的被表示为16 bits的2 bytes无符号整型就是“payload length”,
如果是127 bytes, 紧随其后的被表示为64 bits的8 bytes无符号整型就是“payload length”
Masking-key: 0 or 4 bytes
所有从客户端发送到服务器的帧都包含一个32 bits的掩码(如果“mask bit”被设置成1),否则为0 bit。一旦掩码被设置,所有接收到的payload data都必须与该值以一种算法做异或运算来获取真实值。(见下文)
Payload data: (x+y) bytes
它是"Extension data"和"Application data"的总和,一般扩展数据为空。
Extension data: x bytes
除非扩展被定义,否则就是0
任何扩展必须指定其Extension data的长度
Application data: y bytes
占据"Extension data"之后的剩余帧的空间
注意:这些数据都是以二进制形式表示的,而非ascii编码字符串
Masking-key为掩码,而Payload data里的就是加密过的数据了,即你要发送的数据
但是这个数据Payload data是加了密的,需要解密才行,而解密就用到 掩码:Masking-key
过程:Masking-key与Payload data做异或位运算取得真实数据
下面给一段数据解密的代码(java),也叫所谓的掩码解码逻辑,这个跟子网掩码压根并不是一回事:
具体例子转自https://blog.csdn.net/u011499747/article/details/83055845
比如:
掩码为:30 6c e2 9a
掩码后的16进制数据是(待解密真实数据) :54 19 80 f8 49
解码出的数据是(解密出真实数据):dubby(16进制编码为 64 75 62 62 79)
java模拟掩码后数据通过掩码解密出真实数据过程:
//掩码
byte[] maskingKeyBytes = {(byte) 0x30, (byte) 0x6c, (byte) 0xe2, (byte) 0x9a};
//掩码编码过得payload
byte[] maskedBytes = {(byte) 0x54, (byte) 0x19, (byte) 0x80, (byte) 0xf8, (byte) 0x49};
int length = maskedBytes.length;
//解码的结果
byte[] unmaskedByte = new byte[length];
for (int i = 0; i < length; ++i) {
byte masking = maskingKeyBytes[i % 4];
unmaskedByte[i] = (byte) (maskedBytes[i] ^ masking);
}
for (byte b : unmaskedByte) {
System.out.print(b + " ");
}
规范里解释了Masking-key
掩码的作用了:就是当mask
字段的值为1时,payload-data
字段的数据需要经这个掩码进行解密。
在处理数据之前,我们要清楚一件事:服务器推送到客户端的消息中,mask
字段是0,也就是说Masking-key
为空。这样的话,数据的解析就不涉及到掩码,直接使用就行。
但是我们前面提到过,如果消息是从客户端发送到服务器,那么mask
一定是1,Masking-key
一定是一个32bit的值。下面我们来看看数据是如何解析的:
当消息到达服务器后,服务器程序就开始以字节为单位逐步读取这个帧,当读取到payload-data
时,首先将数据按byte依次与Masking-key
中的4个byte按照如下算法做异或
源码改自https://www.cnblogs.com/jiangzuo/p/5896301.html
webSocket聊天室服务器代码:server.php
run();
/*这里再梳理一下:
每一个sokcet节点就是一个网络的通信单位,比如:小明和小红通话,那么小明是socket节点,小红也是socket节点。
写(write相关方法)某个socket节点就是往这个节点发送数据,读(read相关方法)某个socket节点就是向这个节点接收数据。
每个sokcet节点在php中表示为socket套接字描述符,其实打印出来就是个序号,为了区分每个socket节点唯一,引用它就引用了这个socket指向的客户端,可以读写这个客户端的数据。
通过socket_create方法返回的值就是名为socket套接字描述符的字符串,可以理解为它是标识了一个通信节点地址的句柄,通俗理解为指向一个 通信节点 (它可能是 客户端 或 服务器端 总之就是个 sokcet节点,一个通信最小单位!)
每个socket节点不互通,需要建立一个服务器socket节点(需绑定一个ip和端口作为服务器地址),把所有其他需要沟通的soket节点连接上这个服务器socket节点。
链接上后,所有其他socket可以给服务器socket发送数据,也可以从服务器得到数据,同理服务器socket也可以给任何一个连接上自己的其他socket节点发送数据和接收数据
这样所有socket节点发送的数据,服务器socket节点将全部收到,服务器收到数据再发送给某个socket节点,
想实现socket节点与节点间的间接通信,服务器就要充当类似邮局一样的功能,这个转发数据的功能是靠自己编码实现的。
webSocket协议是应用层协议,而socket对应网络层,更加底层,说白了!应用层的协议就是一种书面约定!传数据还是底层socket而已。
而 webSocket本质只是一个tcp/IP协议的长链接形式,说白了!长链接是啥?不就是网络请求不调用close方法关闭连接就是长连接,每次请求完就断开的就不是长链接
都是普通套接字编程,只是客户端需要配合服务器端不断开连接,所以需要告诉客户端(某个浏览器内核),我连你了,你别断开。这份通知就是所谓的websocket协议
说白了就是一个约定,又叫握手。
过程:
1,浏览器端:服务器!服务器!我的这个socket节点我会一直连着,不断开!我要连你了!(通过'ws://服务器地址:端口找到服务器socket节点,发送升级webSocket协议请求)
2,服务器端:我知道了,你连上我了,我通过了,你放心吧。(通过socket_accept方法得到客户端节点,服务器socket节点写给客户端节点协议升级成功的字符串)
这就完事了,其实只是建立了TCP/IP的普通socket连接而已,后台还是操纵soket读写数据,因为连接未断开,后台写的数据前台能马上取到
从客户端自己建立并连接的服务器来时,每个客户端都自己建立了一个socket节点,这个浏览器实现了,不需要服务器管,服务器仅需要获取这个socket套接字描述符,
服务器一般是通过socket_accept方法来获取这个客户端的socket节点。得到节点就可以读写客户端数据了,就像操作普通文件那样。
以下代码就是利用服务器socket节点的特性来实现一个聊天室的websocket服务器Demo代码
*/
//下面是sock类
class Sock{
public $sockets; //socket的连接池,即client连接进来的socket标志
public $users; //所有client连接进来的信息,包括socket、client名字等
public $master; //socket的resource,即前期初始化socket时返回的socket资源
private $sda=array(); //已接收的数据
private $slen=array(); //数据总长度
private $sjen=array(); //接收数据的长度
private $ar=array(); //加密key
private $n=array();
public function __construct($address, $port){
//创建socket并把保存socket资源在$this->master:通过建立套接字服务器,返回套接字描述符字符串,并把它付给变量master
$this->master=$this->WebSocket($address, $port);
//创建socket连接池:创建数组,并把服务器套接字描述符字符串作为数组的第一个元素,并把数组赋值给sockets
$this->sockets=array($this->master);
}
//对创建的socket循环进行监听,处理数据
function run(){
//死循环,直到socket断开
while(true){
//创建数组changes并将sockets复制给它
$changes=$this->sockets;
$write=NULL;
$except=NULL;
/*
//这个函数是同时接受多个连接的关键,传入需要监听的一组socket套接字描述符,监听他们的变化
socket_select ($sockets, $write = NULL, $except = NULL, NULL);
$sockets可以理解为一个数组,这个数组中存放的是文件描述符。当它有变化(就是有新消息到或者有客户端连接/断开)时,socket_select函数才会返回,继续往下执行。
$write是监听是否有客户端写数据,传入NULL是不关心是否有写变化。
$except是$sockets里面要被排除的元素,传入NULL是”监听”全部。
最后一个参数是超时时间
如果为0:则立即结束
如果为n>1: 则最多在n秒后结束,如遇某一个连接有新动态,则提前返回
如果为null:如遇某一个连接有新动态,则返回
*/
//此方法为监听参数一中数组列出的所有套接字描述符,最后一个参数为NULL,是阻塞函数,说明每调用一次,任何一个套接字有结果才会返回,否则一直等待不往下走
socket_select($changes,$write,$except,NULL);
//既然方法往下执行,说明肯定有一个套接字可以读写,每次调用socket_select()后read和write或者except数组中会包含最新的可以使用的资源数组,这是方法重点!
//开始遍历套接字数组中的所有套接字描述符
foreach($changes as $sock){
//echo "遍历的套接字数组值:".$sock."\r\n";
//echo "服务器套接字数组值:".$this->master."\r\n";
//这个是来判断当前这个可操作的套接字描述符是否是服务器相关的套接字描述符,并把通信过的客户端套接字描述符分别存入sockets数组和users数组中
if($sock==$this->master){
//接收一个连接服务器的客户端的套接字,返回客户端套接字的套接字描述符
$client=socket_accept($this->master);
//给新连接进来的socket一个唯一的ID,函数作用为根据当前时间戳生成一个唯一数值
$key=uniqid();
$this->sockets[]=$client; //将新连接进来的socket存进连接池:这样除了池子里的服务器套接字描述符,还有了这个客户端的套接字描述符
echo "sockets[]数组值:\r\n ";
print_r($this->sockets);
//这里users数组的成员是一个键值对的格式,键是唯一值,值是一个Map,记录了每个客户端的套接字描述符和握手标志(boolean类型)
$this->users[$key]=array(
'socket'=>$client, //记录新连接进来client的socket信息
'shou'=>false //标志该socket资源没有完成握手
);
echo "users[]数组值:\r\n ";
print_r($this->users);
//否则1.为client断开socket连接,2.client发送信息:这是除服务器套接字之外的其他套接字信号
}else{
$len=0;
$buffer='';
//读取该socket的信息,注意:第二个参数是引用传参即接收数据,第三个参数是接收数据的长度
do{
//读取该套接字的消息一次,读取长度为1000,返回接收的字节数并赋值给buf
$l=socket_recv($sock,$buf,1000,0);
$len+=$l;//将字节数长度赋值给变量$len
$buffer.=$buf;//将接受的字节数据赋给变量buffer
//循环条件:如果$1不足1000,说明数据全读完了,结束循环
}while($l==1000);
//循环结束,$buffer值为这次客户端套接字读取的所有信息$len为读到的信息的数据总长度
//根据当前连接的套接字描述符,查找取出users数组中对应的唯一键Key
$k=$this->search($sock);
//如果接收的信息长度小于7,则该client的socket为断开连接(这个逻辑是自己自定义的,根据项目需求)
if($len<7){
//给该client的socket进行断开操作,并在$this->sockets和$this->users里面进行删除
$this->send2($k);
continue;
}
//如果逻辑走到这里,说明了既不是与服务器建立连接,也不是要断开连接,而是传数据
//判断该socket是否已经握手,
if(!$this->users[$k]['shou']){
//如果没有握手,则进行握手处理,传了两个参数一个是套接字键Key,第二个是刚才遍历接收的数据,一般是客户端webSocket升级协议请求的字符信息
$this->woshou($k,$buffer);
}else{
//走到这里就是该client发送信息了,对接受到的信息进行uncode处理(解码处理)
//之所以进行解码是因为除了协议有关的信息外其他发送的消息都进行了传输加密,必须解密才能得到真实信息
$buffer = $this->uncode($buffer,$k);
if($buffer==false){
continue;
}
//如果不为空,则进行消息推送操作
$this->send($k,$buffer);
}
}
}
}
}
//指定关闭$k对应的socket,该方法在执行后,users数组和sockets数组中的套接字描述符值都会被删除,且该套接字会关闭连接
function close($k){
//断开相应socket
socket_close($this->users[$k]['socket']);
//删除相应的user信息
unset($this->users[$k]);
//重新定义sockets连接池
$this->sockets=array($this->master);
foreach($this->users as $v){
$this->sockets[]=$v['socket'];
}
//输出日志
$this->e("key:$k close");
}
//根据sock在users里面查找相应的$k
function search($sock){
//遍历套接字数组Users的键值对,判断是否与其中的某个套接字描述符相等,如果相等则返回当是生成的唯一键:
foreach ($this->users as $k=>$v){
if($sock==$v['socket'])
return $k;
}
return false;
}
//传相应的IP与端口进行创建socket操作,返回服务器的套接字描述符
function WebSocket($address,$port){
//创建一个套接字,返回套接字描述符
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//设置套接字相关参数,以套接字描述符作为入参
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的数据包
//绑定套接字到一个端口,以套接字描述符作为入参,作为服务器
socket_bind($server, $address, $port);
//监听套接字,以套接字描述符作为入参
socket_listen($server);
$this->e('Server Started : '.date('Y-m-d H:i:s'));
$this->e('Listening on : '.$address.' port '.$port);
return $server;
}
/*
* 函数说明:对client的请求进行回应,即握手操作
* @$k clien的socket对应的健,即每个用户有唯一$k并对应socket
* @$buffer 接收client请求的所有信息
*/
function woshou($k,$buffer){
echo "接收的值:"."\r\n";
print_r($buffer);
echo "\r\n";
//截取Sec-WebSocket-Key的值并加密,其中$key后面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串应该是固定的
$buf = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);
$key = trim(substr($buf,0,strpos($buf,"\r\n")));
echo "接收的值:"."\r\n";
print_r($key);
echo "\r\n";
$new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));//经过散列加密后形成新的key值,散列加密的密钥是第二个参数
//以下是字符串拼一个服务器相应的协议信息(客户端请求的是webSokect升级协议,我们要拼出一个成功建立连接的信息响应给客户端)
//按照协议组合信息进行返回
$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]['shou']=true;
return true;
}
//解析客户端发来的frame数据格式并用掩码获取解码后的数据
function uncode($str,$key){
$mask = array();
$data = '';
$msg = unpack('H*',$str);
$head = substr($msg[1],0,2);
if ($head == '81' && !isset($this->slen[$key])) {
$len=substr($msg[1],2,2);
$len=hexdec($len);//把十六进制的转换为十进制
if(substr($msg[1],2,2)=='fe'){
$len=substr($msg[1],4,4);
$len=hexdec($len);
$msg[1]=substr($msg[1],4);
}else if(substr($msg[1],2,2)=='ff'){
$len=substr($msg[1],4,16);
$len=hexdec($len);
$msg[1]=substr($msg[1],16);
}
$mask[] = hexdec(substr($msg[1],4,2));
$mask[] = hexdec(substr($msg[1],6,2));
$mask[] = hexdec(substr($msg[1],8,2));
$mask[] = hexdec(substr($msg[1],10,2));
$s = 12;
$n=0;
}else if($this->slen[$key] > 0){
$len=$this->slen[$key];
$mask=$this->ar[$key];
$n=$this->n[$key];
$s = 0;
}
$e = strlen($msg[1])-2;
for ($i=$s; $i<= $e; $i+= 2) {
$data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2)));
$n++;
}
$dlen=strlen($data);
if($len > 255 && $len > $dlen+intval($this->sjen[$key])){
$this->ar[$key]=$mask;
$this->slen[$key]=$len;
$this->sjen[$key]=$dlen+intval($this->sjen[$key]);
$this->sda[$key]=$this->sda[$key].$data;
$this->n[$key]=$n;
return false;
}else{
unset($this->ar[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);
$data=$this->sda[$key].$data;
unset($this->sda[$key]);
return $data;
}
}
//生成包含了服务器待发送未加密数据的frame数据格式字符串,不做掩码加密处理
function code($msg){
$frame = array();
$frame[0] = '81';
$len = strlen($msg);
if($len < 126){
$frame[1] = $len<16?'0'.dechex($len):dechex($len);
}else if($len < 65025){
$s=dechex($len);
$frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
}else{
$s=dechex($len);
$frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
}
$frame[2] = $this->ord_hex($msg);
$data = implode('',$frame);
return pack("H*", $data);
}
//加密函数用的辅助类,没啥好说的
function ord_hex($data) {
$msg = '';
$l = strlen($data);
for ($i= 0; $i<$l; $i++) {
$msg .= dechex(ord($data{$i}));
}
return $msg;
}
//用户加入或client发送信息,此时发送的消息体$msg是若干参数组合成的字符串,之所以是这种格式是自己根据业务逻辑需求自定义的数据格式。
//自定义的消息格式:
// Array(
// type :"add", //请求方法,比如add是加入聊天室,rmove是离开聊天室,
// ming :"焦爷", //请求人
// nr :"大家好", //聊天内容
// key :"all" , //消息指向:比如消息发给小明,或是发给大家
// nrong:$key //删除的指定$users数组的key
// )
function send($k,$msg){
echo "client发送信息的值:"."\r\n";
print_r($msg);
echo "\r\n";
//将查询字符串解析到第二个参数变量中,以数组的形式保存如:parse_str("ming=Bill&nr=大家好",$arr)
parse_str($msg,$g);//把msg里的参数字符串解析到数组$g中
$ar=array();
echo "client信息解析后的g值:"."\r\n";
print_r($g);
echo "\r\n";
//如果是type是add说明是加入聊天室,进入以下逻辑
if($g['type']=='add'){
//第一次进入添加聊天名字,把姓名保存在相应的users里面,即把当前传过来的名字加进uers数组中相关key添加了个name键值对
$this->users[$k]['name']=$g['ming'];
$ar['type']='add';
$ar['name']=$g['ming'];
$key='all';
}else{
//否则就是消息发送
//发送信息行为,其中$g['key']表示面对大家还是个人,是前段传过来的信息
$ar['nrong']=$g['nr'];//取消息体存到ar的nrong
$key=$g['key'];////取出消息目标参数存到key里
}
//推送信息,传入了三个参数:1,users数组Key,2,消息体数组(Map类型),3,消息接收人的users数组对应的Key值
$this->send1($k,$ar,$key);
}
//对新加入的client推送已经在线的client:把users数组数据取出来,包装成另外一种格式数组返回
//格式为 array([i]=>{code:唯一键,name:用户名},...)
function getusers(){
$ar=array();
foreach($this->users as $k=>$v){
$ar[]=array('code'=>$k,'name'=>$v['name']);
}
return $ar;
}
//$k 发信息人的socketID $key接受人的 socketID ,根据这个socketID可以查找相应的client进行消息推送,即指定client进行发送
/*你可能会疑惑,这个接收人的key来自于数组users的成员key值,然而这个服务器端的key值客户端怎么会知道,并当成参数传给服务器呢?
其实不然,仔细看代码会发现,客户端每个成员的在加入服务器连接时,都发送了两次数据:
1,先向服务器发送请求升级到webSocket协议的字符串协议数据,然后服务器返回协议成立的字符串过去(俗称握手)
2,发送带有type=add的参数的字符数据,服务器收到并返回了格式为 array([i]=>{code:唯一键,name:用户名}的数组数据
没错,客户端一加入服务端连接,立马发送add数据,并同时得到了服务器响应来的所有的users成员key值,自然可以作为传参传过来
而这个过程的实现,就在 下面send1方法的一个判断分支里
*/
function send1($k,$ar,$key='all'){
$ar['code1']=$key;
$ar['code']=$k;
$ar['time']=date('m-d H:i:s');
//对发送信息进行编码处理
$str = $this->code(json_encode($ar));
//面对大家即所有在线者发送信息
if($key=='all'){
//得到在线socket数组
$users=$this->users;
//如果是add表示新加的client
if($ar['type']=='add'){
$ar['type']='madd';
$ar['users']=$this->getusers(); //取出所有在线者,用于显示在在线用户列表中:格式为 array([i]=>{code:sers唯一键,name:用户名},...)
$str1 = $this->code(json_encode($ar)); //单独对新client进行编码处理,数据不一样,俗称数据加密
/*对新client自己单独发送,因为有些数据是不一样的
解释一下,从发消息的源头socket节点读add等数据,然后服务器把自己的用户菜单打包发给了消息源头,
就是"客户端发送了一个加入的信号,服务器收到信号并顺着这个地址顺便将服务器上的所有连接者的唯一标识和姓名(群成员信息)传给了这个请求加入的人"*/
socket_write($users[$k]['socket'],$str1,strlen($str1));
//上面已经对client自己单独发送的,后面就无需再次发送,故unset
unset($users[$k]);
/*意思是说,服务器收到了信息要转发给其他所有客户端sokct,告诉他们人员列表更新了,有伙伴加入进来了。
因为自己更新了目录,所以不用在发送了,从遍历中移除。
*/
}
//除了新client外,对其他client进行发送信息。数据量大时,就要考虑延时等问题了
foreach($users as $v){
socket_write($v['socket'],$str,strlen($str));
}
}else{
//单独对个人发送信息,即双方聊天
//至于为什么是发送者和接收者两端都写数据是因为聊天双方的窗口都要写入聊天的内容
socket_write($this->users[$k]['socket'],$str,strlen($str));
socket_write($this->users[$key]['socket'],$str,strlen($str));
}
}
//用户退出向所用client推送信息
function send2($k){
//删除相应的套接字描述符连接
$this->close($k);
$ar['type']='rmove';
$ar['nrong']=$k;
$this->send1(false,$ar,'all');
}
//记录日志
function e($str){
//$path=dirname(__FILE__).'/log.txt';
$str=$str."\n";
//error_log($str,3,$path);
//编码处理
echo iconv('utf-8','gbk//IGNORE',$str);
}
}
?>
后台启动它:
客户端网页:liaotianshi.html
HTML5 websocket 网页聊天室 javascript php

效果: