Websocket作为HTML5中的一个新特性,一直备受人们关注,因为它真的很酷,打破了http“请求-响应”的常规思维,实现了服务器主动向客户端推送消息。本文介绍了如何利用PHP和JS应用websocket实现一个网页实时聊天室。
目录
websocket与http握手数据传输关系介绍PHP实现web socket服务器文件描述符创建服务器socket服务器逻辑客户端创建客户端页面函数用户名异步处理总结
前言
最近,我很难“挤出”websocket“请求即服务”服务器,这是我很久以前做的。我用js改进了客户端功能,把过程和想法分享给大家。顺便还普及了websocket相关知识。当然现在讨论websocket的文章很多,我就跳过一些理论上的东西。我会给出参考文章供你选择阅读。
在开始正文之前,贴一张聊天室的效果图(请不要在意CSS渣的页面):
websocket
简介WebSocket不是一种技术,而是一种全新的协议。它使用TCP Socket为网络应用定义了一个新的重要的能力:客户端和服务器之间的双全时传输和双向通信。继Java小程序、XMLHttpRequest、Adobe Flash、ActiveXObject、各种Comet技术之后,服务器推送客户端消息成为新趋势。
与http的关系在网络分层方面,websocket和http协议都是应用层的协议,都是基于tcp传输层。但websocket建立连接时,借用http的101 switch协议实现协议升级,从HTTP协议切换到WebSocket通信协议。这个动作协议被称为“握手”。
握手成功后,websocket使用自己的协议进行通信,与http无关。
握手以下是我自己浏览器发的典型握手http头:
收到握手请求后,服务器从请求头中提取“Sec-WebSocket-Key”字段,恢复一个固定字符串“258 ea 5 a 5-e914-47da-95ca-C5 ab 0 DC 85 b 11”,然后用sha1加密,最后将其转换为base64代码,并将其作为带有“Sec-WebSocket-Accept”字段的密钥返回给
数据传输Websocket有自己的数据传输格式,称为帧。下图显示了数据帧的结构,单位为比特:
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|操作码|M|有效负载长度|扩展有效负载长度|
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| |(如果有效载荷len==126/127) |
| |1|2|3| |K| | |
- - - - - - - - - - - - - - - - - - - - - -
|有效载荷长度延长,如果有效载荷长度==127 |
- - - - - - - - - - - - - - - -
| |屏蔽键,如果屏蔽设置为1 |
- -
|屏蔽键(续)|有效载荷数据|
- - - - - - - - - - - - - - - -
:有效负载数据续.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|有效载荷数据(续).|
-
每个字段是什么意思?如果你有兴趣,你可以阅读这篇文章。数据帧觉得二进制运算不是很灵活,就不挑战自己写算法解析数据了。下面的数据帧解析和封装都是在线算法。
但是,在工作中,我仍然经常使用支付网关中数据的小数运算。这一点必须认真研究和总结。嗯,先写下来。
PHP 实现 websocket 服务器
如果PHP实现websocket,主要应用PHP的socket函数库:
PHP的socket函数库和C语言的socket函数非常相似。我之前翻了一个APUE,所以我觉得挺好理解的。看php手册里的socket函数。我想你也可以对PHP的socket编程有一定的了解。
下面将简单评论代码中使用的函数。
文件描述符大家突然提到‘文件描述符’可能会有点奇怪。
但是作为服务器,需要存储和识别连接的套接字。每个套接字代表一个用户,如何关联和查询用户信息与套接字的对应关系是一个问题。这里,应用了一些关于文件描述符的技巧。
我们知道linux是‘一切都是文件’,C语言的socket实现是一个个‘文件描述符’。这个文件描述符一般是一个int值,按照打开文件的升序排列,从0开始一直递增(当然系统是有限的)。每个套接字对应一个文件,读写套接字是操作对应的文件,所以读写功能也可以像文件系统一样应用。
温馨提示:在Linux中,标准输入对应文件描述符0;对应于标准输出的文件描述符是1;错误对应的文件描述符是2;所以我们可以使用0 1 2来重定向输入和输出。
那么类似C socket的PHP socket自然继承了这一点,它创建的socket也是int值为4 5的资源类型。我们可以用(int)或intval()函数将socket转换成唯一的ID,这样就可以用一个‘类索引数组’来存储socket资源和相应的用户信息;
结果是相似的:
$connected_sockets=array(
(int)$socket=array(
resource=$socket,
name=$name,
ip=$ip,
port=$port,
.
)
)
创建服务器socket以下是创建服务器套接字的代码:
//创建TCP套接字。这个函数的可选值在官方文档里写的很详细,这里就不多说了。
$ this-master=socket _ create(AF _ INET,SOCK_STREAM,SOL _ TCP);
//设置IP和端口复用,重启服务器后可以复用这个端口;
socket_set_option($this-master,SOL_SOCKET,SO_REUSEADDR,1);
//将IP和端口绑定到服务器套接字;
socket_bind($this-master,$host,$ port);
listen函数将活动的连接windows套接字更改为已连接的windows套接字,使该套接字可以被其他套接字访问,从而实现服务器功能。后一个参数是要处理的自定义套接字的最大数量。在高并发的情况下,这个值可以设置得更大,尽管它也受到系统环境的约束。
socket_listen($this-master,self:LISTEN _ SOCKET _ NUM);
这样,我们就得到一个服务器套接字。当客户端连接到此套接字时,它会将其状态更改为可读。这取决于服务器接下来的处理逻辑。
服务器逻辑这里我们重点关注socket _ select ($ read,$ write,$ except,$ tv _ sec [,$ tv _ usec]):
选择函数使用传统的选择模型。可读、可写和异常的套接字会放入$ socket、$ write和$ except数组中,然后返回状态改变的套接字数量。如果出现错误,该函数将返回false。
请注意,最后两个时间参数(只是单位不同)可以一起用来指示socket_select阻塞的持续时间。当它为0时,该函数立即返回,可用于轮询机制。当它为空时,函数将一直阻塞。这里,我们将$tv_sec参数设置为null,并让它阻塞,直到一个可操作的套接字返回。
以下是服务器的主要逻辑:
$ write=$ except=NULL
$ sockets=array _ column($ this-sockets, resource );//获取所有套接字资源
$ read _ num=socket _ select($ sockets,$write,$except,NULL);
foreach ($sockets as $socket) {
//如果可读服务器套接字,处理连接逻辑;
if ($socket==$this-master) {
socket _ accept($ this-master);
//socket_accept()接受连接请求“正在监听的套接字(像我们的服务器套接字)”和一个客户端套接字,错误返回false
self:connect($ client);
继续;
}
//如果其他连接的套接字是可读的,则读取它们的数据并处理响应逻辑。
}否则{
//函数socket_recv()从套接字接受长度为len字节的数据,并保存在$buffer中。
$bytes=@socket_recv($socket,$buffer,2048,0);
if(字节9) {
//当客户端突然中断时,服务器会收到一个8字节的消息(由于其数据帧机制,该8字节消息被认为是客户端的异常中断消息),服务器会对离线逻辑进行处理,封装成消息进行广播。
$ recv _ msg=$ this-disconnect($ socket);
}否则{
//如果这个客户端还没有握手,执行握手逻辑。
如果(!$ this-sockets[(int)$ socket][ handshake ]){
self:握手($socket,$ buffer);
继续;
}否则{
$ recv _ msg=self:parse($ buffer);
}
}
//广播消息
$ this-broadcast($ msg);
}
}
}
这里只是服务器处理消息的基本代码。日志记录和异常处理被跳过,有一些解析和封装数据帧的方法。你可能不爱他们。有兴趣可以去github支持我的源码~ ~
另外,为了方便服务器和客户端的交互,我定义了自己的json类型的消息格式,看起来像:
$msg=[
Type=$msg_type,//有一般消息,在线和离线消息,服务器消息。
From=$msg_resource,//source
Content=$msg_content,//消息内容
User_list=$uname_list,//方便同步当前在线人数和姓名。
];
客户端
创建客户端我们只需要在前端用js调用Websocket方法就可以创建一个websocket连接,服务器会完成连接并为我们握手。js使用事件机制来处理浏览器和服务器之间的交互:
//创建websocket连接
var ws=new web socket( ws://127 . 0 . 0 . 1:8080 );
//websocket创建成功事件
ws.onopen=function () {
};
//websocket收到消息事件
ws.onmessage=function (e) {
var msg=JSON . parse(e . data);
}
//websocket错误事件
ws.onerror=function () {
};
发送消息也很简单,直接调用ws.send(msg)方法即可。
页面功能部分页面主要是为了方便用户使用。这里在消息框textarea中添加了一个键盘监听事件,当用户按下回车键时直接发送消息;
功能确认(事件){
var key _ num=event.keyCode
if (13==key_num) {
send();
}否则{
返回false
}
}
还有用户打开客户端时生成的默认唯一用户名;
然后还有一些数据的解析结构,客户端页面的更新,这里就不赘述了。有兴趣可以看看源代码。
用户名异步处理这里不得不提一个用户登录时确定用户名的小问题。我本来想在客户端创建连接后直接把用户名发送给服务器,但是控制台报错“websocket仍然连接或关闭”。
未捕获的DOMException:无法对“WebSocket”执行“send”:仍处于连接状态。
考虑到可能还没有处理连接,我实现了sleep方法,并在发送用户名之前等待了一秒钟,但是错误仍然存在。
后来突然想到js的单线程阻塞机制,意识到一直用睡眠来阻塞是没有用的。利用好js的事件机制才是正道:于是我在服务器端添加了逻辑,握手成功后,向客户端发送握手成功的消息;客户端先将用户名存储到一个全局变量中,然后在收到服务器发送的握手成功的提醒消息后发送用户名,所以第一时间更新用户名成功。
总结
聊天室扩展方向
简单的聊天室完成了,当然会有一个充满希望的美好未来,希望有人实现。
页面美化(给信息增加色彩等。)
服务器识别字符 @ ,只将数据写入一个套接字,实现聊天室的私聊。
多进程(使用redis等缓存数据库来共享资源)
记录消息数据库持久性(日志或不便分析)
以上就是如何使用PHP websocket实现实时网络聊天的细节。关于如何用PHP websocket实现实时网络聊天的更多信息,请关注其他相关文章!