0

0

PHP+Socket系列之实现websocket聊天室

藏色散人

藏色散人

发布时间:2023-02-02 16:39:51

|

4382人浏览过

|

来源于learnku

转载

本篇文章给大家带来了关于php+socket的相关知识,其中主要介绍了怎么使用php原生socket实现一个简易的web聊天室?感兴趣的朋友下面一起来看一下,希望对大家有帮助。                          

php原生socket实现websocket聊天室

前言

这篇文章实现了使用php原生socket实现了一个简易的web聊天室,最终代码在文章最底部。

不出意外的话这应该是这个系列文章的最后一篇了,写这个系列文章时本以为是很简单的东西,但实际几篇写下来使我几乎通读了 workerman 的代码,所以永远不要眼高手低,一定还是要自己尝试,最好是写出来,才能证明自己真正的弄懂了一件事情

websocket介绍

立即学习PHP免费学习笔记(深入)”;

webSocket 协议是一种网络通信协议,在 2008 年诞生,2011 年成为国际标准,RFC6455 定义了它的通信标准,如今所有浏览器都已支持了该协议。webSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工[^1]通讯的协议,服务器可以主动向客户端推送消息,客户端也可以主动向服务端发送消息。
webSocket 约定了一个通信协议的规范,通过握手机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似 tcp 的连接,从而方便 cs 通信。

为什么需要websocket

HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了 请求 => 响应 模型,通信请求仅能由客户端发起,服务端对请求做出应答处理,这种通信模型有一个弊端:无法实现服务端主动向客户端发起消息。传统的 HTTP 请求,其并发能力都是依赖同时发起多个 TCP 连接访问服务器实现的而 websocket 则允许我们在一条 ws 连接上同时并发多个请求,即在 A 请求发出后 A 响应还未到达,就可以继续发出 B 请求。由于 TCP 的慢启动特性,以及连接本身的握手损耗,都使得 websocket 协议的这一特性有很大的效率提升。

http与websocket对比

特点

  • 建立在 TCP 协议之上,服务端的实现相对比较容易

  • 与 HTTP 协议有良好的兼容性,默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽,能通过各种 HTTP 代理服务器。

  • 数据格式比较轻量,性能开销小,通信高效。

  • 可以发送文本,也可以发送二进制数据。

  • 没有同源限制,客户端可以与任意服务器进行通信。

  • 协议标识符是 ws(如果加密则为 wss),服务地址就是 URL。

PHP实现websocket

客户端与服务端握手

websocket 协议在连接前需要握手[^2],通常握手方式有以下几种方式

  • 基于 flash 的握手协议(不建议)

  • 基于 md5 加密方式的握手协议

    较早的握手方法,有两个 key,使用 md5 加密

  • 基于 sha1 加密方式的握手协议

    当前主要的握手协议,本文将以此协议为主

    • 获取客户端上报的 Sec-WebSocket-key

    • 拼接 key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11

    • 对字符串做 SHA1 计算,再把得到的结果通过 base64 加密,最后再返回给客户端

      Bandy AI
      Bandy AI

      全球领先的电商设计Agent

      下载

客户端请求信息如下:

GET /chat HTTP/1.1Host: 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
Sec-WebSocket-Version: 13Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

我们根据此协议通过 PHP 方式实现:

相关语法解释可参考 之前的文章,本文章不做详细介绍。

使用前端测试,打开我们的任意浏览器控制台(console)输入以下内容,返回的 websocket 对象的 readyState 为 1 即为握手成功,此为前端内容,本文不多做介绍,详情可参考 菜鸟教程

console.log(new WebSocket('ws://192.162.2.166:8888'));
// 运行后返回:
WebSocket {
    binaryType: "blob"
    bufferedAmount: 0
    extensions: ""
    onclose: null
    onerror: null
    onmessage: null
    onopen: null
    protocol: ""
    readyState: 1
    url: "ws://192.162.2.166:8888/"}

发送数据与接收数据

使用 websocket 协议传输协议需要遵循特定的格式规范,详情请参考 datatracker.ietf.org/doc/html/rfc6...

发送数据与接收数据

为了方便,这里直接贴出加解密代码,以下代码借鉴与 workermansrc/Protocols/Websocket.php 文件:

// 解码客户端发送的消息
function decode($buffer)
{
    $len = \ord($buffer[1]) & 127;
    if ($len === 126) {
        $masks = \substr($buffer, 4, 4);
        $data = \substr($buffer, 8);
    } else {
        if ($len === 127) {
            $masks = \substr($buffer, 10, 4);
            $data = \substr($buffer, 14);
        } else {
            $masks = \substr($buffer, 2, 4);
            $data = \substr($buffer, 6);
        }
    }
    $dataLength = \strlen($data);
    $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
    return $data ^ $masks;
}

// 编码发送给客户端的消息
function encode($buffer)
{
    if (!is_scalar($buffer)) {
        throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
    }
    $len = \strlen($buffer);

    $first_byte = "\x81";

    if ($len <= 125) {
        $encode_buffer = $first_byte . \chr($len) . $buffer;
    } else {
        if ($len <= 65535) {
            $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
        } else {
            $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
        }
    }

    return $encode_buffer;
}

我们修改刚才 客户端与服务端握手 阶段的代码,修改后全代码全文如下,该段代码实现了将客户端发送的消息转为大写后返回给客户端(当然只是为了演示):

使用 在线测试工具 进行测试,可以看到消息已经可以正常发送接收,接下来的文章将继续优化代码,实现简易聊天室,敬请关注:

测试截图

实现web聊天室

我们紧接着上文的代码继续优化,以实现简易的web聊天室

多路复用

其实就是加一下 socket_select() 函数 :joy: ,本文就不写原理与语法了,详情可参考 之前的文章,以下代码修改自前文 发送数据与接收数据

...

socket_listen($socket);

+$sockets[] = $socket;
+$user = [];
while (true) {
+   $tmp_sockets = $sockets;
+   socket_select($tmp_sockets, $write, $except, null);

+   foreach ($tmp_sockets as $sock) {
+       if ($sock == $socket) {
+           $sockets[] = socket_accept($socket);
+           $user[] = ['socket' => $socket, 'handshake' => false];
+       } else {
+           $curr_user = $user[array_search($sock, $user)];
+           if ($curr_user['handshake']) { // 已握手
+               $msg = socket_read($sock, 102400);
+               echo '客户端发来消息' . decode($msg);
+               socket_write($sock, encode('这是来自服务端的消息'));
+           } else {
+               // 握手
+           }
+       }
+   }

-   $conn_sock = socket_accept($socket);
-   $request = socket_read($conn_sock, 102400);

...

实现聊天室

最终成果演示

websocket成果演示

我们将上述代码改造成类,并在类变量储存用户信息,添加消息处理等逻辑,最后贴出代码,建议保存下来自己尝试一下,也许会有全新的认知,后端代码:

socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
        socket_bind($this->socket, 0, 8888);
        socket_listen($this->socket);

        // 将 socket 资源放入 socket_list
        $this->socket_list[] = $this->socket;

        while (true) {
            $tmp_sockets = $this->socket_list;
            socket_select($tmp_sockets, $write, $except, null);

            foreach ($tmp_sockets as $sock) {
                if ($sock == $this->socket) {
                    $conn_sock = socket_accept($sock);
                    $this->socket_list[] = $conn_sock;
                    $this->user[] = ['socket' => $conn_sock, 'handshake' => false, 'name' => '无名氏'];
                } else {
                    $request = socket_read($sock, 102400);
                    $k = $this->getUserIndex($sock);

                    if (!$request) {
                        continue;
                    }

                    // 用户端断开连接
                    if ((\ord($request[0]) & 0xf) == 0x8) {
                        $this->close($k);
                        continue;
                    }

                    if (!$this->user[$k]['handshake']) {
                        // 握手
                        $this->handshake($k, $request);
                    } else {
                        // 已握手
                        $this->send($k, $request);
                    }
                }
            }
        }
    }

    /**
     * 关闭连接
     *
     * @param $k
     */
    protected function close($k)
    {
        $u_name = $this->user[$k]['name'] ?? '无名氏';
        socket_close($this->user[$k]['socket']);
        $socket_key = array_search($this->user[$k]['socket'], $this->socket_list);
        unset($this->socket_list[$socket_key]);
        unset($this->user[$k]);

        $user = [];
        foreach ($this->user as $v) {
            $user[] = $v['name'];
        }
        $res = [
            'type' => 'close',
            'users' => $user,
            'msg' => $u_name . '已退出',
            'time' => date('Y-m-d H:i:s')
        ];
        $this->sendAllUser($res);
    }

    /**
     * 获取用户索引
     *
     * @param $socket
     * @return int|string
     */
    protected function getUserIndex($socket)
    {
        foreach ($this->user as $k => $v) {
            if ($v['socket'] == $socket) {
                return $k;
            }
        }
    }

    /**
     * 握手
     * @param $k
     * @param $request
     */
    protected function handshake($k, $request)
    {
        preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);
        $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));

        $response = "HTTP/1.1 101 Switching Protocols\r\n";
        $response .= "Upgrade: websocket\r\n";
        $response .= "Connection: Upgrade\r\n";
        $response .= "Sec-WebSocket-Accept: {$key}\r\n\r\n";
        socket_write($this->user[$k]['socket'], $response);
        $this->user[$k]['handshake'] = true;
    }

    /**
     * 接收并处理消息
     *
     * @param $k
     * @param $msg
     */
    public function send($k, $msg)
    {
        $msg = $this->decode($msg);
        $msg = json_decode($msg, true);

        if (!isset($msg['type'])) {
            return;
        }

        switch ($msg['type']) {
            case 'login': // 登录
                $this->user[$k]['name'] = $msg['name'] ?? '无名氏';
                $users = [];
                foreach ($this->user as $v) {
                    $users[] = $v['name'];
                }
                $res = [
                    'type' => 'login',
                    'name' => $this->user[$k]['name'],
                    'msg' => $this->user[$k]['name'] . ': login success',
                    'users' => $users,
                ];
                $this->sendAllUser($res);
                break;
            case 'message': // 接收并发送消息
                $res = [
                    'type' => 'message',
                    'name' => $this->user[$k]['name'] ?? '无名氏',
                    'msg' => $msg['msg'],
                    'time' => date('H:i:s'),
                ];
                $this->sendAllUser($res);
                break;
        }
    }

    /**
     * 发送给所有人
     *
     */
    protected function sendAllUser($msg)
    {
        if (is_array($msg)) {
            $msg = json_encode($msg);
        }

        $msg = $this->encode($msg);

        foreach ($this->user as $k => $v) {
            socket_write($v['socket'], $msg, strlen($msg));
        }
    }

    /**
     * 解码
     *
     * @param $buffer
     * @return string
     */
    protected function decode($buffer)
    {
        $len = \ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = \substr($buffer, 4, 4);
            $data = \substr($buffer, 8);
        } else {
            if ($len === 127) {
                $masks = \substr($buffer, 10, 4);
                $data = \substr($buffer, 14);
            } else {
                $masks = \substr($buffer, 2, 4);
                $data = \substr($buffer, 6);
            }
        }
        $dataLength = \strlen($data);
        $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
        return $data ^ $masks;
    }

    protected function encode($buffer)
    {
        if (!is_scalar($buffer)) {
            throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
        }
        $len = \strlen($buffer);

        $first_byte = "\x81";

        if ($len <= 125) {
            $encode_buffer = $first_byte . \chr($len) . $buffer;
        } else {
            if ($len <= 65535) {
                $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
            } else {
                $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
            }
        }

        return $encode_buffer;
    }
}

前端代码如下(前端内容不在本文讨论范围之内,具体可参考 菜鸟教程):




    
    
    
    Document




这是一个php socket实现的web聊天室

[^1]:是通讯传输的一个术语。 通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合
[^2]:  为了建立 websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)

推荐学习:《之前的文章0》                                            

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
html5动画制作有哪些制作方法
html5动画制作有哪些制作方法

html5动画制作方法有使用CSS3动画、使用JavaScript动画库、使用HTML5 Canvas等。想了解更多html5动画制作方法相关内容,可以阅读本专题下面的文章。

511

2023.10.23

HTML与HTML5的区别
HTML与HTML5的区别

HTML与HTML5的区别:1、html5支持矢量图形,html本身不支持;2、html5中可临时存储数据,html不行;3、html5新增了许多控件;4、html本身不支持音频和视频,html5支持;5、html无法处理不准确的语法,html5能够处理等等。想了解更多HTML与HTML5的相关内容,可以阅读本专题下面的文章。

436

2024.03.06

html5从入门到精通汇总
html5从入门到精通汇总

想系统掌握HTML5开发?本合集精选全网优质学习资源,涵盖免费教程、实战项目、视频课程与权威电子书,从基础语法到高级特性(Canvas、本地存储、响应式布局等)一应俱全,适合零基础小白到进阶开发者,助你高效入门并精通HTML5前端开发。

69

2025.12.30

html5新老标签汇总
html5新老标签汇总

HTML5在2026年持续优化网页语义化与交互体验,不仅引入了如<header>、<nav>、<article>、<section>、<aside>、<footer>等结构化标签,还新增了<video>、<audio>、<canvas>、<figure>、<time>、<mark>等增强多媒体与

104

2025.12.30

html5空格代码怎么写
html5空格代码怎么写

在HTML5中,空格不能直接通过键盘空格键实现,需使用特定代码。本合集详解常用空格写法:&nbsp;(不间断空格)、&ensp;(半个中文空格)、&emsp;(一个中文空格)及CSS的white-space属性等方法,帮助开发者精准控制页面排版,避免因空格失效导致布局错乱,适用于新手入门与实战参考。

78

2025.12.30

html5怎么做网站教程
html5怎么做网站教程

想从零开始学做网站?这份《HTML5怎么做网站教程》合集专为新手打造!涵盖HTML5基础语法、页面结构搭建、表单与多媒体嵌入、响应式布局及与CSS3/JavaScript协同开发等核心内容。无需编程基础,手把手教你用纯HTML5创建美观、兼容、移动端友好的现代网页。附实战案例+代码模板,快速上手,轻松迈出Web开发第一步!

158

2025.12.31

HTML5建模教程
HTML5建模教程

想快速掌握HTML5模板搭建?本合集汇集实用HTML5建模教程,从零基础入门到实战开发全覆盖!内容涵盖响应式布局、语义化标签、Canvas绘图、表单验证及移动端适配等核心技能,提供可直接复用的模板结构与代码示例。无需复杂配置,助你高效构建现代网页,轻松上手前端开发!

29

2025.12.31

html5怎么使用
html5怎么使用

想快速上手HTML5开发?本合集为你整理最实用的HTML5使用指南!涵盖HTML5基础语法、主流框架(如Bootstrap、Vue、React)集成方法,以及无需安装、直接在线编辑运行的平台推荐(如CodePen、JSFiddle)。无论你是新手还是进阶开发者,都能轻松掌握HTML5网页制作、响应式布局与交互功能开发,零配置开启高效前端编程之旅!

42

2025.12.31

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 10万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.2万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 0.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号