如何用PHP 实现网页实时聊天
最新修改日期为二零二一年五月二十六日九时五十三分五十三秒 责任编辑为枕边书
HTML5新增的功能备受瞩目,该特性极为新颖,它突破了传统HTTP“请求-响应”的运作模式,实现了服务器主动向客户端发送信息,本文将阐述如何借助PHP与JS技术,构建一个具备实时通讯功能的网页聊天系统。
前言
近期总算抽空,补全了先前构建的“请求-原样返回”服务系统,通过增强了客户端操作,现将实施过程与设计理念陈述如下,同时介绍相关领域资讯,虽然当前探讨此类话题的文章已相当丰富,部分抽象理论便不再赘述,特列推荐文献供参考。
在正式内容呈现之前,先展示一张聊天室界面截图,不必在意页面的样式设计比较粗糙。

简介
这并非一项技术,而是种全新机制。它借助 TCP 的套接字,赋予网络应用一项关键功能:实现客户端与服务器端之间的双向互动与数据交换。这属于继 Java、Adobe Flash、各类 Comet 技术面世之后,服务器向客户端推送信息的新兴方向。
与http的关系
网络分层结构里,http 协议和应用层其他协议同属一个层级,这些协议都以 tcp 传输层为基础,不过,在建立连接环节,会借助 http 的 101 特性完成协议的转换,从 HTTP 协议转变为通信协议,这种转换过程在协议规范里称作“握手”
接洽建立之后,便依照固有规范展开联络,与超文本传输协议全然无关。
握手
以下是一个我自己的浏览器发送的典型的握手 http 头:

服务器接收到握手申请时,会从申请信息中找出 “Sec--Key” 部分内容,接着加上一个固定的文本 ‘-E914-47DA-95CA-',随后用 sha1 算法进行计算,把计算结果转为特定格式,这样得到的就是 key,会以 “Sec--” 名称发送给对方,对方验证这个 key 后,连接就建立起来了,握手过程也就完成了
数据传输
有特定的数据传输规则,叫做 帧,下图展示了这种帧的构造,每个部分都以比特为单位
那个东西一直放在那里,就是不动,我们也不知道它到底是怎么回事,它好像被固定住了,谁也弄不明白它为什么一直那样,简直让人头疼,太奇怪了
零壹贰叁肆伍陆柒捌玖零壹贰叁肆伍陆柒捌玖零壹贰叁肆伍陆柒捌玖零壹
那个庞大的建筑群,由无数单元组成,每个单元内部都设有独立的起居空间,并且配备了齐全的生活设施,整体结构呈现出一种高度规整的布局特点,这种规划方式充分体现了设计者的匠心独运之处。
这个序列由五个字符组成,前四个字符都是R,第五个字符是M,紧接着是一个表示长度的数字,后面还有一些空格
我观察到了一些情况,其中包含多个方面,具体细节如下,需要分别考虑,并且有不同的数量要求,总共分为两种情况。
数量达到特定值时,需要特别处理,这种情况出现在长度等于126或127的情况之下,对此要进行判断,并执行相应操作,确保程序正常运行,避免出现错误,维护数据完整性,这是系统设计的关键环节,必须引起重视,不能忽视。
把表格的行列标记分开来看,第一行包含三个项目,第二行有一个项目叫K,其他部分是空白区域
将原句中的长句拆分成多个小分句,每个分句之间用逗号隔开,同时避免使用原文中已经出现过的词语,并对句子结构和用词进行调整和充实,以实现最大程度的差异化改写,但必须保留原句的核心含义,不更改专有名词,不插入英文单词,保持原文的语言风格,去掉最前面的序号,并在句子末尾保留必要的标点符号。
| , if len == 127 |

这段文字由两个部分组成,前半部分是一串连在一起的短横线,后半部分是一个长横线后面跟着字母和数字的组合。
|key启用时,若掩码设为1
这个句子非常简短,只有一个加号和两个破折号,没有实际内容需要改写。如果需要改写一个有实际内容的句子,请提供完整的句子,我会按照您的要求进行改写。
| -秘钥 () | 信息 |
+ 这是一条由加号和连字符组成的简单标记。
信息记录,包含了全部内容,涵盖了众多细节,涉及多个方面,展现了完整面貌,具有广泛意义。
这段文字的后面是一连串的破折号,总共有十五个之多,每个破折号都单独占据一个位置,它们排成一排,形成了一种视觉上的延伸感,让人感觉仿佛可以一直看到尽头,但实际上它们并没有终点,只是简单地重复着相同的符号,给人一种单调却又持续的感觉。
信息记录涵盖了全部内容,其中详细数据贯穿始终,具体细节十分丰富,整体构成完整体系。
这句话可以改写成多个短句,用逗号隔开,例如:这句话非常长,需要拆分成好几个小句子,才能保持清晰度,同时也要注意不要遗漏句末的标点符号。
详细每个字段代表什么内容,感兴趣的可以查阅这篇文章 The 5.数据帧 觉得自己对二进制的处理还不够熟练,因此没有尝试自己编写算法来解析数据,接下来的数据帧解析和封装工作都是参考了网络上的方法完成的
然而,在处理支付网关相关事务时,我常常需要进行数据进制转换,这方面知识必须认真梳理归纳,嗯,先记录下来。
PHP 实现 服务器
PHP 实现 的话,主要是应用 PHP 的 函数库:
PHP 的 函数集合与 C 语言的 函数大体相同,之前曾粗略阅读过 APUE, 因此觉得其内容不难掌握。在 PHP 的官方文档中浏览一下 函数,相信大家也能对 php 的 编程方式有所了解。
下面会在代码中对所用函数进行简单的注释。
文件描述符
忽然提及'文件描述符',大家可能会有些奇怪。
但作为服务器,必须记住已接入的每一个连接。每个连接对应一位用户,怎样把用户资料和连接关联起来并检索,便成了个难题。此时可以借助文件描述符的一些小窍门来处理。
我们清楚 linux 以'万物皆文件'为核心理念怎么用php做网站,C 语言在其中的体现便是若干个'文件描述符',这些描述符通常是按打开文件时序递增的整数,数值从零开始不断累加(不过系统所能管理的数量是有限的)。每项内容均关联一个文档,执行读取或写入时都是针对关联的文档进行,因此可以像文件系统那样使用读和写功能。
在linux系统里,输入流关联到的是编号为0的文件接口,输出流关联到的是编号为1的文件接口,而错误流关联到的是编号为2的文件接口怎么用php做网站,因此我们可以借助0 1 2这三个编号来对输入输出进行重新定向。
C 语言的特性 PHP 也会继承,它生成的也是值像 4、5 这样的资源类型。我们可以借助强制类型转换或空函数将其变为一个独立的标识符,这样就能通过一个“类别关联数组”来存放资源和相应的用户资料。
结果类似:
$connected_sockets = array(
(int)$socket => array(
'resource' => $socket,
'name' => $name,
'ip' => $ip,
'port' => $port,
...
)
)
创建服务器

下面是一段创建服务器 的代码:
建立一个 TCP 连接, 相关参数的详细说明在官方指南中有完整描述, 本处不作赘述 建立主套接字,使用IPv4协议族,选择流式套接字类型,基于TCP传输控制协议进行初始化 // 设置IP和端口重用,在重启服务器后能重新使用此端口; 设置主套接字选项,协议族为SOL_SOCKET,选项名称为SO_REUSEADDR,选项值为1 // 将IP和端口绑定在服务器socket上; 绑定主套接字到指定地址和端口,地址为$host,端口为$port。 listen函数能够将主动发起连接的套接字转变为被动等待连接的套接字,让这个套接字可以被其他套接字所访问,以便完成服务器功能。接下来的参数用于设定同时处理的最大待处理套接字数量,在并发量大的情况下,这个数值可以适当调大,不过它的大小也受到系统环境的限制。 启动监听端口,绑定到主套接字,限定最大连接数量为预设值
因此,我们获得一个服务器,一旦有客户端接入该服务器,它就会转变为可读状态,接下来就要依据服务器的处理流程来执行了。
服务器逻辑
这里着重讲一下 ($read, $write, $, $
, $
):
函数采用常规 方式,可读、写入、异常的 内容会依次存入 $, $write, $ 这三个数组里,接着会输出状态发生变动的 数量,一旦出现故障,该函数便会给出 false 作为结果
要留意最后两个时间值,它们仅计量单位有别,能够相互配合,用以衡量阻塞延续的时间,数值为零则该函数即刻终止,适合用于循环查询的场合,若设定为空值,函数将持续等待,直至收到可处理的响应,在此我们设定参数为空,使其保持等待状态,直到出现可执行的输出结果
下面是服务器的主要逻辑:
$write = $except = NULL;
通过数组列函数,提取对象中 sockets 属性的每个元素的 resource 键值,从而得到所有 socket 资源的集合
读取数量通过socket选择函数获得,该函数接收四个参数,分别是套接字集合,写入集合,异常集合和超时时间,结果赋值给变量read_num
对于每一个socket, 都进行处理, 依次遍历, 逐个检查, 不遗漏任何一个
// 如果可读的是服务器 socket, 则处理连接逻辑;
当套接字与主套接字相同时,

socket_accept($this->master);
socket_accept() 接收处于监听状态的 socket(例如服务器 socket)发起的连接, 创建一个客户端 socket, 出现问题时返回值将为假
self::connect($client);
continue;
}
若当前可接收的是另一已建立连接的通道,则获取该通道的信息,并执行相应的响应操作
} else {
函数 socket_recv() 从指定的 socket 接收 len 字节的数据量,然后将这些数据存放到变量 $buffer 内部。
接收到的字节数存储在$bytes变量中,通过$socket对象和$buffer缓冲区完成,数据长度上限为2048个字符,操作模式为0
if ($bytes < 9) {
// 当客户端忽然中断时,服务器会接收到一个 8 字节长度的消息(由于其数据帧机制,8字节的消息我们认为它是客户端异常中断消息),服务器处理下线逻辑,并将其封装为消息广播出去
$recv_msg = $this->disconnect($socket);
} else {
// 如果此客户端还未握手,执行握手逻辑
当($socket)的值不是整数时,如果($this->sockets)数组中对应元素的握手状态为假,
执行自我握手操作,参数为$socket和$buffer
continue;
} else {
接收到的消息通过解析函数处理,结果赋值给变量 recv_msg,解析对象为 buffer,调用的是类静态方法 parse
}
}
// 广播消息
$this->broadcast($msg);
}
}
}

这段仅是服务器消息处理的底层实现,并未包含日志记录和异常管理部分,同时还有数据包解析与打包的技术,或许并非所有人都感兴趣,若有人愿意,可以前往相关平台查看我的代码。
另外,为了方便服务器和客户端之间进行信息交换,我自行设计了一种基于json结构的通讯协议,其样式如下:
$msg = [
类型为消息类型,包括常规信息,进入或离开通知,系统通告
消息源自资源库,具体为msg_resource。
消息主体信息为该变量值, // 具体内容如下
用户数组为$uname_list,这有助于实现当前在线者数量与姓名的同步功能。
];
客户端创建客户端
前端我们借助 js 操作十分便捷地发起一个请求,服务器将主动完成连接建立与初始化协商,js 通过事件系统来管理浏览器和服务器之间的数据往来
// 创建一个 websocket 连接
创建一个网络连接实例,其地址为"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) 这个操作即可。
页面功能
页面设计注重提升用户体验,为此为消息输入区域设置了一项功能,用户在按下确认键后,消息会立即被提交,无需额外操作
function confirm(event) {
var key_num = event.keyCode;
if (13 == key_num) {
send();
} else {
return false;
}
}
还有用户打开客户端时生成一个默认唯一用户名;
接下来涉及对信息的分析组织,以及用户端界面的调整变动,这些内容在此不再详细说明,若有关注可以查阅程序代码。
用户名异步处理
当前在用户登录环节,关于用户名确认存在一个细节问题,原先计划在客户端建立连接后立即将用户名发送至服务器,然而控制台却显示“连接尚未建立或已中断”的异常提示。
要继续发送到空字符串:仍然处于该状态。
连接状态或许尚未稳定,因此我尝试通过调用休眠功能暂停执行一秒钟,随后重新提交了账号信息,但问题依旧没有解决。
后来忽然领悟到 js 的单线程特性会造成阻塞,发现单纯使用 sleep 函数来暂停也是徒劳,应该充分发挥 js 的事件驱动模式才是正确思路:于是服务器端增加了新的处理流程,在建立连接后立即通知客户端连接已经建立;客户端先将用户名保存在一个公共变量里,等收到服务器确认连接成功的通知后再发送用户名,这样就能确保用户名在最短时间内被更新。
总结
聊天室扩展方向
基础聊天平台现已建成,务必为其规划一个充满憧憬的灿烂前景,期待有人能够促成

